aboutsummaryrefslogtreecommitdiffstats
path: root/server/src/com/vaadin/ui/components/grid
diff options
context:
space:
mode:
Diffstat (limited to 'server/src/com/vaadin/ui/components/grid')
-rw-r--r--server/src/com/vaadin/ui/components/grid/AbstractRenderer.java88
-rw-r--r--server/src/com/vaadin/ui/components/grid/Grid.java1307
-rw-r--r--server/src/com/vaadin/ui/components/grid/GridColumn.java427
-rw-r--r--server/src/com/vaadin/ui/components/grid/GridFooter.java66
-rw-r--r--server/src/com/vaadin/ui/components/grid/GridHeader.java124
-rw-r--r--server/src/com/vaadin/ui/components/grid/GridStaticSection.java425
-rw-r--r--server/src/com/vaadin/ui/components/grid/Renderer.java71
-rw-r--r--server/src/com/vaadin/ui/components/grid/SortOrderChangeEvent.java76
-rw-r--r--server/src/com/vaadin/ui/components/grid/SortOrderChangeListener.java34
-rw-r--r--server/src/com/vaadin/ui/components/grid/renderers/DateRenderer.java152
-rw-r--r--server/src/com/vaadin/ui/components/grid/renderers/HtmlRenderer.java38
-rw-r--r--server/src/com/vaadin/ui/components/grid/renderers/NumberRenderer.java159
-rw-r--r--server/src/com/vaadin/ui/components/grid/renderers/TextRenderer.java39
-rw-r--r--server/src/com/vaadin/ui/components/grid/selection/AbstractSelectionModel.java71
-rw-r--r--server/src/com/vaadin/ui/components/grid/selection/MultiSelectionModel.java138
-rw-r--r--server/src/com/vaadin/ui/components/grid/selection/NoSelectionModel.java54
-rw-r--r--server/src/com/vaadin/ui/components/grid/selection/SelectionChangeEvent.java73
-rw-r--r--server/src/com/vaadin/ui/components/grid/selection/SelectionChangeListener.java35
-rw-r--r--server/src/com/vaadin/ui/components/grid/selection/SelectionChangeNotifier.java43
-rw-r--r--server/src/com/vaadin/ui/components/grid/selection/SelectionModel.java234
-rw-r--r--server/src/com/vaadin/ui/components/grid/selection/SingleSelectionModel.java81
-rw-r--r--server/src/com/vaadin/ui/components/grid/sort/Sort.java153
-rw-r--r--server/src/com/vaadin/ui/components/grid/sort/SortOrder.java106
23 files changed, 3994 insertions, 0 deletions
diff --git a/server/src/com/vaadin/ui/components/grid/AbstractRenderer.java b/server/src/com/vaadin/ui/components/grid/AbstractRenderer.java
new file mode 100644
index 0000000000..d1cf77c24b
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/AbstractRenderer.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui.components.grid;
+
+import com.vaadin.server.AbstractClientConnector;
+import com.vaadin.server.AbstractExtension;
+
+/**
+ * An abstract base class for server-side Grid renderers.
+ * {@link com.vaadin.client.ui.grid.Renderer Grid renderers}. This class
+ * currently extends the AbstractExtension superclass, but this fact should be
+ * regarded as an implementation detail and subject to change in a future major
+ * or minor Vaadin revision.
+ *
+ * @param <T>
+ * the type this renderer knows how to present
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public abstract class AbstractRenderer<T> extends AbstractExtension implements
+ Renderer<T> {
+
+ private final Class<T> presentationType;
+
+ protected AbstractRenderer(Class<T> presentationType) {
+ this.presentationType = presentationType;
+ }
+
+ /**
+ * This method is inherited from AbstractExtension but should never be
+ * called directly with an AbstractRenderer.
+ */
+ @Deprecated
+ @Override
+ protected Class<Grid> getSupportedParentType() {
+ return Grid.class;
+ }
+
+ /**
+ * This method is inherited from AbstractExtension but should never be
+ * called directly with an AbstractRenderer.
+ */
+ @Deprecated
+ @Override
+ protected void extend(AbstractClientConnector target) {
+ super.extend(target);
+ }
+
+ @Override
+ public Class<T> getPresentationType() {
+ return presentationType;
+ }
+
+ /**
+ * Gets the item id for a row key.
+ * <p>
+ * A key is used to identify a particular row on both a server and a client.
+ * This method can be used to get the item id for the row key that the
+ * client has sent.
+ *
+ * @param key
+ * the row key for which to retrieve an item id
+ * @return the item id corresponding to {@code key}
+ */
+ protected Object getItemId(String key) {
+ if (getParent() instanceof Grid) {
+ Grid grid = (Grid) getParent();
+ return grid.getKeyMapper().getItemId(key);
+ } else {
+ throw new IllegalStateException(
+ "Renderers can be used only with Grid");
+ }
+ }
+}
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..3c115f9241
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/Grid.java
@@ -0,0 +1,1307 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.vaadin.ui.components.grid;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+
+import com.google.gwt.thirdparty.guava.common.collect.Sets;
+import com.google.gwt.thirdparty.guava.common.collect.Sets.SetView;
+import com.vaadin.data.Container;
+import com.vaadin.data.Container.PropertySetChangeEvent;
+import com.vaadin.data.Container.PropertySetChangeListener;
+import com.vaadin.data.Container.PropertySetChangeNotifier;
+import com.vaadin.data.Container.Sortable;
+import com.vaadin.data.RpcDataProviderExtension;
+import com.vaadin.data.RpcDataProviderExtension.DataProviderKeyMapper;
+import com.vaadin.server.KeyMapper;
+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.GridState.SharedSelectionMode;
+import com.vaadin.shared.ui.grid.GridStaticCellType;
+import com.vaadin.shared.ui.grid.HeightMode;
+import com.vaadin.shared.ui.grid.ScrollDestination;
+import com.vaadin.shared.ui.grid.SortDirection;
+import com.vaadin.shared.ui.grid.SortEventOriginator;
+import com.vaadin.ui.AbstractComponent;
+import com.vaadin.ui.Component;
+import com.vaadin.ui.HasComponents;
+import com.vaadin.ui.components.grid.GridFooter.FooterCell;
+import com.vaadin.ui.components.grid.GridFooter.FooterRow;
+import com.vaadin.ui.components.grid.GridHeader.HeaderCell;
+import com.vaadin.ui.components.grid.GridHeader.HeaderRow;
+import com.vaadin.ui.components.grid.selection.MultiSelectionModel;
+import com.vaadin.ui.components.grid.selection.NoSelectionModel;
+import com.vaadin.ui.components.grid.selection.SelectionChangeEvent;
+import com.vaadin.ui.components.grid.selection.SelectionChangeListener;
+import com.vaadin.ui.components.grid.selection.SelectionChangeNotifier;
+import com.vaadin.ui.components.grid.selection.SelectionModel;
+import com.vaadin.ui.components.grid.selection.SingleSelectionModel;
+import com.vaadin.ui.components.grid.sort.Sort;
+import com.vaadin.ui.components.grid.sort.SortOrder;
+import com.vaadin.util.ReflectTools;
+
+/**
+ * A grid component for displaying tabular data.
+ * <p>
+ * Grid is always bound to a {@link Container.Indexed}, but is not a
+ * {@code Container} of any kind in of itself. The contents of the given
+ * Container is displayed with the help of {@link Renderer Renderers}.
+ *
+ * <h3 id="grid-headers-and-footers">Headers and Footers</h3>
+ * <p>
+ *
+ *
+ * <h3 id="grid-converters-and-renderers">Converters and Renderers</h3>
+ * <p>
+ * Each column has its own {@link Renderer} that displays data into something
+ * that can be displayed in the browser. That data is first converted with a
+ * {@link com.vaadin.data.util.converter.Converter Converter} into something
+ * that the Renderer can process. This can also be an implicit step - if a
+ * column has a simple data type, like a String, no explicit assignment is
+ * needed.
+ * <p>
+ * Usually a renderer takes some kind of object, and converts it into a
+ * HTML-formatted string.
+ * <p>
+ * <code><pre>
+ * Grid grid = new Grid(myContainer);
+ * GridColumn column = grid.getColumn(STRING_DATE_PROPERTY);
+ * column.setConverter(new StringToDateConverter());
+ * column.setRenderer(new MyColorfulDateRenderer());
+ * </pre></code>
+ *
+ * <h3 id="grid-lazyloading">Lazy Loading</h3>
+ * <p>
+ * The data is accessed as it is needed by Grid and not any sooner. In other
+ * words, if the given Container is huge, but only the first few rows are
+ * displayed to the user, only those (and a few more, for caching purposes) are
+ * accessed.
+ *
+ * <h3 id="grid-selection-modes-and-models">Selection Modes and Models</h3>
+ * <p>
+ * Grid supports three selection <em>{@link SelectionMode modes}</em> (single,
+ * multi, none), and comes bundled with one
+ * <em>{@link SelectionModel model}</em> for each of the modes. The distinction
+ * between a selection mode and selection model is as follows: a <em>mode</em>
+ * essentially says whether you can have one, many or no rows selected. The
+ * model, however, has the behavioral details of each. A single selection model
+ * may require that the user deselects one row before selecting another one. A
+ * variant of a multiselect might have a configurable maximum of rows that may
+ * be selected. And so on.
+ * <p>
+ * <code><pre>
+ * Grid grid = new Grid(myContainer);
+ *
+ * // uses the bundled SingleSelectionModel class
+ * grid.setSelectionMode(SelectionMode.SINGLE);
+ *
+ * // changes the behavior to a custom selection model
+ * grid.setSelectionModel(new MyTwoSelectionModel());
+ * </pre></code>
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class Grid extends AbstractComponent implements SelectionChangeNotifier,
+ HasComponents {
+
+ /**
+ * Selection modes representing built-in {@link SelectionModel
+ * SelectionModels} that come bundled with {@link Grid}.
+ * <p>
+ * Passing one of these enums into
+ * {@link Grid#setSelectionMode(SelectionMode)} is equivalent to calling
+ * {@link Grid#setSelectionModel(SelectionModel)} with one of the built-in
+ * implementations of {@link SelectionModel}.
+ *
+ * @see Grid#setSelectionMode(SelectionMode)
+ * @see Grid#setSelectionModel(SelectionModel)
+ */
+ public enum SelectionMode {
+ /** A SelectionMode that maps to {@link SingleSelectionModel} */
+ SINGLE {
+ @Override
+ protected SelectionModel createModel() {
+ return new SingleSelectionModel();
+ }
+
+ },
+
+ /** A SelectionMode that maps to {@link MultiSelectionModel} */
+ MULTI {
+ @Override
+ protected SelectionModel createModel() {
+ return new MultiSelectionModel();
+ }
+ },
+
+ /** A SelectionMode that maps to {@link NoSelectionModel} */
+ NONE {
+ @Override
+ protected SelectionModel createModel() {
+ return new NoSelectionModel();
+ }
+ };
+
+ protected abstract SelectionModel createModel();
+ }
+
+ /**
+ * 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 current sort order
+ */
+ private final List<SortOrder> sortOrder = new ArrayList<SortOrder>();
+
+ /**
+ * 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());
+ removeExtension(column.getRenderer());
+ }
+ datasourceExtension.propertiesRemoved(removedColumns);
+
+ // Add new columns
+ HashSet<Object> addedPropertyIds = new HashSet<Object>();
+ for (Object propertyId : properties) {
+ if (!columns.containsKey(propertyId)) {
+ appendColumn(propertyId);
+ addedPropertyIds.add(propertyId);
+ }
+ }
+ datasourceExtension.propertiesAdded(addedPropertyIds);
+
+ Object frozenPropertyId = columnKeys
+ .get(getState(false).lastFrozenColumnId);
+ if (!columns.containsKey(frozenPropertyId)) {
+ setLastFrozenPropertyId(null);
+ }
+ }
+ };
+
+ private RpcDataProviderExtension datasourceExtension;
+
+ /**
+ * The selection model that is currently in use. Never <code>null</code>
+ * after the constructor has been run.
+ */
+ private SelectionModel selectionModel;
+
+ /**
+ * The number of times to ignore selection state sync to the client.
+ * <p>
+ * This usually means that the client side has modified the selection. We
+ * still want to inform the listeners that the selection has changed, but we
+ * don't want to send those changes "back to the client".
+ */
+ private int ignoreSelectionClientSync = 0;
+
+ private final GridHeader header = new GridHeader(this);
+ private final GridFooter footer = new GridFooter(this);
+
+ private static final Method SELECTION_CHANGE_METHOD = ReflectTools
+ .findMethod(SelectionChangeListener.class, "selectionChange",
+ SelectionChangeEvent.class);
+
+ private static final Method SORT_ORDER_CHANGE_METHOD = ReflectTools
+ .findMethod(SortOrderChangeListener.class, "sortOrderChange",
+ SortOrderChangeEvent.class);
+
+ /**
+ * Creates a new Grid using the given datasource.
+ *
+ * @param datasource
+ * the data source for the grid
+ */
+ public Grid(final Container.Indexed datasource) {
+ setContainerDataSource(datasource);
+
+ setSelectionMode(SelectionMode.MULTI);
+ addSelectionChangeListener(new SelectionChangeListener() {
+ @Override
+ public void selectionChange(SelectionChangeEvent event) {
+ for (Object removedItemId : event.getRemoved()) {
+ getKeyMapper().unpin(removedItemId);
+ }
+
+ for (Object addedItemId : event.getAdded()) {
+ if (!getKeyMapper().isPinned(addedItemId)) {
+ getKeyMapper().pin(addedItemId);
+ }
+ }
+
+ List<String> keys = getKeyMapper().getKeys(getSelectedRows());
+
+ boolean markAsDirty = true;
+
+ /*
+ * If this clause is true, it means that the selection event
+ * originated from the client. This means that we don't want to
+ * send the changes back to the client (markAsDirty => false).
+ */
+ if (ignoreSelectionClientSync > 0) {
+ ignoreSelectionClientSync--;
+ markAsDirty = false;
+
+ try {
+
+ /*
+ * Make sure that the diffstate is aware of the
+ * "undirty" modification, so that the diffs are
+ * calculated correctly the next time we actually want
+ * to send the selection state to the client.
+ */
+ getUI().getConnectorTracker().getDiffState(Grid.this)
+ .put("selectedKeys", new JSONArray(keys));
+ } catch (JSONException e) {
+ throw new RuntimeException("Internal error", e);
+ }
+ }
+
+ getState(markAsDirty).selectedKeys = keys;
+ }
+ });
+
+ registerRpc(new GridServerRpc() {
+
+ @Override
+ public void selectionChange(List<String> selection) {
+ final HashSet<Object> newSelection = new HashSet<Object>(
+ getKeyMapper().getItemIds(selection));
+ final HashSet<Object> oldSelection = new HashSet<Object>(
+ getSelectedRows());
+
+ SetView<Object> addedItemIds = Sets.difference(newSelection,
+ oldSelection);
+ SetView<Object> removedItemIds = Sets.difference(oldSelection,
+ newSelection);
+
+ if (!removedItemIds.isEmpty()) {
+ /*
+ * Since these changes come from the client, we want to
+ * modify the selection model and get that event fired to
+ * all the listeners. One of the listeners is our internal
+ * selection listener, and this tells it not to send the
+ * selection event back to the client.
+ */
+ ignoreSelectionClientSync++;
+
+ if (removedItemIds.size() == 1) {
+ deselect(removedItemIds.iterator().next());
+ } else {
+ assert getSelectionModel() instanceof SelectionModel.Multi : "Got multiple deselections, but the selection model is not a SelectionModel.Multi";
+ ((SelectionModel.Multi) getSelectionModel())
+ .deselect(removedItemIds);
+ }
+ }
+
+ if (!addedItemIds.isEmpty()) {
+ /*
+ * Since these changes come from the client, we want to
+ * modify the selection model and get that event fired to
+ * all the listeners. One of the listeners is our internal
+ * selection listener, and this tells it not to send the
+ * selection event back to the client.
+ */
+ ignoreSelectionClientSync++;
+
+ if (addedItemIds.size() == 1) {
+ select(addedItemIds.iterator().next());
+ } else {
+ assert getSelectionModel() instanceof SelectionModel.Multi : "Got multiple selections, but the selection model is not a SelectionModel.Multi";
+ ((SelectionModel.Multi) getSelectionModel())
+ .select(addedItemIds);
+ }
+ }
+ }
+
+ @Override
+ public void sort(String[] columnIds, SortDirection[] directions,
+ SortEventOriginator originator) {
+ assert columnIds.length == directions.length;
+
+ List<SortOrder> order = new ArrayList<SortOrder>(
+ columnIds.length);
+ for (int i = 0; i < columnIds.length; i++) {
+ Object propertyId = getPropertyIdByColumnId(columnIds[i]);
+ order.add(new SortOrder(propertyId, directions[i]));
+ }
+
+ setSortOrder(order, originator);
+ }
+ });
+ }
+
+ /**
+ * 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 (datasourceExtension != null) {
+ removeExtension(datasourceExtension);
+ }
+
+ datasource = container;
+
+ //
+ // Adjust sort order
+ //
+
+ if (container instanceof Container.Sortable) {
+
+ // If the container is sortable, go through the current sort order
+ // and match each item to the sortable properties of the new
+ // container. If the new container does not support an item in the
+ // current sort order, that item is removed from the current sort
+ // order list.
+ Collection<?> sortableProps = ((Container.Sortable) getContainerDatasource())
+ .getSortableContainerPropertyIds();
+
+ Iterator<SortOrder> i = sortOrder.iterator();
+ while (i.hasNext()) {
+ if (!sortableProps.contains(i.next().getPropertyId())) {
+ i.remove();
+ }
+ }
+
+ sort(SortEventOriginator.INTERNAL);
+ } else {
+
+ // If the new container is not sortable, we'll just re-set the sort
+ // order altogether.
+ clearSortOrder();
+ }
+
+ datasourceExtension = new RpcDataProviderExtension(container);
+ datasourceExtension.extend(this);
+
+ /*
+ * selectionModel == null when the invocation comes from the
+ * constructor.
+ */
+ if (selectionModel != null) {
+ selectionModel.reset();
+ }
+
+ // Listen to changes in properties and remove columns if needed
+ if (datasource instanceof PropertySetChangeNotifier) {
+ ((PropertySetChangeNotifier) datasource)
+ .addPropertySetChangeListener(propertyListener);
+ }
+ /*
+ * 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
+ HeaderRow row = getHeader().getDefaultRow();
+ for (Object propertyId : datasource.getContainerPropertyIds()) {
+ if (!columns.containsKey(propertyId)) {
+ GridColumn column = appendColumn(propertyId);
+
+ // Initial sorting is defined by container
+ if (datasource instanceof Sortable) {
+ column.setSortable(((Sortable) datasource)
+ .getSortableContainerPropertyIds().contains(
+ propertyId));
+ }
+
+ // Add by default property id as column header
+ row.getCell(propertyId).setText(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);
+ }
+
+ /**
+ * 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);
+
+ for (int i = 0; i < getHeader().getRowCount(); ++i) {
+ getHeader().getRow(i).addCell(datasourcePropertyId);
+ }
+
+ for (int i = 0; i < getFooter().getRowCount(); ++i) {
+ getFooter().getRow(i).addCell(datasourcePropertyId);
+ }
+
+ 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 scrollTo(Object itemId) throws IllegalArgumentException {
+ scrollTo(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 scrollTo(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();
+ }
+
+ /**
+ * Sets the number of rows that should be visible in Grid's body, while
+ * {@link #getHeightMode()} is {@link HeightMode#ROW}.
+ * <p>
+ * If Grid is currently not in {@link HeightMode#ROW}, the given value is
+ * remembered, and applied once the mode is applied.
+ *
+ * @param rows
+ * The height in terms of number of rows displayed in Grid's
+ * body. If Grid doesn't contain enough rows, white space is
+ * displayed instead. If <code>null</code> is given, then Grid's
+ * height is undefined
+ * @throws IllegalArgumentException
+ * if {@code rows} is zero or less
+ * @throws IllegalArgumentException
+ * if {@code rows} is {@link Double#isInifinite(double)
+ * infinite}
+ * @throws IllegalArgumentException
+ * if {@code rows} is {@link Double#isNaN(double) NaN}
+ */
+ public void setHeightByRows(double rows) {
+ if (rows <= 0.0d) {
+ throw new IllegalArgumentException(
+ "More than zero rows must be shown.");
+ } else if (Double.isInfinite(rows)) {
+ throw new IllegalArgumentException(
+ "Grid doesn't support infinite heights");
+ } else if (Double.isNaN(rows)) {
+ throw new IllegalArgumentException("NaN is not a valid row count");
+ }
+
+ getState().heightByRows = rows;
+ }
+
+ /**
+ * Gets the amount of rows in Grid's body that are shown, while
+ * {@link #getHeightMode()} is {@link HeightMode#ROW}.
+ *
+ * @return the amount of rows that are being shown in Grid's body
+ * @see #setHeightByRows(double)
+ */
+ public double getHeightByRows() {
+ return getState(false).heightByRows;
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * <em>Note:</em> This method will change the widget's size in the browser
+ * only if {@link #getHeightMode()} returns {@link HeightMode#CSS}.
+ *
+ * @see #setHeightMode(HeightMode)
+ */
+ @Override
+ public void setHeight(float height, Unit unit) {
+ super.setHeight(height, unit);
+ }
+
+ /**
+ * Defines the mode in which the Grid widget's height is calculated.
+ * <p>
+ * If {@link HeightMode#CSS} is given, Grid will respect the values given
+ * via a {@code setHeight}-method, and behave as a traditional Component.
+ * <p>
+ * If {@link HeightMode#ROW} is given, Grid will make sure that the body
+ * will display as many rows as {@link #getHeightByRows()} defines.
+ * <em>Note:</em> If headers/footers are inserted or removed, the widget
+ * will resize itself to still display the required amount of rows in its
+ * body. It also takes the horizontal scrollbar into account.
+ *
+ * @param heightMode
+ * the mode in to which Grid should be set
+ */
+ public void setHeightMode(HeightMode heightMode) {
+ /*
+ * This method is a workaround for the fact that Vaadin re-applies
+ * widget dimensions (height/width) on each state change event. The
+ * original design was to have setHeight an setHeightByRow be equals,
+ * and whichever was called the latest was considered in effect.
+ *
+ * But, because of Vaadin always calling setHeight on the widget, this
+ * approach doesn't work.
+ */
+
+ getState().heightMode = heightMode;
+ }
+
+ /**
+ * Returns the current {@link HeightMode} the Grid is in.
+ * <p>
+ * Defaults to {@link HeightMode#CSS}.
+ *
+ * @return the current HeightMode
+ */
+ public HeightMode getHeightMode() {
+ return getState(false).heightMode;
+ }
+
+ /* Selection related methods: */
+
+ /**
+ * Takes a new {@link SelectionModel} into use.
+ * <p>
+ * The SelectionModel that is previously in use will have all its items
+ * deselected.
+ * <p>
+ * If the given SelectionModel is already in use, this method does nothing.
+ *
+ * @param selectionModel
+ * the new SelectionModel to use
+ * @throws IllegalArgumentException
+ * if {@code selectionModel} is <code>null</code>
+ */
+ public void setSelectionModel(SelectionModel selectionModel)
+ throws IllegalArgumentException {
+ if (selectionModel == null) {
+ throw new IllegalArgumentException(
+ "Selection model may not be null");
+ }
+
+ if (this.selectionModel != selectionModel) {
+ // this.selectionModel is null on init
+ if (this.selectionModel != null) {
+ this.selectionModel.reset();
+ this.selectionModel.setGrid(null);
+ }
+
+ this.selectionModel = selectionModel;
+ this.selectionModel.setGrid(this);
+ this.selectionModel.reset();
+
+ if (selectionModel.getClass().equals(SingleSelectionModel.class)) {
+ getState().selectionMode = SharedSelectionMode.SINGLE;
+ } else if (selectionModel.getClass().equals(
+ MultiSelectionModel.class)) {
+ getState().selectionMode = SharedSelectionMode.MULTI;
+ } else if (selectionModel.getClass().equals(NoSelectionModel.class)) {
+ getState().selectionMode = SharedSelectionMode.NONE;
+ } else {
+ throw new UnsupportedOperationException("Grid currently "
+ + "supports only its own bundled selection models");
+ }
+ }
+ }
+
+ /**
+ * Returns the currently used {@link SelectionModel}.
+ *
+ * @return the currently used SelectionModel
+ */
+ public SelectionModel getSelectionModel() {
+ return selectionModel;
+ }
+
+ /**
+ * Changes the Grid's selection mode.
+ * <p>
+ * Grid supports three selection modes: multiselect, single select and no
+ * selection, and this is a conveniency method for choosing between one of
+ * them.
+ * <P>
+ * Technically, this method is a shortcut that can be used instead of
+ * calling {@code setSelectionModel} with a specific SelectionModel
+ * instance. Grid comes with three built-in SelectionModel classes, and the
+ * {@link SelectionMode} enum represents each of them.
+ * <p>
+ * Essentially, the two following method calls are equivalent:
+ * <p>
+ * <code><pre>
+ * grid.setSelectionMode(SelectionMode.MULTI);
+ * grid.setSelectionModel(new MultiSelectionMode());
+ * </pre></code>
+ *
+ *
+ * @param selectionMode
+ * the selection mode to switch to
+ * @return The {@link SelectionModel} instance that was taken into use
+ * @throws IllegalArgumentException
+ * if {@code selectionMode} is <code>null</code>
+ * @see SelectionModel
+ */
+ public SelectionModel setSelectionMode(final SelectionMode selectionMode)
+ throws IllegalArgumentException {
+ if (selectionMode == null) {
+ throw new IllegalArgumentException("selection mode may not be null");
+ }
+ final SelectionModel newSelectionModel = selectionMode.createModel();
+ setSelectionModel(newSelectionModel);
+ return newSelectionModel;
+ }
+
+ /**
+ * Checks whether an item is selected or not.
+ *
+ * @param itemId
+ * the item id to check for
+ * @return <code>true</code> iff the item is selected
+ */
+ // keep this javadoc in sync with SelectionModel.isSelected
+ public boolean isSelected(Object itemId) {
+ return selectionModel.isSelected(itemId);
+ }
+
+ /**
+ * Returns a collection of all the currently selected itemIds.
+ * <p>
+ * This method is a shorthand that is forwarded to the object that is
+ * returned by {@link #getSelectionModel()}.
+ *
+ * @return a collection of all the currently selected itemIds
+ */
+ // keep this javadoc in sync with SelectionModel.getSelectedRows
+ public Collection<Object> getSelectedRows() {
+ return getSelectionModel().getSelectedRows();
+ }
+
+ /**
+ * Gets the item id of the currently selected item.
+ * <p>
+ * This method is a shorthand that is forwarded to the object that is
+ * returned by {@link #getSelectionModel()}. Only
+ * {@link SelectionModel.Single} is supported.
+ *
+ * @return the item id of the currently selected item, or <code>null</code>
+ * if nothing is selected
+ * @throws IllegalStateException
+ * if the object that is returned by
+ * {@link #getSelectionModel()} is not an instance of
+ * {@link SelectionModel.Single}
+ */
+ // keep this javadoc in sync with SelectionModel.Single.getSelectedRow
+ public Object getSelectedRow() throws IllegalStateException {
+ if (selectionModel instanceof SelectionModel.Single) {
+ return ((SelectionModel.Single) selectionModel).getSelectedRow();
+ } else {
+ throw new IllegalStateException(Grid.class.getSimpleName()
+ + " does not support the 'getSelectedRow' shortcut method "
+ + "unless the selection model implements "
+ + SelectionModel.Single.class.getName()
+ + ". The current one does not ("
+ + selectionModel.getClass().getName() + ")");
+ }
+ }
+
+ /**
+ * Marks an item as selected.
+ * <p>
+ * This method is a shorthand that is forwarded to the object that is
+ * returned by {@link #getSelectionModel()}. Only
+ * {@link SelectionModel.Single} or {@link SelectionModel.Multi} are
+ * supported.
+ *
+ *
+ * @param itemIds
+ * the itemId to mark as selected
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if the itemId already was selected
+ * @throws IllegalArgumentException
+ * if the {@code itemId} doesn't exist in the currently active
+ * Container
+ * @throws IllegalStateException
+ * if the selection was illegal. One such reason might be that
+ * the implementation already had an item selected, and that
+ * needs to be explicitly deselected before re-selecting
+ * something
+ * @throws IllegalStateException
+ * if the object that is returned by
+ * {@link #getSelectionModel()} does not implement
+ * {@link SelectionModel.Single} or {@link SelectionModel.Multi}
+ */
+ // keep this javadoc in sync with SelectionModel.Single.select
+ public boolean select(Object itemId) throws IllegalArgumentException,
+ IllegalStateException {
+ if (selectionModel instanceof SelectionModel.Single) {
+ return ((SelectionModel.Single) selectionModel).select(itemId);
+ } else if (selectionModel instanceof SelectionModel.Multi) {
+ return ((SelectionModel.Multi) selectionModel).select(itemId);
+ } else {
+ throw new IllegalStateException(Grid.class.getSimpleName()
+ + " does not support the 'select' shortcut method "
+ + "unless the selection model implements "
+ + SelectionModel.Single.class.getName() + " or "
+ + SelectionModel.Multi.class.getName()
+ + ". The current one does not ("
+ + selectionModel.getClass().getName() + ").");
+ }
+ }
+
+ /**
+ * Marks an item as deselected.
+ * <p>
+ * This method is a shorthand that is forwarded to the object that is
+ * returned by {@link #getSelectionModel()}. Only
+ * {@link SelectionModel.Single} and {@link SelectionModel.Multi} are
+ * supported.
+ *
+ * @param itemId
+ * the itemId to remove from being selected
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if the itemId already was selected
+ * @throws IllegalArgumentException
+ * if the {@code itemId} doesn't exist in the currently active
+ * Container
+ * @throws IllegalStateException
+ * if the deselection was illegal. One such reason might be that
+ * the implementation already had an item selected, and that
+ * needs to be explicitly deselected before re-selecting
+ * something
+ * @throws IllegalStateException
+ * if the object that is returned by
+ * {@link #getSelectionModel()} does not implement
+ * {@link SelectionModel.Single} or {@link SelectionModel.Multi}
+ */
+ // keep this javadoc in sync with SelectionModel.Single.deselect
+ public boolean deselect(Object itemId) throws IllegalStateException {
+ if (selectionModel instanceof SelectionModel.Single) {
+ return ((SelectionModel.Single) selectionModel).deselect(itemId);
+ } else if (selectionModel instanceof SelectionModel.Multi) {
+ return ((SelectionModel.Multi) selectionModel).deselect(itemId);
+ } else {
+ throw new IllegalStateException(Grid.class.getSimpleName()
+ + " does not support the 'deselect' shortcut method "
+ + "unless the selection model implements "
+ + SelectionModel.Single.class.getName() + " or "
+ + SelectionModel.Multi.class.getName()
+ + ". The current one does not ("
+ + selectionModel.getClass().getName() + ").");
+ }
+ }
+
+ /**
+ * Fires a selection change event.
+ * <p>
+ * <strong>Note:</strong> This is not a method that should be called by
+ * application logic. This method is publicly accessible only so that
+ * {@link SelectionModel SelectionModels} would be able to inform Grid of
+ * these events.
+ *
+ * @param addedSelections
+ * the selections that were added by this event
+ * @param removedSelections
+ * the selections that were removed by this event
+ */
+ public void fireSelectionChangeEvent(Collection<Object> oldSelection,
+ Collection<Object> newSelection) {
+ fireEvent(new SelectionChangeEvent(this, oldSelection, newSelection));
+ }
+
+ @Override
+ public void addSelectionChangeListener(SelectionChangeListener listener) {
+ addListener(SelectionChangeEvent.class, listener,
+ SELECTION_CHANGE_METHOD);
+ }
+
+ @Override
+ public void removeSelectionChangeListener(SelectionChangeListener listener) {
+ removeListener(SelectionChangeEvent.class, listener,
+ SELECTION_CHANGE_METHOD);
+ }
+
+ /**
+ * Gets the
+ * {@link com.vaadin.data.RpcDataProviderExtension.DataProviderKeyMapper
+ * DataProviderKeyMapper} being used by the data source.
+ *
+ * @return the key mapper being used by the data source
+ */
+ DataProviderKeyMapper getKeyMapper() {
+ return datasourceExtension.getKeyMapper();
+ }
+
+ /**
+ * Adds a renderer to this grid's connector hierarchy.
+ *
+ * @param renderer
+ * the renderer to add
+ */
+ void addRenderer(Renderer<?> renderer) {
+ addExtension(renderer);
+ }
+
+ /**
+ * Sets the current sort order using the fluid Sort API. Read the
+ * documentation for {@link Sort} for more information.
+ *
+ * @param s
+ * a sort instance
+ */
+ public void sort(Sort s) {
+ setSortOrder(s.build());
+ }
+
+ /**
+ * Sort this Grid in ascending order by a specified property.
+ *
+ * @param propertyId
+ * a property ID
+ */
+ public void sort(Object propertyId) {
+ sort(propertyId, SortDirection.ASCENDING);
+ }
+
+ /**
+ * Sort this Grid in user-specified {@link SortOrder} by a property.
+ *
+ * @param propertyId
+ * a property ID
+ * @param direction
+ * a sort order value (ascending/descending)
+ */
+ public void sort(Object propertyId, SortDirection direction) {
+ sort(Sort.by(propertyId, direction));
+ }
+
+ /**
+ * Clear the current sort order, and re-sort the grid.
+ */
+ public void clearSortOrder() {
+ sortOrder.clear();
+ sort(false);
+ }
+
+ /**
+ * Sets the sort order to use. This method throws
+ * {@link IllegalStateException} if the attached container is not a
+ * {@link Container.Sortable}, and {@link IllegalArgumentException} if a
+ * property in the list is not recognized by the container, or if the
+ * 'order' parameter is null.
+ *
+ * @param order
+ * a sort order list.
+ */
+ public void setSortOrder(List<SortOrder> order) {
+ setSortOrder(order, SortEventOriginator.API);
+ }
+
+ private void setSortOrder(List<SortOrder> order,
+ SortEventOriginator originator) {
+ if (!(getContainerDatasource() instanceof Container.Sortable)) {
+ throw new IllegalStateException(
+ "Attached container is not sortable (does not implement Container.Sortable)");
+ }
+
+ if (order == null) {
+ throw new IllegalArgumentException("Order list may not be null!");
+ }
+
+ sortOrder.clear();
+
+ Collection<?> sortableProps = ((Container.Sortable) getContainerDatasource())
+ .getSortableContainerPropertyIds();
+
+ for (SortOrder o : order) {
+ if (!sortableProps.contains(o.getPropertyId())) {
+ throw new IllegalArgumentException(
+ "Property "
+ + o.getPropertyId()
+ + " does not exist or is not sortable in the current container");
+ }
+ }
+
+ sortOrder.addAll(order);
+ sort(originator);
+ }
+
+ /**
+ * Get the current sort order list.
+ *
+ * @return a sort order list
+ */
+ public List<SortOrder> getSortOrder() {
+ return Collections.unmodifiableList(sortOrder);
+ }
+
+ /**
+ * Apply sorting to data source.
+ */
+ private void sort(SortEventOriginator originator) {
+
+ Container c = getContainerDatasource();
+ if (c instanceof Container.Sortable) {
+ Container.Sortable cs = (Container.Sortable) c;
+
+ final int items = sortOrder.size();
+ Object[] propertyIds = new Object[items];
+ boolean[] directions = new boolean[items];
+
+ String[] columnKeys = new String[items];
+ SortDirection[] stateDirs = new SortDirection[items];
+
+ for (int i = 0; i < items; ++i) {
+ SortOrder order = sortOrder.get(i);
+
+ columnKeys[i] = this.columnKeys.key(order.getPropertyId());
+ stateDirs[i] = order.getDirection();
+
+ propertyIds[i] = order.getPropertyId();
+ switch (order.getDirection()) {
+ case ASCENDING:
+ directions[i] = true;
+ break;
+ case DESCENDING:
+ directions[i] = false;
+ break;
+ default:
+ throw new IllegalArgumentException("getDirection() of "
+ + order + " returned an unexpected value");
+ }
+ }
+
+ cs.sort(propertyIds, directions);
+
+ fireEvent(new SortOrderChangeEvent(this, new ArrayList<SortOrder>(
+ sortOrder), originator));
+
+ getState().sortColumns = columnKeys;
+ getState(false).sortDirs = stateDirs;
+ } else {
+ throw new IllegalStateException(
+ "Container is not sortable (does not implement Container.Sortable)");
+ }
+ }
+
+ /**
+ * Adds a sort order change listener that gets notified when the sort order
+ * changes.
+ *
+ * @param listener
+ * the sort order change listener to add
+ */
+ public void addSortOrderChangeListener(SortOrderChangeListener listener) {
+ addListener(SortOrderChangeEvent.class, listener,
+ SORT_ORDER_CHANGE_METHOD);
+ }
+
+ /**
+ * Removes a sort order change listener previously added using
+ * {@link #addSortOrderChangeListener(SortOrderChangeListener)}.
+ *
+ * @param listener
+ * the sort order change listener to remove
+ */
+ public void removeSortOrderChangeListener(SortOrderChangeListener listener) {
+ removeListener(SortOrderChangeEvent.class, listener,
+ SORT_ORDER_CHANGE_METHOD);
+ }
+
+ /**
+ * Returns the header section of this grid. The default header contains a
+ * single row displaying the column captions.
+ *
+ * @return the header
+ */
+ public GridHeader getHeader() {
+ return header;
+ }
+
+ /**
+ * Returns the footer section of this grid. The default header contains a
+ * single row displaying the column captions.
+ *
+ * @return the footer
+ */
+ public GridFooter getFooter() {
+ return footer;
+ }
+
+ @Override
+ public Iterator<Component> iterator() {
+ List<Component> componentList = new ArrayList<Component>();
+
+ GridHeader header = getHeader();
+ for (int i = 0; i < header.getRowCount(); ++i) {
+ HeaderRow row = header.getRow(i);
+ for (Object propId : datasource.getContainerPropertyIds()) {
+ HeaderCell cell = row.getCell(propId);
+ if (cell.getCellState().type == GridStaticCellType.WIDGET) {
+ componentList.add(cell.getComponent());
+ }
+ }
+ }
+
+ GridFooter footer = getFooter();
+ for (int i = 0; i < footer.getRowCount(); ++i) {
+ FooterRow row = footer.getRow(i);
+ for (Object propId : datasource.getContainerPropertyIds()) {
+ FooterCell cell = row.getCell(propId);
+ if (cell.getCellState().type == GridStaticCellType.WIDGET) {
+ componentList.add(cell.getComponent());
+ }
+ }
+ }
+
+ return componentList.iterator();
+ }
+}
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..0ef805eb2e
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/GridColumn.java
@@ -0,0 +1,427 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.vaadin.ui.components.grid;
+
+import java.io.Serializable;
+
+import com.vaadin.data.util.converter.Converter;
+import com.vaadin.data.util.converter.ConverterUtil;
+import com.vaadin.server.VaadinSession;
+import com.vaadin.shared.ui.grid.GridColumnState;
+import com.vaadin.shared.ui.grid.GridStaticSectionState.CellState;
+import com.vaadin.ui.UI;
+import com.vaadin.ui.components.grid.renderers.TextRenderer;
+
+/**
+ * A column in the grid. Can be obtained by calling
+ * {@link Grid#getColumn(Object propertyId)}.
+ *
+ * @since
+ * @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;
+
+ private Converter<?, Object> converter;
+
+ /**
+ * A check for allowing the {@link #GridColumn(Grid, GridColumnState)
+ * constructor} to call {@link #setConverter(Converter)} with a
+ * <code>null</code>, even if model and renderer aren't compatible.
+ */
+ private boolean isFirstConverterAssignment = true;
+
+ /**
+ * 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;
+ internalSetRenderer(new TextRenderer());
+ }
+
+ /**
+ * 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 default row of header, null if no default row
+ *
+ * @throws IllegalStateException
+ * if the column no longer is attached to the grid
+ */
+ @Deprecated
+ 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
+ */
+ @Deprecated
+ public void setHeaderCaption(String caption) throws IllegalStateException {
+ checkColumnIsAttached();
+ state.header = caption;
+ }
+
+ /**
+ * 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
+ */
+ @Deprecated
+ public String getFooterCaption() throws IllegalStateException {
+ checkColumnIsAttached();
+ return getFooterCellState().text;
+ }
+
+ /**
+ * 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
+ */
+ @Deprecated
+ public void setFooterCaption(String caption) throws IllegalStateException {
+ checkColumnIsAttached();
+ getFooterCellState().text = caption;
+ state.footer = caption;
+ grid.markAsDirty();
+ }
+
+ private CellState getFooterCellState() {
+ int index = grid.getState().columns.indexOf(state);
+ return grid.getState().footer.rows.get(0).cells.get(index);
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * Sets the renderer for this column.
+ * <p>
+ * If a suitable converter isn't defined explicitly, the session converter
+ * factory is used to find a compatible converter.
+ *
+ * @param renderer
+ * the renderer to use
+ * @throws IllegalArgumentException
+ * if no compatible converter could be found
+ *
+ * @see VaadinSession#getConverterFactory()
+ * @see ConverterUtil#getConverter(Class, Class, VaadinSession)
+ * @see #setConverter(Converter)
+ */
+ public void setRenderer(Renderer<?> renderer) {
+ if (!internalSetRenderer(renderer)) {
+ throw new IllegalArgumentException(
+ "Could not find a converter for converting from the model type "
+ + getModelType()
+ + " to the renderer presentation type "
+ + renderer.getPresentationType());
+ }
+ }
+
+ /**
+ * Sets the renderer for this column and the converter used to convert from
+ * the property value type to the renderer presentation type.
+ *
+ * @param renderer
+ * the renderer to use, cannot be null
+ * @param converter
+ * the converter to use
+ *
+ * @throws IllegalArgumentException
+ * if the renderer is already associated with a grid column
+ */
+ public <T> void setRenderer(Renderer<T> renderer,
+ Converter<? extends T, ?> converter) {
+ if (renderer.getParent() != null) {
+ throw new IllegalArgumentException(
+ "Cannot set a renderer that is already connected to a grid column");
+ }
+
+ if (getRenderer() != null) {
+ grid.removeExtension(getRenderer());
+ }
+
+ grid.addRenderer(renderer);
+ state.rendererConnector = renderer;
+ setConverter(converter);
+ }
+
+ /**
+ * Sets the converter used to convert from the property value type to the
+ * renderer presentation type.
+ *
+ * @param converter
+ * the converter to use, or {@code null} to not use any
+ * converters
+ * @throws IllegalArgumentException
+ * if the types are not compatible
+ */
+ public void setConverter(Converter<?, ?> converter)
+ throws IllegalArgumentException {
+ Class<?> modelType = getModelType();
+ if (converter != null) {
+ if (!converter.getModelType().isAssignableFrom(modelType)) {
+ throw new IllegalArgumentException("The converter model type "
+ + converter.getModelType()
+ + " is not compatible with the property type "
+ + modelType);
+
+ } else if (!getRenderer().getPresentationType().isAssignableFrom(
+ converter.getPresentationType())) {
+ throw new IllegalArgumentException(
+ "The converter presentation type "
+ + converter.getPresentationType()
+ + " is not compatible with the renderer presentation type "
+ + getRenderer().getPresentationType());
+ }
+ }
+
+ else {
+ /*
+ * Since the converter is null (i.e. will be removed), we need to
+ * know that the renderer and model are compatible. If not, we can't
+ * allow for this to happen.
+ *
+ * The constructor is allowed to call this method with null without
+ * any compatibility checks, therefore we have a special case for
+ * it.
+ */
+
+ Class<?> rendererPresentationType = getRenderer()
+ .getPresentationType();
+ if (!isFirstConverterAssignment
+ && !rendererPresentationType.isAssignableFrom(modelType)) {
+ throw new IllegalArgumentException("Cannot remove converter, "
+ + "as renderer's presentation type "
+ + rendererPresentationType.getName() + " and column's "
+ + "model " + modelType.getName() + " type aren't "
+ + "directly with each other");
+ }
+ }
+
+ isFirstConverterAssignment = false;
+
+ @SuppressWarnings("unchecked")
+ Converter<?, Object> castConverter = (Converter<?, Object>) converter;
+ this.converter = castConverter;
+ }
+
+ /**
+ * Returns the renderer instance used by this column.
+ *
+ * @return the renderer
+ */
+ public Renderer<?> getRenderer() {
+ return (Renderer<?>) getState().rendererConnector;
+ }
+
+ /**
+ * Returns the converter instance used by this column.
+ *
+ * @return the converter
+ */
+ public Converter<?, ?> getConverter() {
+ return converter;
+ }
+
+ private <T> boolean internalSetRenderer(Renderer<T> renderer) {
+
+ Converter<? extends T, ?> converter;
+ if (isCompatibleWithProperty(renderer, getConverter())) {
+ // Use the existing converter (possibly none) if types compatible
+ converter = (Converter<? extends T, ?>) getConverter();
+ } else {
+ converter = ConverterUtil.getConverter(
+ renderer.getPresentationType(), getModelType(),
+ getSession());
+ }
+ setRenderer(renderer, converter);
+ return isCompatibleWithProperty(renderer, converter);
+ }
+
+ private VaadinSession getSession() {
+ UI ui = grid.getUI();
+ return ui != null ? ui.getSession() : null;
+ }
+
+ private boolean isCompatibleWithProperty(Renderer<?> renderer,
+ Converter<?, ?> converter) {
+ Class<?> type;
+ if (converter == null) {
+ type = getModelType();
+ } else {
+ type = converter.getPresentationType();
+ }
+ return renderer.getPresentationType().isAssignableFrom(type);
+ }
+
+ private Class<?> getModelType() {
+ return grid.getContainerDatasource().getType(
+ grid.getPropertyIdByColumnId(state.id));
+ }
+
+ /**
+ * Should sorting controls be available for the column
+ *
+ * @param sortable
+ * <code>true</code> if the sorting controls should be visible.
+ */
+ public void setSortable(boolean sortable) {
+ checkColumnIsAttached();
+ state.sortable = sortable;
+ grid.markAsDirty();
+ }
+
+ /**
+ * Are the sorting controls visible in the column header
+ */
+ public boolean isSortable() {
+ return state.sortable;
+ }
+}
diff --git a/server/src/com/vaadin/ui/components/grid/GridFooter.java b/server/src/com/vaadin/ui/components/grid/GridFooter.java
new file mode 100644
index 0000000000..0a28a481cf
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/GridFooter.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui.components.grid;
+
+import com.vaadin.shared.ui.grid.GridStaticSectionState;
+
+/**
+ * Represents the footer section of a Grid. By default Footer is not visible.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class GridFooter extends GridStaticSection<GridFooter.FooterRow> {
+
+ public class FooterRow extends GridStaticSection.StaticRow<FooterCell> {
+
+ protected FooterRow(GridStaticSection<?> section) {
+ super(section);
+ }
+
+ @Override
+ protected FooterCell createCell() {
+ return new FooterCell(this);
+ }
+
+ }
+
+ public class FooterCell extends GridStaticSection.StaticCell {
+
+ protected FooterCell(FooterRow row) {
+ super(row);
+ }
+ }
+
+ private final GridStaticSectionState footerState = new GridStaticSectionState();
+
+ protected GridFooter(Grid grid) {
+ this.grid = grid;
+ grid.getState(true).footer = footerState;
+ setVisible(false);
+ }
+
+ @Override
+ protected GridStaticSectionState getSectionState() {
+ return footerState;
+ }
+
+ @Override
+ protected FooterRow createRow() {
+ return new FooterRow(this);
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/components/grid/GridHeader.java b/server/src/com/vaadin/ui/components/grid/GridHeader.java
new file mode 100644
index 0000000000..9d7ec24a97
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/GridHeader.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui.components.grid;
+
+import com.vaadin.shared.ui.grid.GridStaticSectionState;
+
+/**
+ * Represents the header section of a Grid.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class GridHeader extends GridStaticSection<GridHeader.HeaderRow> {
+
+ public class HeaderRow extends GridStaticSection.StaticRow<HeaderCell> {
+
+ protected HeaderRow(GridStaticSection<?> section) {
+ super(section);
+ }
+
+ private void setDefaultRow(boolean value) {
+ getRowState().defaultRow = value;
+ }
+
+ @Override
+ protected HeaderCell createCell() {
+ return new HeaderCell(this);
+ }
+ }
+
+ public class HeaderCell extends GridStaticSection.StaticCell {
+
+ protected HeaderCell(HeaderRow row) {
+ super(row);
+ }
+ }
+
+ private HeaderRow defaultRow = null;
+ private final GridStaticSectionState headerState = new GridStaticSectionState();
+
+ protected GridHeader(Grid grid) {
+ this.grid = grid;
+ grid.getState(true).header = headerState;
+ HeaderRow row = createRow();
+ rows.add(row);
+ setDefaultRow(row);
+ getSectionState().rows.add(row.getRowState());
+ }
+
+ /**
+ * Sets the default row of this header. The default row is a special header
+ * row providing a user interface for sorting columns.
+ *
+ * @param row
+ * the new default row, or null for no default row
+ *
+ * @throws IllegalArgumentException
+ * this header does not contain the row
+ */
+ public void setDefaultRow(HeaderRow row) {
+ if (row == defaultRow) {
+ return;
+ }
+
+ if (row != null && !rows.contains(row)) {
+ throw new IllegalArgumentException(
+ "Cannot set a default row that does not exist in the section");
+ }
+
+ if (defaultRow != null) {
+ defaultRow.setDefaultRow(false);
+ }
+
+ if (row != null) {
+ row.setDefaultRow(true);
+ }
+
+ defaultRow = row;
+ markAsDirty();
+ }
+
+ /**
+ * Returns the current default row of this header. The default row is a
+ * special header row providing a user interface for sorting columns.
+ *
+ * @return the default row or null if no default row set
+ */
+ public HeaderRow getDefaultRow() {
+ return defaultRow;
+ }
+
+ @Override
+ protected GridStaticSectionState getSectionState() {
+ return headerState;
+ }
+
+ @Override
+ protected HeaderRow createRow() {
+ return new HeaderRow(this);
+ }
+
+ @Override
+ public HeaderRow removeRow(int rowIndex) {
+ HeaderRow row = super.removeRow(rowIndex);
+ if (row == defaultRow) {
+ // Default Header Row was just removed.
+ setDefaultRow(null);
+ }
+ return row;
+ }
+}
diff --git a/server/src/com/vaadin/ui/components/grid/GridStaticSection.java b/server/src/com/vaadin/ui/components/grid/GridStaticSection.java
new file mode 100644
index 0000000000..eb098d0d4e
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/GridStaticSection.java
@@ -0,0 +1,425 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui.components.grid;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.vaadin.data.Container.Indexed;
+import com.vaadin.shared.ui.grid.GridStaticCellType;
+import com.vaadin.shared.ui.grid.GridStaticSectionState;
+import com.vaadin.shared.ui.grid.GridStaticSectionState.CellState;
+import com.vaadin.shared.ui.grid.GridStaticSectionState.RowState;
+import com.vaadin.ui.Component;
+
+/**
+ * Abstract base class for Grid header and footer sections.
+ *
+ * @since
+ * @author Vaadin Ltd
+ * @param <ROWTYPE>
+ * the type of the rows in the section
+ */
+abstract class GridStaticSection<ROWTYPE extends GridStaticSection.StaticRow<?>>
+ implements Serializable {
+
+ /**
+ * Abstract base class for Grid header and footer rows.
+ *
+ * @param <CELLTYPE>
+ * the type of the cells in the row
+ */
+ abstract static class StaticRow<CELLTYPE extends StaticCell> implements
+ Serializable {
+
+ private RowState rowState = new RowState();
+ protected GridStaticSection<?> section;
+ private Map<Object, CELLTYPE> cells = new LinkedHashMap<Object, CELLTYPE>();
+ private Collection<List<CELLTYPE>> cellGroups = new HashSet<List<CELLTYPE>>();
+
+ protected StaticRow(GridStaticSection<?> section) {
+ this.section = section;
+ }
+
+ protected void addCell(Object propertyId) {
+ CELLTYPE cell = createCell();
+ cells.put(propertyId, cell);
+ rowState.cells.add(cell.getCellState());
+ }
+
+ /**
+ * Creates and returns a new instance of the cell type.
+ *
+ * @return the created cell
+ */
+ protected abstract CELLTYPE createCell();
+
+ protected RowState getRowState() {
+ return rowState;
+ }
+
+ /**
+ * Returns the cell at the given position in this row.
+ *
+ * @param propertyId
+ * the itemId of column
+ * @return the cell on given column
+ * @throws IndexOutOfBoundsException
+ * if the index is out of bounds
+ */
+ public CELLTYPE getCell(Object propertyId) {
+ return cells.get(propertyId);
+ }
+
+ /**
+ * Merges cells in a row
+ *
+ * @param cells
+ * The cells to be merged
+ * @return The first cell of the merged cells
+ */
+ protected CELLTYPE join(List<CELLTYPE> cells) {
+ assert cells.size() > 1 : "You cannot merge less than 2 cells together";
+
+ // Ensure no cell is already grouped
+ for (CELLTYPE cell : cells) {
+ if (getCellGroupForCell(cell) != null) {
+ throw new IllegalStateException("Cell " + cell.getText()
+ + " is already grouped.");
+ }
+ }
+
+ // Ensure continuous range
+ Iterator<CELLTYPE> cellIterator = this.cells.values().iterator();
+ CELLTYPE current = null;
+ int firstIndex = 0;
+
+ while (cellIterator.hasNext()) {
+ current = cellIterator.next();
+ if (current == cells.get(0)) {
+ break;
+ }
+ firstIndex++;
+ }
+
+ for (int i = 1; i < cells.size(); ++i) {
+ current = cellIterator.next();
+
+ if (current != cells.get(i)) {
+ throw new IllegalStateException(
+ "Cell range must be a continous range");
+ }
+ }
+
+ // Create a new group
+ final ArrayList<CELLTYPE> cellGroup = new ArrayList<CELLTYPE>(cells);
+ cellGroups.add(cellGroup);
+
+ // Add group to state
+ List<Integer> stateGroup = new ArrayList<Integer>();
+ for (int i = 0; i < cells.size(); ++i) {
+ stateGroup.add(firstIndex + i);
+ }
+ rowState.cellGroups.add(stateGroup);
+ section.markAsDirty();
+
+ // Returns first cell of group
+ return cells.get(0);
+ }
+
+ /**
+ * Merges columns cells in a row
+ *
+ * @param properties
+ * The column properties which header should be merged
+ * @return The remaining visible cell after the merge
+ */
+ public CELLTYPE join(Object... properties) {
+ List<CELLTYPE> cells = new ArrayList<CELLTYPE>();
+ for (int i = 0; i < properties.length; ++i) {
+ cells.add(getCell(properties[i]));
+ }
+
+ return join(cells);
+ }
+
+ /**
+ * Merges columns cells in a row
+ *
+ * @param cells
+ * The cells to merge. Must be from the same row.
+ * @return The remaining visible cell after the merge
+ */
+ public CELLTYPE join(CELLTYPE... cells) {
+ return join(Arrays.asList(cells));
+ }
+
+ private List<CELLTYPE> getCellGroupForCell(CELLTYPE cell) {
+ for (List<CELLTYPE> group : cellGroups) {
+ if (group.contains(cell)) {
+ return group;
+ }
+ }
+ return null;
+ }
+ }
+
+ /**
+ * A header or footer cell. Has a simple textual caption.
+ */
+ abstract static class StaticCell implements Serializable {
+
+ private CellState cellState = new CellState();
+ private StaticRow<?> row;
+
+ protected StaticCell(StaticRow<?> row) {
+ this.row = row;
+ }
+
+ /**
+ * Gets the row where this cell is.
+ *
+ * @return row for this cell
+ */
+ public StaticRow<?> getRow() {
+ return row;
+ }
+
+ protected CellState getCellState() {
+ return cellState;
+ }
+
+ /**
+ * Sets the text displayed in this cell.
+ *
+ * @param text
+ * a plain text caption
+ */
+ public void setText(String text) {
+ cellState.text = text;
+ cellState.type = GridStaticCellType.TEXT;
+ row.section.markAsDirty();
+ }
+
+ /**
+ * Returns the text displayed in this cell.
+ *
+ * @return the plain text caption
+ */
+ public String getText() {
+ if (cellState.type != GridStaticCellType.TEXT) {
+ throw new IllegalStateException(
+ "Cannot fetch Text from a cell with type "
+ + cellState.type);
+ }
+ return cellState.text;
+ }
+
+ /**
+ * Returns the HTML content displayed in this cell.
+ *
+ * @return the html
+ *
+ */
+ public String getHtml() {
+ if (cellState.type != GridStaticCellType.HTML) {
+ throw new IllegalStateException(
+ "Cannot fetch HTML from a cell with type "
+ + cellState.type);
+ }
+ return cellState.html;
+ }
+
+ /**
+ * Sets the HTML content displayed in this cell.
+ *
+ * @param html
+ * the html to set
+ */
+ public void setHtml(String html) {
+ cellState.html = html;
+ cellState.type = GridStaticCellType.HTML;
+ row.section.markAsDirty();
+ }
+
+ /**
+ * Returns the component displayed in this cell.
+ *
+ * @return the component
+ */
+ public Component getComponent() {
+ if (cellState.type != GridStaticCellType.WIDGET) {
+ throw new IllegalStateException(
+ "Cannot fetch Component from a cell with type "
+ + cellState.type);
+ }
+ return (Component) cellState.connector;
+ }
+
+ /**
+ * Sets the component displayed in this cell.
+ *
+ * @param component
+ * the component to set
+ */
+ public void setComponent(Component component) {
+ component.setParent(row.section.grid);
+ cellState.connector = component;
+ cellState.type = GridStaticCellType.WIDGET;
+ row.section.markAsDirty();
+ }
+ }
+
+ protected Grid grid;
+ protected List<ROWTYPE> rows = new ArrayList<ROWTYPE>();
+
+ /**
+ * Sets the visibility of the whole section.
+ *
+ * @param visible
+ * true to show this section, false to hide
+ */
+ public void setVisible(boolean visible) {
+ if (getSectionState().visible != visible) {
+ getSectionState().visible = visible;
+ markAsDirty();
+ }
+ }
+
+ /**
+ * Returns the visibility of this section.
+ *
+ * @return true if visible, false otherwise.
+ */
+ public boolean isVisible() {
+ return getSectionState().visible;
+ }
+
+ /**
+ * Removes the row at the given position.
+ *
+ * @param index
+ * the position of the row
+ *
+ * @throws IndexOutOfBoundsException
+ * if the index is out of bounds
+ */
+ public ROWTYPE removeRow(int rowIndex) {
+ ROWTYPE row = rows.remove(rowIndex);
+ getSectionState().rows.remove(rowIndex);
+
+ markAsDirty();
+ return row;
+ }
+
+ /**
+ * Removes the given row from the section.
+ *
+ * @param row
+ * the row to be removed
+ *
+ * @throws IllegalArgumentException
+ * if the row does not exist in this section
+ */
+ public void removeRow(ROWTYPE row) {
+ try {
+ removeRow(rows.indexOf(row));
+ } catch (IndexOutOfBoundsException e) {
+ throw new IllegalArgumentException(
+ "Section does not contain the given row");
+ }
+ }
+
+ /**
+ * Gets row at given index.
+ *
+ * @param rowIndex
+ * 0 based index for row. Counted from top to bottom
+ * @return row at given index
+ */
+ public ROWTYPE getRow(int rowIndex) {
+ return rows.get(rowIndex);
+ }
+
+ /**
+ * Adds a new row at the top of this section.
+ *
+ * @return the new row
+ */
+ public ROWTYPE prependRow() {
+ return addRowAt(0);
+ }
+
+ /**
+ * Adds a new row at the bottom of this section.
+ *
+ * @return the new row
+ */
+ public ROWTYPE appendRow() {
+ return addRowAt(rows.size());
+ }
+
+ /**
+ * Inserts a new row at the given position.
+ *
+ * @param index
+ * the position at which to insert the row
+ * @return the new row
+ *
+ * @throws IndexOutOfBoundsException
+ * if the index is out of bounds
+ */
+ public ROWTYPE addRowAt(int index) {
+ ROWTYPE row = createRow();
+ rows.add(index, row);
+ getSectionState().rows.add(index, row.getRowState());
+
+ Indexed dataSource = grid.getContainerDatasource();
+ for (Object id : dataSource.getContainerPropertyIds()) {
+ row.addCell(id);
+ }
+
+ markAsDirty();
+ return row;
+ }
+
+ /**
+ * Gets the amount of rows in this section.
+ *
+ * @return row count
+ */
+ public int getRowCount() {
+ return rows.size();
+ }
+
+ protected abstract GridStaticSectionState getSectionState();
+
+ protected abstract ROWTYPE createRow();
+
+ /**
+ * Informs the grid that state has changed and it should be redrawn.
+ */
+ protected void markAsDirty() {
+ grid.markAsDirty();
+ }
+}
diff --git a/server/src/com/vaadin/ui/components/grid/Renderer.java b/server/src/com/vaadin/ui/components/grid/Renderer.java
new file mode 100644
index 0000000000..b9074fb9f7
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/Renderer.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui.components.grid;
+
+import com.vaadin.server.ClientConnector;
+import com.vaadin.server.Extension;
+
+/**
+ * A ClientConnector for controlling client-side
+ * {@link com.vaadin.client.ui.grid.Renderer Grid renderers}. Renderers
+ * currently extend the Extension interface, but this fact should be regarded as
+ * an implementation detail and subject to change in a future major or minor
+ * Vaadin revision.
+ *
+ * @param <T>
+ * the type this renderer knows how to present
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public interface Renderer<T> extends Extension {
+
+ /**
+ * Returns the class literal corresponding to the presentation type T.
+ *
+ * @return the class literal of T
+ */
+ Class<T> getPresentationType();
+
+ /**
+ * Encodes the given value into a form that can be transferred to the
+ * client. The type of the returned value must be one of the types that are
+ * accepted by <a href=
+ * "http://www.json.org/javadoc/org/json/JSONObject.html#put%28java.lang.String,%20java.lang.Object%29"
+ * >{@code org.json.JSONObject#put(String, Object)}</a>.
+ *
+ * @param value
+ * the value to encode
+ * @return an encoded form of the given value
+ */
+ Object encode(T value);
+
+ /**
+ * This method is inherited from Extension but should never be called
+ * directly with a Renderer.
+ */
+ @Override
+ @Deprecated
+ void remove();
+
+ /**
+ * This method is inherited from Extension but should never be called
+ * directly with a Renderer.
+ */
+ @Override
+ @Deprecated
+ void setParent(ClientConnector parent);
+}
diff --git a/server/src/com/vaadin/ui/components/grid/SortOrderChangeEvent.java b/server/src/com/vaadin/ui/components/grid/SortOrderChangeEvent.java
new file mode 100644
index 0000000000..690fcdf1c4
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/SortOrderChangeEvent.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui.components.grid;
+
+import java.util.List;
+
+import com.vaadin.shared.ui.grid.SortEventOriginator;
+import com.vaadin.ui.Component;
+import com.vaadin.ui.components.grid.sort.SortOrder;
+
+/**
+ * Event fired by {@link Grid} when the sort order has changed.
+ *
+ * @see SortOrderChangeListener
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class SortOrderChangeEvent extends Component.Event {
+
+ private final List<SortOrder> sortOrder;
+ private final SortEventOriginator originator;
+
+ /**
+ * Creates a new sort order change event for a grid and a sort order list.
+ *
+ * @param grid
+ * the grid from which the event originates
+ * @param sortOrder
+ * the new sort order list
+ * @param wasInitiatedByUser
+ * should be set to true if this event results from end-user
+ * interaction instead of an API call or side effect
+ */
+ public SortOrderChangeEvent(Grid grid, List<SortOrder> sortOrder,
+ SortEventOriginator originator) {
+ super(grid);
+ this.sortOrder = sortOrder;
+ this.originator = originator;
+ }
+
+ /**
+ * Gets the sort order list.
+ *
+ * @return the sort order list
+ */
+ public List<SortOrder> getSortOrder() {
+ return sortOrder;
+ }
+
+ /**
+ * Gets a value describing the originator of this event, i.e. what actions
+ * resulted in this event being fired.
+ *
+ * @return a sort event originator value
+ *
+ * @see SortEventOriginator
+ */
+ public SortEventOriginator getOriginator() {
+ return originator;
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/components/grid/SortOrderChangeListener.java b/server/src/com/vaadin/ui/components/grid/SortOrderChangeListener.java
new file mode 100644
index 0000000000..82d7ba3108
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/SortOrderChangeListener.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui.components.grid;
+
+import java.io.Serializable;
+
+/**
+ * Listener for sort order change events from {@link Grid}.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public interface SortOrderChangeListener extends Serializable {
+ /**
+ * Called when the sort order has changed.
+ *
+ * @param event
+ * the sort order change event
+ */
+ public void sortOrderChange(SortOrderChangeEvent event);
+}
diff --git a/server/src/com/vaadin/ui/components/grid/renderers/DateRenderer.java b/server/src/com/vaadin/ui/components/grid/renderers/DateRenderer.java
new file mode 100644
index 0000000000..736b61d9e2
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/renderers/DateRenderer.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui.components.grid.renderers;
+
+import java.text.DateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+import com.vaadin.ui.components.grid.AbstractRenderer;
+
+/**
+ * A renderer for presenting date values.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class DateRenderer extends AbstractRenderer<Date> {
+ private final Locale locale;
+ private final String formatString;
+ private final DateFormat dateFormat;
+
+ /**
+ * Creates a new date renderer.
+ * <p>
+ * The renderer is configured to render with the {@link Date#toString()}
+ * representation for the default locale.
+ */
+ public DateRenderer() {
+ this(Locale.getDefault());
+ }
+
+ /**
+ * Creates a new date renderer.
+ * <p>
+ * The renderer is configured to render with the {@link Date#toString()}
+ * representation for the given locale.
+ *
+ * @param locale
+ * the locale in which to present dates
+ * @throws IllegalArgumentException
+ * if {@code locale} is {@code null}
+ */
+ public DateRenderer(Locale locale) throws IllegalArgumentException {
+ this("%s", locale);
+ }
+
+ /**
+ * Creates a new date renderer.
+ * <p>
+ * The renderer is configured to render with the given string format, as
+ * displayed in the default locale.
+ *
+ * @param formatString
+ * the format string with which to format the date
+ * @throws IllegalArgumentException
+ * if {@code formatString} is {@code null}
+ * @see <a
+ * href="http://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html#syntax">Format
+ * String Syntax</a>
+ */
+ public DateRenderer(String formatString) throws IllegalArgumentException {
+ this(formatString, Locale.getDefault());
+ }
+
+ /**
+ * Creates a new date renderer.
+ * <p>
+ * The renderer is configured to render with the given string format, as
+ * displayed in the given locale.
+ *
+ * @param formatString
+ * the format string to format the date with
+ * @param locale
+ * the locale to use
+ * @throws IllegalArgumentException
+ * if either argument is {@code null}
+ * @see <a
+ * href="http://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html#syntax">Format
+ * String Syntax</a>
+ */
+ public DateRenderer(String formatString, Locale locale)
+ throws IllegalArgumentException {
+ super(Date.class);
+
+ if (formatString == null) {
+ throw new IllegalArgumentException("format string may not be null");
+ }
+
+ if (locale == null) {
+ throw new IllegalArgumentException("locale may not be null");
+ }
+
+ this.locale = locale;
+ this.formatString = formatString;
+ dateFormat = null;
+ }
+
+ /**
+ * Creates a new date renderer.
+ * <p>
+ * The renderer is configured to render with he given date format.
+ *
+ * @param dateFormat
+ * the date format to use when rendering dates
+ * @throws IllegalArgumentException
+ * if {@code dateFormat} is {@code null}
+ */
+ public DateRenderer(DateFormat dateFormat) throws IllegalArgumentException {
+ super(Date.class);
+ if (dateFormat == null) {
+ throw new IllegalArgumentException("date format may not be null");
+ }
+
+ locale = null;
+ formatString = null;
+ this.dateFormat = dateFormat;
+ }
+
+ @Override
+ public String encode(Date value) {
+ if (dateFormat != null) {
+ return dateFormat.format(value);
+ } else {
+ return String.format(locale, formatString, value);
+ }
+ }
+
+ @Override
+ public String toString() {
+ final String fieldInfo;
+ if (dateFormat != null) {
+ fieldInfo = "dateFormat: " + dateFormat.toString();
+ } else {
+ fieldInfo = "locale: " + locale + ", formatString: " + formatString;
+ }
+
+ return String.format("%s [%s]", getClass().getSimpleName(), fieldInfo);
+ }
+}
diff --git a/server/src/com/vaadin/ui/components/grid/renderers/HtmlRenderer.java b/server/src/com/vaadin/ui/components/grid/renderers/HtmlRenderer.java
new file mode 100644
index 0000000000..6439608c20
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/renderers/HtmlRenderer.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui.components.grid.renderers;
+
+import com.vaadin.ui.components.grid.AbstractRenderer;
+
+/**
+ * A renderer for presenting HTML content.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class HtmlRenderer extends AbstractRenderer<String> {
+ /**
+ * Creates a new HTML renderer.
+ */
+ public HtmlRenderer() {
+ super(String.class);
+ }
+
+ @Override
+ public String encode(String value) {
+ return value;
+ }
+}
diff --git a/server/src/com/vaadin/ui/components/grid/renderers/NumberRenderer.java b/server/src/com/vaadin/ui/components/grid/renderers/NumberRenderer.java
new file mode 100644
index 0000000000..12fcfc890a
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/renderers/NumberRenderer.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui.components.grid.renderers;
+
+import java.text.NumberFormat;
+import java.util.Locale;
+
+import com.vaadin.ui.components.grid.AbstractRenderer;
+
+/**
+ * A renderer for presenting number values.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class NumberRenderer extends AbstractRenderer<Number> {
+ private final Locale locale;
+ private final NumberFormat numberFormat;
+ private final String formatString;
+
+ /**
+ * Creates a new number renderer.
+ * <p>
+ * The renderer is configured to render with the number's natural string
+ * representation in the default locale.
+ */
+ public NumberRenderer() {
+ this(Locale.getDefault());
+ }
+
+ /**
+ * Creates a new number renderer.
+ * <p>
+ * The renderer is configured to render the number as defined with the given
+ * number format.
+ *
+ * @param numberFormat
+ * the number format with which to display numbers
+ * @throws IllegalArgumentException
+ * if {@code numberFormat} is {@code null}
+ */
+ public NumberRenderer(NumberFormat numberFormat)
+ throws IllegalArgumentException {
+ super(Number.class);
+
+ if (numberFormat == null) {
+ throw new IllegalArgumentException("Number format may not be null");
+ }
+
+ locale = null;
+ this.numberFormat = numberFormat;
+ formatString = null;
+ }
+
+ /**
+ * Creates a new number renderer.
+ * <p>
+ * The renderer is configured to render with the number's natural string
+ * representation in the given locale.
+ *
+ * @param locale
+ * the locale in which to display numbers
+ * @throws IllegalArgumentException
+ * if {@code locale} is {@code null}
+ */
+ public NumberRenderer(Locale locale) throws IllegalArgumentException {
+ this("%s", locale);
+ }
+
+ /**
+ * Creates a new number renderer.
+ * <p>
+ * The renderer is configured to render with the given format string in the
+ * default locale.
+ *
+ * @param formatString
+ * the format string with which to format the number
+ * @throws IllegalArgumentException
+ * if {@code formatString} is {@code null}
+ * @see <a
+ * href="http://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html#syntax">Format
+ * String Syntax</a>
+ */
+ public NumberRenderer(String formatString) throws IllegalArgumentException {
+ this(formatString, Locale.getDefault());
+ }
+
+ /**
+ * Creates a new number renderer.
+ * <p>
+ * The renderer is configured to render with the given format string in the
+ * given locale.
+ *
+ * @param formatString
+ * the format string with which to format the number
+ * @param locale
+ * the locale in which to present numbers
+ * @throws IllegalArgumentException
+ * if either argument is {@code null}
+ * @see <a
+ * href="http://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html#syntax">Format
+ * String Syntax</a>
+ */
+ public NumberRenderer(String formatString, Locale locale) {
+ super(Number.class);
+
+ if (formatString == null) {
+ throw new IllegalArgumentException("Format string may not be null");
+ }
+
+ if (locale == null) {
+ throw new IllegalArgumentException("Locale may not be null");
+ }
+
+ this.locale = locale;
+ numberFormat = null;
+ this.formatString = formatString;
+ }
+
+ @Override
+ public String encode(Number value) {
+ if (formatString != null && locale != null) {
+ return String.format(locale, formatString, value);
+ } else if (numberFormat != null) {
+ return numberFormat.format(value);
+ } else {
+ throw new IllegalStateException(String.format("Internal bug: "
+ + "%s is in an illegal state: "
+ + "[locale: %s, numberFormat: %s, formatString: %s]",
+ getClass().getSimpleName(), locale, numberFormat,
+ formatString));
+ }
+ }
+
+ @Override
+ public String toString() {
+ final String fieldInfo;
+ if (numberFormat != null) {
+ fieldInfo = "numberFormat: " + numberFormat.toString();
+ } else {
+ fieldInfo = "locale: " + locale + ", formatString: " + formatString;
+ }
+
+ return String.format("%s [%s]", getClass().getSimpleName(), fieldInfo);
+ }
+}
diff --git a/server/src/com/vaadin/ui/components/grid/renderers/TextRenderer.java b/server/src/com/vaadin/ui/components/grid/renderers/TextRenderer.java
new file mode 100644
index 0000000000..61348a9e49
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/renderers/TextRenderer.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui.components.grid.renderers;
+
+import com.vaadin.ui.components.grid.AbstractRenderer;
+
+/**
+ * A renderer for presenting simple plain-text string values.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class TextRenderer extends AbstractRenderer<String> {
+
+ /**
+ * Creates a new text renderer
+ */
+ public TextRenderer() {
+ super(String.class);
+ }
+
+ @Override
+ public Object encode(String value) {
+ return value;
+ }
+}
diff --git a/server/src/com/vaadin/ui/components/grid/selection/AbstractSelectionModel.java b/server/src/com/vaadin/ui/components/grid/selection/AbstractSelectionModel.java
new file mode 100644
index 0000000000..e153b8a4e4
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/selection/AbstractSelectionModel.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui.components.grid.selection;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashSet;
+
+import com.vaadin.ui.components.grid.Grid;
+
+/**
+ * A base class for SelectionModels that contains some of the logic that is
+ * reusable.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public abstract class AbstractSelectionModel implements SelectionModel {
+ protected final LinkedHashSet<Object> selection = new LinkedHashSet<Object>();
+ protected Grid grid = null;
+
+ @Override
+ public boolean isSelected(final Object itemId) {
+ return selection.contains(itemId);
+ }
+
+ @Override
+ public Collection<Object> getSelectedRows() {
+ return new ArrayList<Object>(selection);
+ }
+
+ @Override
+ public void setGrid(final Grid grid) {
+ this.grid = grid;
+ }
+
+ /**
+ * Fires a {@link SelectionChangeEvent} to all the
+ * {@link SelectionChangeListener SelectionChangeListeners} currently added
+ * to the Grid in which this SelectionModel is.
+ * <p>
+ * Note that this is only a helper method, and routes the call all the way
+ * to Grid. A {@link SelectionModel} is not a
+ * {@link SelectionChangeNotifier}
+ *
+ * @param oldSelection
+ * the complete {@link Collection} of the itemIds that were
+ * selected <em>before</em> this event happened
+ * @param newSelection
+ * the complete {@link Collection} of the itemIds that are
+ * selected <em>after</em> this event happened
+ */
+ protected void fireSelectionChangeEvent(
+ final Collection<Object> oldSelection,
+ final Collection<Object> newSelection) {
+ grid.fireSelectionChangeEvent(oldSelection, newSelection);
+ }
+}
diff --git a/server/src/com/vaadin/ui/components/grid/selection/MultiSelectionModel.java b/server/src/com/vaadin/ui/components/grid/selection/MultiSelectionModel.java
new file mode 100644
index 0000000000..602e5ca169
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/selection/MultiSelectionModel.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui.components.grid.selection;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+
+import com.vaadin.data.Container.Indexed;
+
+/**
+ * A default implementation of a {@link SelectionModel.Multi}
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class MultiSelectionModel extends AbstractSelectionModel implements
+ SelectionModel.Multi {
+
+ @Override
+ public boolean select(final Object... itemIds)
+ throws IllegalArgumentException {
+ if (itemIds != null) {
+ // select will fire the event
+ return select(Arrays.asList(itemIds));
+ } else {
+ throw new IllegalArgumentException(
+ "Vararg array of itemIds may not be null");
+ }
+ }
+
+ @Override
+ public boolean select(final Collection<?> itemIds)
+ throws IllegalArgumentException {
+ if (itemIds == null) {
+ throw new IllegalArgumentException("itemIds may not be null");
+ }
+
+ final boolean hasSomeDifferingElements = !selection
+ .containsAll(itemIds);
+ if (hasSomeDifferingElements) {
+ final HashSet<Object> oldSelection = new HashSet<Object>(selection);
+ selection.addAll(itemIds);
+ fireSelectionChangeEvent(oldSelection, selection);
+ }
+ return hasSomeDifferingElements;
+ }
+
+ @Override
+ public boolean deselect(final Object... itemIds)
+ throws IllegalArgumentException {
+ if (itemIds != null) {
+ // deselect will fire the event
+ return deselect(Arrays.asList(itemIds));
+ } else {
+ throw new IllegalArgumentException(
+ "Vararg array of itemIds may not be null");
+ }
+ }
+
+ @Override
+ public boolean deselect(final Collection<?> itemIds)
+ throws IllegalArgumentException {
+ if (itemIds == null) {
+ throw new IllegalArgumentException("itemIds may not be null");
+ }
+
+ final boolean hasCommonElements = !Collections.disjoint(itemIds,
+ selection);
+ if (hasCommonElements) {
+ final HashSet<Object> oldSelection = new HashSet<Object>(selection);
+ selection.removeAll(itemIds);
+ fireSelectionChangeEvent(oldSelection, selection);
+ }
+ return hasCommonElements;
+ }
+
+ @Override
+ public boolean selectAll() {
+ // select will fire the event
+ final Indexed container = grid.getContainerDatasource();
+ if (container != null) {
+ return select(container.getItemIds());
+ } else if (selection.isEmpty()) {
+ return false;
+ } else {
+ /*
+ * this should never happen (no container but has a selection), but
+ * I guess the only theoretically correct course of action...
+ */
+ return deselectAll();
+ }
+ }
+
+ @Override
+ public boolean deselectAll() {
+ // deselect will fire the event
+ return deselect(getSelectedRows());
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * The returned Collection is in <strong>order of selection</strong> &ndash;
+ * the item that was first selected will be first in the collection, and so
+ * on. Should an item have been selected twice without being deselected in
+ * between, it will have remained in its original position.
+ */
+ @Override
+ public Collection<Object> getSelectedRows() {
+ // overridden only for JavaDoc
+ return super.getSelectedRows();
+ }
+
+ /**
+ * Resets the selection model.
+ * <p>
+ * Equivalent to calling {@link #deselectAll()}
+ */
+ @Override
+ public void reset() {
+ deselectAll();
+ }
+}
diff --git a/server/src/com/vaadin/ui/components/grid/selection/NoSelectionModel.java b/server/src/com/vaadin/ui/components/grid/selection/NoSelectionModel.java
new file mode 100644
index 0000000000..89c31398ea
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/selection/NoSelectionModel.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui.components.grid.selection;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import com.vaadin.ui.components.grid.Grid;
+
+/**
+ * A default implementation for a {@link SelectionModel.None}
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class NoSelectionModel implements SelectionModel.None {
+ @Override
+ public void setGrid(final Grid grid) {
+ // NOOP, not needed for anything
+ }
+
+ @Override
+ public boolean isSelected(final Object itemId) {
+ return false;
+ }
+
+ @Override
+ public Collection<Object> getSelectedRows() {
+ return Collections.emptyList();
+ }
+
+ /**
+ * Semantically resets the selection model.
+ * <p>
+ * Effectively a no-op.
+ */
+ @Override
+ public void reset() {
+ // NOOP
+ }
+}
diff --git a/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeEvent.java b/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeEvent.java
new file mode 100644
index 0000000000..af6a37dfde
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeEvent.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui.components.grid.selection;
+
+import java.util.Collection;
+import java.util.EventObject;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+import com.google.gwt.thirdparty.guava.common.collect.Sets;
+import com.vaadin.ui.components.grid.Grid;
+
+/**
+ * An event that specifies what in a selection has changed, and where the
+ * selection took place.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class SelectionChangeEvent extends EventObject {
+
+ private LinkedHashSet<Object> oldSelection;
+ private LinkedHashSet<Object> newSelection;
+
+ public SelectionChangeEvent(Grid source, Collection<Object> oldSelection,
+ Collection<Object> newSelection) {
+ super(source);
+ this.oldSelection = new LinkedHashSet<Object>(oldSelection);
+ this.newSelection = new LinkedHashSet<Object>(newSelection);
+ }
+
+ /**
+ * A {@link Collection} of all the itemIds that became selected.
+ * <p>
+ * <em>Note:</em> this excludes all itemIds that might have been previously
+ * selected.
+ *
+ * @return a Collection of the itemIds that became selected
+ */
+ public Set<Object> getAdded() {
+ return Sets.difference(newSelection, oldSelection);
+ }
+
+ /**
+ * A {@link Collection} of all the itemIds that became deselected.
+ * <p>
+ * <em>Note:</em> this excludes all itemIds that might have been previously
+ * deselected.
+ *
+ * @return a Collection of the itemIds that became deselected
+ */
+ public Set<Object> getRemoved() {
+ return Sets.difference(oldSelection, newSelection);
+ }
+
+ @Override
+ public Grid getSource() {
+ return (Grid) super.getSource();
+ }
+}
diff --git a/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeListener.java b/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeListener.java
new file mode 100644
index 0000000000..0d10e8c74d
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeListener.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui.components.grid.selection;
+
+import java.io.Serializable;
+
+/**
+ * The listener interface for receiving {@link SelectionChangeEvent
+ * SelectionChangeEvents}.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public interface SelectionChangeListener extends Serializable {
+ /**
+ * Notifies the listener that the selection state has changed.
+ *
+ * @param event
+ * the selection change event
+ */
+ void selectionChange(SelectionChangeEvent event);
+}
diff --git a/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeNotifier.java b/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeNotifier.java
new file mode 100644
index 0000000000..40cef965dd
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeNotifier.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui.components.grid.selection;
+
+import java.io.Serializable;
+
+/**
+ * The interface for adding and removing listeners for
+ * {@link SelectionChangeEvent SelectionChangeEvents}.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public interface SelectionChangeNotifier extends Serializable {
+ /**
+ * Registers a new selection change listener
+ *
+ * @param listener
+ * the listener to register
+ */
+ void addSelectionChangeListener(SelectionChangeListener listener);
+
+ /**
+ * Removes a previously registered selection change listener
+ *
+ * @param listener
+ * the listener to remove
+ */
+ void removeSelectionChangeListener(SelectionChangeListener listener);
+}
diff --git a/server/src/com/vaadin/ui/components/grid/selection/SelectionModel.java b/server/src/com/vaadin/ui/components/grid/selection/SelectionModel.java
new file mode 100644
index 0000000000..60bb130ab1
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/selection/SelectionModel.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui.components.grid.selection;
+
+import java.io.Serializable;
+import java.util.Collection;
+
+import com.vaadin.ui.components.grid.Grid;
+
+/**
+ * The server-side interface that controls Grid's selection state.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public interface SelectionModel extends Serializable {
+ /**
+ * Checks whether an item is selected or not.
+ *
+ * @param itemId
+ * the item id to check for
+ * @return <code>true</code> iff the item is selected
+ */
+ boolean isSelected(Object itemId);
+
+ /**
+ * Returns a collection of all the currently selected itemIds.
+ *
+ * @return a collection of all the currently selected itemIds
+ */
+ Collection<Object> getSelectedRows();
+
+ /**
+ * Injects the current {@link Grid} instance into the SelectionModel.
+ * <p>
+ * <em>Note:</em> This method should not be called manually.
+ *
+ * @param grid
+ * the Grid in which the SelectionModel currently is, or
+ * <code>null</code> when a selection model is being detached
+ * from a Grid.
+ */
+ void setGrid(Grid grid);
+
+ /**
+ * Resets the SelectiomModel to an initial state.
+ * <p>
+ * Most often this means that the selection state is cleared, but
+ * implementations are free to interpret the "initial state" as they wish.
+ * Some, for example, may want to keep the first selected item as selected.
+ */
+ void reset();
+
+ /**
+ * A SelectionModel that supports multiple selections to be made.
+ * <p>
+ * This interface has a contract of having the same behavior, no matter how
+ * the selection model is interacted with. In other words, if something is
+ * forbidden to do in e.g. the user interface, it must also be forbidden to
+ * do in the server-side and client-side APIs.
+ */
+ public interface Multi extends SelectionModel {
+
+ /**
+ * Marks items as selected.
+ * <p>
+ * This method does not clear any previous selection state, only adds to
+ * it.
+ *
+ * @param itemIds
+ * the itemId(s) to mark as selected
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if all the given itemIds already were
+ * selected
+ * @throws IllegalArgumentException
+ * if the <code>itemIds</code> varargs array is
+ * <code>null</code>
+ * @see #deselect(Object...)
+ */
+ boolean select(Object... itemIds) throws IllegalArgumentException;
+
+ /**
+ * Marks items as selected.
+ * <p>
+ * This method does not clear any previous selection state, only adds to
+ * it.
+ *
+ * @param itemIds
+ * the itemIds to mark as selected
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if all the given itemIds already were
+ * selected
+ * @throws IllegalArgumentException
+ * if <code>itemIds</code> is <code>null</code>
+ * @see #deselect(Collection)
+ */
+ boolean select(Collection<?> itemIds) throws IllegalArgumentException;
+
+ /**
+ * Marks items as deselected.
+ *
+ * @param itemIds
+ * the itemId(s) to remove from being selected
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if none the given itemIds were selected
+ * previously
+ * @throws IllegalArgumentException
+ * if the <code>itemIds</code> varargs array is
+ * <code>null</code>
+ * @see #select(Object...)
+ */
+ boolean deselect(Object... itemIds) throws IllegalArgumentException;
+
+ /**
+ * Marks items as deselected.
+ *
+ * @param itemIds
+ * the itemId(s) to remove from being selected
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if none the given itemIds were selected
+ * previously
+ * @throws IllegalArgumentException
+ * if <code>itemIds</code> is <code>null</code>
+ * @see #select(Collection)
+ */
+ boolean deselect(Collection<?> itemIds) throws IllegalArgumentException;
+
+ /**
+ * Marks all the items in the current Container as selected
+ *
+ * @return <code>true</code> iff some items were previously not selected
+ * @see #deselectAll()
+ */
+ boolean selectAll();
+
+ /**
+ * Marks all the items in the current Container as deselected
+ *
+ * @return <code>true</code> iff some items were previously selected
+ * @see #selectAll()
+ */
+ boolean deselectAll();
+ }
+
+ /**
+ * A SelectionModel that supports for only single rows to be selected at a
+ * time.
+ * <p>
+ * This interface has a contract of having the same behavior, no matter how
+ * the selection model is interacted with. In other words, if something is
+ * forbidden to do in e.g. the user interface, it must also be forbidden to
+ * do in the server-side and client-side APIs.
+ */
+ public interface Single extends SelectionModel {
+ /**
+ * Marks an item as selected.
+ *
+ * @param itemIds
+ * the itemId to mark as selected
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if the itemId already was selected
+ * @throws IllegalStateException
+ * if the selection was illegal. One such reason might be
+ * that the implementation already had an item selected, and
+ * that needs to be explicitly deselected before
+ * re-selecting something
+ * @see #deselect(Object)
+ */
+ boolean select(Object itemId) throws IllegalStateException;
+
+ /**
+ * Marks an item as deselected.
+ *
+ * @param itemId
+ * the itemId to remove from being selected
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if the itemId already was selected
+ * @throws IllegalStateException
+ * if the deselection was illegal. One such reason might be
+ * that the implementation enforces that an item is always
+ * selected
+ * @see #select(Object)
+ */
+ boolean deselect(Object itemId) throws IllegalStateException;
+
+ /**
+ * Gets the item id of the currently selected item.
+ *
+ * @return the item id of the currently selected item, or
+ * <code>null</code> if nothing is selected
+ */
+ Object getSelectedRow();
+ }
+
+ /**
+ * A SelectionModel that does not allow for rows to be selected.
+ * <p>
+ * This interface has a contract of having the same behavior, no matter how
+ * the selection model is interacted with. In other words, if the developer
+ * is unable to select something programmatically, it is not allowed for the
+ * end-user to select anything, either.
+ */
+ public interface None extends SelectionModel {
+
+ /**
+ * {@inheritDoc}
+ *
+ * @return always <code>false</code>.
+ */
+ @Override
+ public boolean isSelected(Object itemId);
+
+ /**
+ * {@inheritDoc}
+ *
+ * @return always an empty collection.
+ */
+ @Override
+ public Collection<Object> getSelectedRows();
+ }
+}
diff --git a/server/src/com/vaadin/ui/components/grid/selection/SingleSelectionModel.java b/server/src/com/vaadin/ui/components/grid/selection/SingleSelectionModel.java
new file mode 100644
index 0000000000..0f6e8a296d
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/selection/SingleSelectionModel.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui.components.grid.selection;
+
+import java.util.Collection;
+import java.util.Collections;
+
+/**
+ * A default implementation of a {@link SelectionModel.Single}
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class SingleSelectionModel extends AbstractSelectionModel implements
+ SelectionModel.Single {
+ @Override
+ public boolean select(final Object itemId) {
+ final Object selectedRow = getSelectedRow();
+ final boolean modified = selection.add(itemId);
+ if (modified) {
+ final Collection<Object> deselected;
+ if (selectedRow != null) {
+ deselectInternal(selectedRow, false);
+ deselected = Collections.singleton(selectedRow);
+ } else {
+ deselected = Collections.emptySet();
+ }
+
+ fireSelectionChangeEvent(deselected, selection);
+ }
+
+ return modified;
+ }
+
+ @Override
+ public boolean deselect(final Object itemId) {
+ return deselectInternal(itemId, true);
+ }
+
+ private boolean deselectInternal(final Object itemId,
+ boolean fireEventIfNeeded) {
+ final boolean modified = selection.remove(itemId);
+ if (fireEventIfNeeded && modified) {
+ fireSelectionChangeEvent(Collections.singleton(itemId),
+ Collections.emptySet());
+ }
+ return modified;
+ }
+
+ @Override
+ public Object getSelectedRow() {
+ if (selection.isEmpty()) {
+ return null;
+ } else {
+ return selection.iterator().next();
+ }
+ }
+
+ /**
+ * Resets the selection state.
+ * <p>
+ * If an item is selected, it will become deselected.
+ */
+ @Override
+ public void reset() {
+ deselect(getSelectedRow());
+ }
+}
diff --git a/server/src/com/vaadin/ui/components/grid/sort/Sort.java b/server/src/com/vaadin/ui/components/grid/sort/Sort.java
new file mode 100644
index 0000000000..54831378b6
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/sort/Sort.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui.components.grid.sort;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.vaadin.shared.ui.grid.SortDirection;
+
+/**
+ * Fluid Sort API. Provides a convenient, human-readable way of specifying
+ * multi-column sort order.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class Sort implements Serializable {
+
+ private final Sort previous;
+ private final SortOrder order;
+
+ /**
+ * Initial constructor, called by the static by() methods.
+ *
+ * @param propertyId
+ * a property ID, corresponding to a property in the data source
+ * @param direction
+ * a sort direction value
+ */
+ private Sort(Object propertyId, SortDirection direction) {
+ previous = null;
+ order = new SortOrder(propertyId, direction);
+ }
+
+ /**
+ * Chaining constructor, called by the non-static then() methods. This
+ * constructor links to the previous Sort object.
+ *
+ * @param previous
+ * the sort marker that comes before this one
+ * @param propertyId
+ * a property ID, corresponding to a property in the data source
+ * @param direction
+ * a sort direction value
+ */
+ private Sort(Sort previous, Object propertyId, SortDirection direction) {
+ this.previous = previous;
+ order = new SortOrder(propertyId, direction);
+
+ Sort s = previous;
+ while (s != null) {
+ if (s.order.getPropertyId() == propertyId) {
+ throw new IllegalStateException(
+ "Can not sort along the same property (" + propertyId
+ + ") twice!");
+ }
+ s = s.previous;
+ }
+
+ }
+
+ /**
+ * Start building a Sort order by sorting a provided column in ascending
+ * order.
+ *
+ * @param propertyId
+ * a property id, corresponding to a data source property
+ * @return a sort object
+ */
+ public static Sort by(Object propertyId) {
+ return by(propertyId, SortDirection.ASCENDING);
+ }
+
+ /**
+ * Start building a Sort order by sorting a provided column.
+ *
+ * @param propertyId
+ * a property id, corresponding to a data source property
+ * @param direction
+ * a sort direction value
+ * @return a sort object
+ */
+ public static Sort by(Object propertyId, SortDirection direction) {
+ return new Sort(propertyId, direction);
+ }
+
+ /**
+ * Continue building a Sort order. The provided property is sorted in
+ * ascending order if the previously added properties have been evaluated as
+ * equals.
+ *
+ * @param propertyId
+ * a property id, corresponding to a data source property
+ * @return a sort object
+ */
+ public Sort then(Object propertyId) {
+ return then(propertyId, SortDirection.ASCENDING);
+ }
+
+ /**
+ * Continue building a Sort order. The provided property is sorted in
+ * specified order if the previously added properties have been evaluated as
+ * equals.
+ *
+ * @param propertyId
+ * a property id, corresponding to a data source property
+ * @param direction
+ * a sort direction value
+ * @return a sort object
+ */
+ public Sort then(Object propertyId, SortDirection direction) {
+ return new Sort(this, propertyId, direction);
+ }
+
+ /**
+ * Build a sort order list, ready to be passed to Grid
+ *
+ * @return a sort order list.
+ */
+ public List<SortOrder> build() {
+
+ int count = 1;
+ Sort s = this;
+ while (s.previous != null) {
+ s = s.previous;
+ ++count;
+ }
+
+ List<SortOrder> order = new ArrayList<SortOrder>(count);
+
+ s = this;
+ do {
+ order.add(0, s.order);
+ s = s.previous;
+ } while (s != null);
+
+ return order;
+ }
+}
diff --git a/server/src/com/vaadin/ui/components/grid/sort/SortOrder.java b/server/src/com/vaadin/ui/components/grid/sort/SortOrder.java
new file mode 100644
index 0000000000..a76148fe0c
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/sort/SortOrder.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui.components.grid.sort;
+
+import java.io.Serializable;
+
+import com.vaadin.shared.ui.grid.SortDirection;
+
+/**
+ * Sort order descriptor. Links together a {@link SortDirection} value and a
+ * Vaadin container property ID.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class SortOrder implements Serializable {
+
+ private final Object propertyId;
+ private final SortDirection direction;
+
+ /**
+ * Create a SortOrder object. Both arguments must be non-null.
+ *
+ * @param propertyId
+ * id of the data source property to sort by
+ * @param direction
+ * value indicating whether the property id should be sorted in
+ * ascending or descending order
+ */
+ public SortOrder(Object propertyId, SortDirection direction) {
+ if (propertyId == null) {
+ throw new IllegalArgumentException("Property ID can not be null!");
+ }
+ if (direction == null) {
+ throw new IllegalArgumentException(
+ "Direction value can not be null!");
+ }
+ this.propertyId = propertyId;
+ this.direction = direction;
+ }
+
+ /**
+ * Returns the property ID.
+ *
+ * @return a property ID
+ */
+ public Object getPropertyId() {
+ return propertyId;
+ }
+
+ /**
+ * Returns the {@link SortDirection} value.
+ *
+ * @return a sort direction value
+ */
+ public SortDirection getDirection() {
+ return direction;
+ }
+
+ @Override
+ public String toString() {
+ return propertyId + " " + direction;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + direction.hashCode();
+ result = prime * result + propertyId.hashCode();
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ } else if (obj == null) {
+ return false;
+ } else if (getClass() != obj.getClass()) {
+ return false;
+ }
+
+ SortOrder other = (SortOrder) obj;
+ if (direction != other.direction) {
+ return false;
+ } else if (!propertyId.equals(other.propertyId)) {
+ return false;
+ }
+ return true;
+ }
+
+}