/*
* 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;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
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.Indexed;
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.Item;
import com.vaadin.data.Property;
import com.vaadin.data.RpcDataProviderExtension;
import com.vaadin.data.RpcDataProviderExtension.DataProviderKeyMapper;
import com.vaadin.data.fieldgroup.FieldGroup;
import com.vaadin.data.fieldgroup.FieldGroup.BindException;
import com.vaadin.data.fieldgroup.FieldGroup.CommitException;
import com.vaadin.data.fieldgroup.FieldGroupFieldFactory;
import com.vaadin.data.sort.Sort;
import com.vaadin.data.sort.SortOrder;
import com.vaadin.data.util.IndexedContainer;
import com.vaadin.data.util.converter.Converter;
import com.vaadin.data.util.converter.ConverterUtil;
import com.vaadin.event.SelectionChangeEvent;
import com.vaadin.event.SelectionChangeEvent.SelectionChangeListener;
import com.vaadin.event.SelectionChangeEvent.SelectionChangeNotifier;
import com.vaadin.event.SortOrderChangeEvent;
import com.vaadin.event.SortOrderChangeEvent.SortOrderChangeListener;
import com.vaadin.event.SortOrderChangeEvent.SortOrderChangeNotifier;
import com.vaadin.server.AbstractClientConnector;
import com.vaadin.server.AbstractExtension;
import com.vaadin.server.ErrorHandler;
import com.vaadin.server.ErrorMessage;
import com.vaadin.server.JsonCodec;
import com.vaadin.server.KeyMapper;
import com.vaadin.server.VaadinSession;
import com.vaadin.shared.ui.grid.EditorRowClientRpc;
import com.vaadin.shared.ui.grid.EditorRowServerRpc;
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.GridStaticSectionState;
import com.vaadin.shared.ui.grid.GridStaticSectionState.CellState;
import com.vaadin.shared.ui.grid.GridStaticSectionState.RowState;
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.util.SharedUtil;
import com.vaadin.ui.renderer.Renderer;
import com.vaadin.ui.renderer.TextRenderer;
import com.vaadin.util.ReflectTools;
import elemental.json.Json;
import elemental.json.JsonArray;
import elemental.json.JsonObject;
import elemental.json.JsonValue;
/**
* A grid component for displaying tabular data.
*
* 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}.
*
*
*
*
*
*
Converters and Renderers
*
* 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.
*
* Usually a renderer takes some kind of object, and converts it into a
* HTML-formatted string.
*
*
* Grid grid = new Grid(myContainer);
* Column column = grid.getColumn(STRING_DATE_PROPERTY);
* column.setConverter(new StringToDateConverter());
* column.setRenderer(new MyColorfulDateRenderer());
*
*
*
Lazy Loading
*
* 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.
*
*
Selection Modes and Models
*
* Grid supports three selection {@link SelectionMode modes} (single,
* multi, none), and comes bundled with one
* {@link SelectionModel model} for each of the modes. The distinction
* between a selection mode and selection model is as follows: a mode
* 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.
*
*
* 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());
*
*
* @since
* @author Vaadin Ltd
*/
public class Grid extends AbstractComponent implements SelectionChangeNotifier,
SortOrderChangeNotifier, SelectiveRenderer {
/**
* Selection modes representing built-in {@link SelectionModel
* SelectionModels} that come bundled with {@link Grid}.
*
* 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 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 true
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 getSelectedRows();
/**
* Injects the current {@link Grid} instance into the SelectionModel.
*
* Note: This method should not be called manually.
*
* @param grid
* the Grid in which the SelectionModel currently is, or
* null
when a selection model is being detached
* from a Grid.
*/
void setGrid(Grid grid);
/**
* Resets the SelectiomModel to an initial state.
*
* 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.
*
* 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.
*
* This method does not clear any previous selection state, only
* adds to it.
*
* @param itemIds
* the itemId(s) to mark as selected
* @return true
if the selection state changed.
* false
if all the given itemIds already were
* selected
* @throws IllegalArgumentException
* if the itemIds
varargs array is
* null
or given itemIds don't exist in the
* container of Grid
* @see #deselect(Object...)
*/
boolean select(Object... itemIds) throws IllegalArgumentException;
/**
* Marks items as selected.
*
* This method does not clear any previous selection state, only
* adds to it.
*
* @param itemIds
* the itemIds to mark as selected
* @return true
if the selection state changed.
* false
if all the given itemIds already were
* selected
* @throws IllegalArgumentException
* if itemIds
is null
or given
* itemIds don't exist in the container of Grid
* @see #deselect(Collection)
*/
boolean select(Collection> itemIds)
throws IllegalArgumentException;
/**
* Marks items as deselected.
*
* @param itemIds
* the itemId(s) to remove from being selected
* @return true
if the selection state changed.
* false
if none the given itemIds were
* selected previously
* @throws IllegalArgumentException
* if the itemIds
varargs array is
* null
* @see #select(Object...)
*/
boolean deselect(Object... itemIds) throws IllegalArgumentException;
/**
* Marks items as deselected.
*
* @param itemIds
* the itemId(s) to remove from being selected
* @return true
if the selection state changed.
* false
if none the given itemIds were
* selected previously
* @throws IllegalArgumentException
* if itemIds
is null
* @see #select(Collection)
*/
boolean deselect(Collection> itemIds)
throws IllegalArgumentException;
/**
* Marks all the items in the current Container as selected
*
* @return true
iff some items were previously not
* selected
* @see #deselectAll()
*/
boolean selectAll();
/**
* Marks all the items in the current Container as deselected
*
* @return true
iff some items were previously selected
* @see #selectAll()
*/
boolean deselectAll();
}
/**
* A SelectionModel that supports for only single rows to be selected at
* a time.
*
* 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; null
for
* deselect
* @return true
if the selection state changed.
* false
if the itemId already was selected
* @throws IllegalStateException
* if the selection was illegal. One such reason might
* be that the given id was null, indicating a deselect,
* but implementation doesn't allow deselecting.
* re-selecting something
* @throws IllegalArgumentException
* if given itemId does not exist in the container of
* Grid
*/
boolean select(Object itemId) throws IllegalStateException,
IllegalArgumentException;
/**
* Gets the item id of the currently selected item.
*
* @return the item id of the currently selected item, or
* null
if nothing is selected
*/
Object getSelectedRow();
}
/**
* A SelectionModel that does not allow for rows to be selected.
*
* 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 false
.
*/
@Override
public boolean isSelected(Object itemId);
/**
* {@inheritDoc}
*
* @return always an empty collection.
*/
@Override
public Collection getSelectedRows();
}
}
/**
* A base class for SelectionModels that contains some of the logic that is
* reusable.
*
* @since
* @author Vaadin Ltd
*/
public static abstract class AbstractSelectionModel implements
SelectionModel {
protected final LinkedHashSet selection = new LinkedHashSet();
protected Grid grid = null;
@Override
public boolean isSelected(final Object itemId) {
return selection.contains(itemId);
}
@Override
public Collection getSelectedRows() {
return new ArrayList(selection);
}
@Override
public void setGrid(final Grid grid) {
this.grid = grid;
}
/**
* Sanity check for existence of item id.
*
* @param itemId
* item id to be selected / deselected
*
* @throws IllegalArgumentException
* if item Id doesn't exist in the container of Grid
*/
protected void checkItemIdExists(Object itemId)
throws IllegalArgumentException {
if (!grid.getContainerDataSource().containsId(itemId)) {
throw new IllegalArgumentException("Given item id (" + itemId
+ ") does not exist in the container");
}
}
/**
* Sanity check for existence of item ids in given collection.
*
* @param itemIds
* item id collection to be selected / deselected
*
* @throws IllegalArgumentException
* if at least one item id doesn't exist in the container of
* Grid
*/
protected void checkItemIdsExist(Collection> itemIds)
throws IllegalArgumentException {
for (Object itemId : itemIds) {
checkItemIdExists(itemId);
}
}
/**
* Fires a {@link SelectionChangeEvent} to all the
* {@link SelectionChangeListener SelectionChangeListeners} currently
* added to the Grid in which this SelectionModel is.
*
* 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 before this event happened
* @param newSelection
* the complete {@link Collection} of the itemIds that are
* selected after this event happened
*/
protected void fireSelectionChangeEvent(
final Collection oldSelection,
final Collection newSelection) {
grid.fireSelectionChangeEvent(oldSelection, newSelection);
}
}
/**
* A default implementation of a {@link SelectionModel.Single}
*
* @since
* @author Vaadin Ltd
*/
public static class SingleSelectionModel extends AbstractSelectionModel
implements SelectionModel.Single {
@Override
public boolean select(final Object itemId) {
if (itemId == null) {
return deselect(getSelectedRow());
}
checkItemIdExists(itemId);
final Object selectedRow = getSelectedRow();
final boolean modified = selection.add(itemId);
if (modified) {
final Collection deselected;
if (selectedRow != null) {
deselectInternal(selectedRow, false);
deselected = Collections.singleton(selectedRow);
} else {
deselected = Collections.emptySet();
}
fireSelectionChangeEvent(deselected, selection);
}
return modified;
}
private 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.
*
* If an item is selected, it will become deselected.
*/
@Override
public void reset() {
deselect(getSelectedRow());
}
}
/**
* A default implementation for a {@link SelectionModel.None}
*
* @since
* @author Vaadin Ltd
*/
public static 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 getSelectedRows() {
return Collections.emptyList();
}
/**
* Semantically resets the selection model.
*
* Effectively a no-op.
*/
@Override
public void reset() {
// NOOP
}
}
/**
* A default implementation of a {@link SelectionModel.Multi}
*
* @since
* @author Vaadin Ltd
*/
public static class MultiSelectionModel extends AbstractSelectionModel
implements SelectionModel.Multi {
/**
* The default selection size limit.
*
* @see #setSelectionLimit(int)
*/
public static final int DEFAULT_MAX_SELECTIONS = 1000;
private int selectionLimit = DEFAULT_MAX_SELECTIONS;
@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");
}
}
/**
* {@inheritDoc}
*
* All items might not be selected if the limit set using
* {@link #setSelectionLimit(int)} is exceeded.
*/
@Override
public boolean select(final Collection> itemIds)
throws IllegalArgumentException {
if (itemIds == null) {
throw new IllegalArgumentException("itemIds may not be null");
}
// Sanity check
checkItemIdsExist(itemIds);
final boolean selectionWillChange = !selection.containsAll(itemIds)
&& selection.size() < selectionLimit;
if (selectionWillChange) {
final HashSet oldSelection = new HashSet(
selection);
if (selection.size() + itemIds.size() >= selectionLimit) {
// Add one at a time if there's a risk of overflow
Iterator> iterator = itemIds.iterator();
while (iterator.hasNext()
&& selection.size() < selectionLimit) {
selection.add(iterator.next());
}
} else {
selection.addAll(itemIds);
}
fireSelectionChangeEvent(oldSelection, selection);
}
return selectionWillChange;
}
/**
* Sets the maximum number of rows that can be selected at once. This is
* a mechanism to prevent exhausting server memory in situations where
* users select lots of rows. If the limit is reached, newly selected
* rows will not become recorded.
*
* Old selections are not discarded if the current number of selected
* row exceeds the new limit.
*
* The default limit is {@value #DEFAULT_MAX_SELECTIONS} rows.
*
* @param selectionLimit
* the non-negative selection limit to set
* @throws IllegalArgumentException
* if the limit is negative
*/
public void setSelectionLimit(int selectionLimit) {
if (selectionLimit < 0) {
throw new IllegalArgumentException(
"The selection limit must be non-negative");
}
this.selectionLimit = selectionLimit;
}
/**
* Gets the selection limit.
*
* @see #setSelectionLimit(int)
*
* @return the selection limit
*/
public int getSelectionLimit() {
return selectionLimit;
}
@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 oldSelection = new HashSet(
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}
*
* The returned Collection is in order of selection
* – 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 getSelectedRows() {
// overridden only for JavaDoc
return super.getSelectedRows();
}
/**
* Resets the selection model.
*
* Equivalent to calling {@link #deselectAll()}
*/
@Override
public void reset() {
deselectAll();
}
}
/**
* Callback interface for generating custom style names for data rows and
* cells.
*
* @see Grid#setCellStyleGenerator(CellStyleGenerator)
*/
public interface CellStyleGenerator extends Serializable {
/**
* Called by Grid to generate a style name for a row or cell element.
* Row styles are generated when the column parameter is
* null
, otherwise a cell style is generated.
*
* @param grid
* the source grid
* @param itemId
* the itemId of the target row
* @param propertyId
* the propertyId of the target cell, null
when
* getting row style
* @return the style name to add to this cell or row element, or
* null
to not set any style
*/
public abstract String getStyle(Grid grid, Object itemId,
Object propertyId);
}
/**
* Abstract base class for Grid header and footer sections.
*
* @param
* the type of the rows in the section
*/
protected static abstract class StaticSection>
implements Serializable {
/**
* Abstract base class for Grid header and footer rows.
*
* @param
* the type of the cells in the row
*/
abstract static class StaticRow implements
Serializable {
private RowState rowState = new RowState();
protected StaticSection> section;
private Map cells = new LinkedHashMap();
private Map, CELLTYPE> cellGroups = new HashMap, CELLTYPE>();
protected StaticRow(StaticSection> section) {
this.section = section;
}
protected void addCell(Object propertyId) {
CELLTYPE cell = createCell();
cell.setColumnId(section.grid.getColumn(propertyId).getState().id);
cells.put(propertyId, cell);
rowState.cells.add(cell.getCellState());
}
protected void removeCell(Object propertyId) {
CELLTYPE cell = cells.remove(propertyId);
if (cell != null) {
Set cellGroupForCell = getCellGroupForCell(cell);
if (cellGroupForCell != null) {
removeCellFromGroup(cell, cellGroupForCell);
}
rowState.cells.remove(cell.getCellState());
}
}
private void removeCellFromGroup(CELLTYPE cell,
Set cellGroup) {
String columnId = cell.getColumnId();
for (Set group : rowState.cellGroups.keySet()) {
if (group.contains(columnId)) {
if (group.size() > 2) {
// Update map key correctly
CELLTYPE mergedCell = cellGroups.remove(cellGroup);
cellGroup.remove(cell);
cellGroups.put(cellGroup, mergedCell);
group.remove(columnId);
} else {
rowState.cellGroups.remove(group);
cellGroups.remove(cellGroup);
}
return;
}
}
}
/**
* 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 for the given property id on this row. If the
* column is merged returned cell is the cell for the whole group.
*
* @param propertyId
* the property id of the column
* @return the cell for the given property, merged cell for merged
* properties, null if not found
*/
public CELLTYPE getCell(Object propertyId) {
CELLTYPE cell = cells.get(propertyId);
Set cellGroup = getCellGroupForCell(cell);
if (cellGroup != null) {
cell = cellGroups.get(cellGroup);
}
return cell;
}
/**
* Merges columns cells in a row
*
* @param propertyIds
* The property ids of columns to merge
* @return The remaining visible cell after the merge
*/
public CELLTYPE join(Object... propertyIds) {
assert propertyIds.length > 1 : "You need to merge at least 2 properties";
Set cells = new HashSet();
for (int i = 0; i < propertyIds.length; ++i) {
cells.add(getCell(propertyIds[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) {
assert cells.length > 1 : "You need to merge at least 2 cells";
return join(new HashSet(Arrays.asList(cells)));
}
protected CELLTYPE join(Set cells) {
for (CELLTYPE cell : cells) {
if (getCellGroupForCell(cell) != null) {
throw new IllegalArgumentException(
"Cell already merged");
} else if (!this.cells.containsValue(cell)) {
throw new IllegalArgumentException(
"Cell does not exist on this row");
}
}
// Create new cell data for the group
CELLTYPE newCell = createCell();
Set columnGroup = new HashSet();
for (CELLTYPE cell : cells) {
columnGroup.add(cell.getColumnId());
}
rowState.cellGroups.put(columnGroup, newCell.getCellState());
cellGroups.put(cells, newCell);
return newCell;
}
private Set getCellGroupForCell(CELLTYPE cell) {
for (Set group : cellGroups.keySet()) {
if (group.contains(cell)) {
return group;
}
}
return null;
}
/**
* Returns the custom style name for this row.
*
* @return the style name or null if no style name has been set
*/
public String getStyleName() {
return getRowState().styleName;
}
/**
* Sets a custom style name for this row.
*
* @param styleName
* the style name to set or null to not use any style
* name
*/
public void setStyleName(String styleName) {
getRowState().styleName = styleName;
}
}
/**
* 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;
}
void setColumnId(String id) {
cellState.columnId = id;
}
String getColumnId() {
return cellState.columnId;
}
/**
* 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();
}
/**
* Returns the custom style name for this cell.
*
* @return the style name or null if no style name has been set
*/
public String getStyleName() {
return cellState.styleName;
}
/**
* Sets a custom style name for this cell.
*
* @param styleName
* the style name to set or null to not use any style
* name
*/
public void setStyleName(String styleName) {
cellState.styleName = styleName;
row.section.markAsDirty();
}
}
protected Grid grid;
protected List rows = new ArrayList();
/**
* 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 IllegalArgumentException
* if no row exists at given index
* @see #removeRow(StaticRow)
* @see #addRowAt(int)
* @see #appendRow()
* @see #prependRow()
*/
public ROWTYPE removeRow(int rowIndex) {
if (rowIndex >= rows.size() || rowIndex < 0) {
throw new IllegalArgumentException("No row at given index "
+ 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
* @see #removeRow(int)
* @see #addRowAt(int)
* @see #appendRow()
* @see #prependRow()
*/
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) {
if (rowIndex >= rows.size() || rowIndex < 0) {
throw new IllegalArgumentException("No row at given index "
+ rowIndex);
}
return rows.get(rowIndex);
}
/**
* Adds a new row at the top of this section.
*
* @return the new row
* @see #appendRow()
* @see #addRowAt(int)
* @see #removeRow(StaticRow)
* @see #removeRow(int)
*/
public ROWTYPE prependRow() {
return addRowAt(0);
}
/**
* Adds a new row at the bottom of this section.
*
* @return the new row
* @see #prependRow()
* @see #addRowAt(int)
* @see #removeRow(StaticRow)
* @see #removeRow(int)
*/
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
* @see #appendRow()
* @see #prependRow()
* @see #removeRow(StaticRow)
* @see #removeRow(int)
*/
public ROWTYPE addRowAt(int index) {
if (index > rows.size() || index < 0) {
throw new IllegalArgumentException(
"Unable to add row at index " + 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();
}
/**
* Removes a column for given property id from the section.
*
* @param propertyId
* property to be removed
*/
protected void removeColumn(Object propertyId) {
for (ROWTYPE row : rows) {
row.removeCell(propertyId);
}
}
/**
* Adds a column for given property id to the section.
*
* @param propertyId
* property to be added
*/
protected void addColumn(Object propertyId) {
for (ROWTYPE row : rows) {
row.addCell(propertyId);
}
}
/**
* Performs a sanity check that section is in correct state.
*
* @throws IllegalStateException
* if merged cells are not i n continuous range
*/
protected void sanityCheck() throws IllegalStateException {
List columnOrder = grid.getState().columnOrder;
for (ROWTYPE row : rows) {
for (Set cellGroup : row.getRowState().cellGroups
.keySet()) {
if (!checkCellGroupAndOrder(columnOrder, cellGroup)) {
throw new IllegalStateException(
"Not all merged cells were in a continuous range.");
}
}
}
}
private boolean checkCellGroupAndOrder(List columnOrder,
Set cellGroup) {
if (!columnOrder.containsAll(cellGroup)) {
return false;
}
for (int i = 0; i < columnOrder.size(); ++i) {
if (!cellGroup.contains(columnOrder.get(i))) {
continue;
}
for (int j = 1; j < cellGroup.size(); ++j) {
if (!cellGroup.contains(columnOrder.get(i + j))) {
return false;
}
}
return true;
}
return false;
}
}
/**
* Represents the header section of a Grid.
*/
protected static class Header extends StaticSection {
private HeaderRow defaultRow = null;
private final GridStaticSectionState headerState = new GridStaticSectionState();
protected Header(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;
}
@Override
protected void sanityCheck() throws IllegalStateException {
super.sanityCheck();
boolean hasDefaultRow = false;
for (HeaderRow row : rows) {
if (row.getRowState().defaultRow) {
if (!hasDefaultRow) {
hasDefaultRow = true;
} else {
throw new IllegalStateException(
"Multiple default rows in header");
}
}
}
}
}
/**
* Represents a header row in Grid.
*/
public static class HeaderRow extends StaticSection.StaticRow {
protected HeaderRow(StaticSection> section) {
super(section);
}
private void setDefaultRow(boolean value) {
getRowState().defaultRow = value;
}
@Override
protected HeaderCell createCell() {
return new HeaderCell(this);
}
}
/**
* Represents a header cell in Grid. Can be a merged cell for multiple
* columns.
*/
public static class HeaderCell extends StaticSection.StaticCell {
protected HeaderCell(HeaderRow row) {
super(row);
}
}
/**
* Represents the footer section of a Grid. By default Footer is not
* visible.
*/
protected static class Footer extends StaticSection {
private final GridStaticSectionState footerState = new GridStaticSectionState();
protected Footer(Grid grid) {
this.grid = grid;
grid.getState(true).footer = footerState;
}
@Override
protected GridStaticSectionState getSectionState() {
return footerState;
}
@Override
protected FooterRow createRow() {
return new FooterRow(this);
}
@Override
protected void sanityCheck() throws IllegalStateException {
super.sanityCheck();
}
}
/**
* Represents a footer row in Grid.
*/
public static class FooterRow extends StaticSection.StaticRow {
protected FooterRow(StaticSection> section) {
super(section);
}
@Override
protected FooterCell createCell() {
return new FooterCell(this);
}
}
/**
* Represents a footer cell in Grid.
*/
public static class FooterCell extends StaticSection.StaticCell {
protected FooterCell(FooterRow row) {
super(row);
}
}
/**
* A column in the grid. Can be obtained by calling
* {@link Grid#getColumn(Object propertyId)}.
*/
public static class Column 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;
/**
* Backing property for column
*/
private final Object columnProperty;
private Converter, Object> converter;
/**
* A check for allowing the {@link #Column(Grid, GridColumnState)
* constructor} to call {@link #setConverter(Converter)} with a
* null
, 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
* @param columnProperty
* the backing property id for this column
*/
Column(Grid grid, GridColumnState state, Object columnProperty) {
this.grid = grid;
this.state = state;
this.columnProperty = columnProperty;
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;
}
/**
* Return the property id for the backing property of this Column
*
* @return property id
*/
public Object getColumnProperty() {
return columnProperty;
}
/**
* 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
*/
public String getHeaderCaption() throws IllegalStateException {
checkColumnIsAttached();
HeaderRow row = grid.getHeader().getDefaultRow();
if (row != null) {
return row.getCell(grid.getPropertyIdByColumnId(state.id))
.getText();
}
return null;
}
/**
* Sets the caption of the header.
*
* @param caption
* the text to show in the caption
* @return the column itself
*
* @throws IllegalStateException
* if the column is no longer attached to any grid
*/
public Column setHeaderCaption(String caption)
throws IllegalStateException {
checkColumnIsAttached();
HeaderRow row = grid.getHeader().getDefaultRow();
if (row != null) {
row.getCell(grid.getPropertyIdByColumnId(state.id)).setText(
caption);
}
return this;
}
/**
* 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 double getWidth() throws IllegalStateException {
checkColumnIsAttached();
return state.width;
}
/**
* Sets the width (in pixels).
*
* @param pixelWidth
* the new pixel width of the column
* @return the column itself
*
* @throws IllegalStateException
* if the column is no longer attached to any grid
* @throws IllegalArgumentException
* thrown if pixel width is less than zero
*/
public Column setWidth(double pixelWidth) throws IllegalStateException,
IllegalArgumentException {
checkColumnIsAttached();
if (pixelWidth < 0) {
throw new IllegalArgumentException(
"Pixel width should be greated than 0 (in "
+ toString() + ")");
}
state.width = pixelWidth;
grid.markAsDirty();
return this;
}
/**
* 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.
*
* @return the column itself
*/
public Column setWidthUndefined() {
checkColumnIsAttached();
state.width = -1;
grid.markAsDirty();
return this;
}
/**
* 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.
*
* @return the column itself
*
* @throws IllegalArgumentException
* if the column is no longer attached to any grid
* @see Grid#setFrozenColumnCount(int)
*/
public Column setLastFrozenColumn() {
checkColumnIsAttached();
grid.setFrozenColumnCount(grid.getState(false).columnOrder
.indexOf(this) + 1);
return this;
}
/**
* Sets the renderer for this column.
*
* 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
* @return the column itself
*
* @throws IllegalArgumentException
* if no compatible converter could be found
*
* @see VaadinSession#getConverterFactory()
* @see ConverterUtil#getConverter(Class, Class, VaadinSession)
* @see #setConverter(Converter)
*/
public Column 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() + " (in "
+ toString() + ")");
}
return this;
}
/**
* 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
* @return the column itself
*
* @throws IllegalArgumentException
* if the renderer is already associated with a grid column
*/
public Column setRenderer(Renderer renderer,
Converter extends T, ?> converter) {
if (renderer.getParent() != null) {
throw new IllegalArgumentException(
"Cannot set a renderer that is already connected to a grid column (in "
+ toString() + ")");
}
if (getRenderer() != null) {
grid.removeExtension(getRenderer());
}
grid.addRenderer(renderer);
state.rendererConnector = renderer;
setConverter(converter);
return this;
}
/**
* 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
* @return the column itself
*
* @throws IllegalArgumentException
* if the types are not compatible
*/
public Column 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 + " (in " + toString() + ")");
} 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()
+ " (in " + toString() + ")");
}
}
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 compatible with each other (in "
+ toString() + ")");
}
}
isFirstConverterAssignment = false;
@SuppressWarnings("unchecked")
Converter, Object> castConverter = (Converter, Object>) converter;
this.converter = castConverter;
return this;
}
/**
* 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 boolean internalSetRenderer(Renderer 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
* true
if the sorting controls should be
* visible.
* @return the column itself
*/
public Column setSortable(boolean sortable) {
checkColumnIsAttached();
state.sortable = sortable;
grid.markAsDirty();
return this;
}
/**
* Are the sorting controls visible in the column header
*/
public boolean isSortable() {
return state.sortable;
}
@Override
public String toString() {
return getClass().getSimpleName() + "[propertyId:"
+ grid.getPropertyIdByColumnId(state.id) + "]";
}
}
/**
* 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
* the type this renderer knows how to present
*/
public static abstract class AbstractRenderer extends AbstractExtension
implements Renderer {
private final Class presentationType;
protected AbstractRenderer(Class presentationType) {
this.presentationType = presentationType;
}
/**
* This method is inherited from AbstractExtension but should never be
* called directly with an AbstractRenderer.
*/
@Deprecated
@Override
protected Class 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 getPresentationType() {
return presentationType;
}
@Override
public JsonValue encode(T value) {
return encode(value, getPresentationType());
}
/**
* Encodes the given value to JSON.
*
* This is a helper method that can be invoked by an
* {@link #encode(Object) encode(T)} override if serializing a value of
* type other than {@link #getPresentationType() the presentation type}
* is desired. For instance, a {@code Renderer} could first turn a
* date value into a formatted string and return
* {@code encode(dateString, String.class)}.
*
* @param value
* the value to be encoded
* @param type
* the type of the value
* @return a JSON representation of the given value
*/
protected JsonValue encode(U value, Class type) {
return JsonCodec.encode(value, null, type,
getUI().getConnectorTracker()).getEncodedValue();
}
/**
* Gets the item id for a row key.
*
* 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 rowKey
* the row key for which to retrieve an item id
* @return the item id corresponding to {@code key}
*/
protected Object getItemId(String rowKey) {
if (getParent() instanceof Grid) {
Grid grid = (Grid) getParent();
return grid.getKeyMapper().getItemId(rowKey);
} else {
throw new IllegalStateException(
"Renderers can be used only with Grid");
}
}
}
/**
* The data source attached to the grid
*/
private Container.Indexed datasource;
/**
* Property id to column instance mapping
*/
private final Map columns = new HashMap();
/**
* Key generator for column server-to-client communication
*/
private final KeyMapper columnKeys = new KeyMapper();
/**
* The current sort order
*/
private final List sortOrder = new ArrayList();
/**
* 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(event.getContainer()
.getContainerPropertyIds());
// Cleanup columns that are no longer in grid
List removedColumns = new LinkedList();
for (Object columnId : columns.keySet()) {
if (!properties.contains(columnId)) {
removedColumns.add(columnId);
}
}
for (Object columnId : removedColumns) {
removeColumn(columnId);
columnKeys.remove(columnId);
}
datasourceExtension.propertiesRemoved(removedColumns);
// Add new columns
HashSet addedPropertyIds = new HashSet();
for (Object propertyId : properties) {
if (!columns.containsKey(propertyId)) {
appendColumn(propertyId);
addedPropertyIds.add(propertyId);
}
}
datasourceExtension.propertiesAdded(addedPropertyIds);
if (getFrozenColumnCount() > columns.size()) {
setFrozenColumnCount(columns.size());
}
// Update sortable columns
if (event.getContainer() instanceof Sortable) {
Collection> sortableProperties = ((Sortable) event
.getContainer()).getSortableContainerPropertyIds();
for (Entry columnEntry : columns.entrySet()) {
columnEntry.getValue().setSortable(
sortableProperties.contains(columnEntry.getKey()));
}
}
}
};
private RpcDataProviderExtension datasourceExtension;
/**
* The selection model that is currently in use. Never null
* after the constructor has been run.
*/
private SelectionModel selectionModel;
/**
* Used to know whether selection change events originate from the server or
* the client so the selection change handler knows whether the changes
* should be sent to the client.
*/
private boolean applyingSelectionFromClient;
private final Header header = new Header(this);
private final Footer footer = new Footer(this);
private Object editedItemId = null;
private FieldGroup editorRowFieldGroup = new FieldGroup();
private HashSet uneditableProperties = new HashSet();
private ErrorHandler editorRowErrorHandler;
private CellStyleGenerator cellStyleGenerator;
/**
* true
if Grid is using the internal IndexedContainer created
* in Grid() constructor, or false
if the user has set their
* own Container.
*
* @see #setContainerDataSource()
* @see #Grid()
*/
private boolean defaultContainer = true;
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 with a new {@link IndexedContainer} as the datasource.
*/
public Grid() {
internalSetContainerDataSource(new IndexedContainer());
initGrid();
}
/**
* Creates a new Grid using the given datasource.
*
* @param datasource
* the data source for the grid
*/
public Grid(final Container.Indexed datasource) {
setContainerDataSource(datasource);
initGrid();
}
/**
* Grid initial setup
*/
private void initGrid() {
setSelectionMode(SelectionMode.MULTI);
addSelectionChangeListener(new SelectionChangeListener() {
@Override
public void selectionChange(SelectionChangeEvent event) {
if (applyingSelectionFromClient) {
/*
* Avoid sending changes back to the client if they
* originated from the client. Instead, the RPC handler is
* responsible for keeping track of the resulting selection
* state and notifying the client if it doens't match the
* expectation.
*/
return;
}
/*
* The rows are pinned here to ensure that the client gets the
* correct key from server when the selected row is first
* loaded.
*
* Once the client has gotten info that it is supposed to select
* a row, it will pin the data from the client side as well and
* it will be unpinned once it gets deselected. Nothing on the
* server side should ever unpin anything from KeyMapper.
* Pinning is mostly a client feature and is only used when
* selecting something from the server side.
*/
for (Object addedItemId : event.getAdded()) {
if (!getKeyMapper().isPinned(addedItemId)) {
getKeyMapper().pin(addedItemId);
}
}
getState().selectedKeys = getKeyMapper().getKeys(
getSelectedRows());
}
});
registerRpc(new GridServerRpc() {
@Override
public void selectionChange(List selection) {
Collection receivedSelection = getKeyMapper()
.getItemIds(selection);
final HashSet receivedSelectionSet = new HashSet(
receivedSelection);
final HashSet previousSelectionSet = new HashSet(
getSelectedRows());
applyingSelectionFromClient = true;
try {
SelectionModel selectionModel = getSelectionModel();
SetView removedItemIds = Sets.difference(
previousSelectionSet, receivedSelectionSet);
if (!removedItemIds.isEmpty()) {
if (removedItemIds.size() == 1) {
deselect(removedItemIds.iterator().next());
} else {
assert selectionModel instanceof SelectionModel.Multi : "Got multiple deselections, but the selection model is not a SelectionModel.Multi";
((SelectionModel.Multi) selectionModel)
.deselect(removedItemIds);
}
}
SetView addedItemIds = Sets.difference(
receivedSelectionSet, previousSelectionSet);
if (!addedItemIds.isEmpty()) {
if (addedItemIds.size() == 1) {
select(addedItemIds.iterator().next());
} else {
assert selectionModel instanceof SelectionModel.Multi : "Got multiple selections, but the selection model is not a SelectionModel.Multi";
((SelectionModel.Multi) selectionModel)
.select(addedItemIds);
}
}
} finally {
applyingSelectionFromClient = false;
}
Collection actualSelection = getSelectedRows();
// Make sure all selected rows are pinned
for (Object itemId : actualSelection) {
if (!getKeyMapper().isPinned(itemId)) {
getKeyMapper().pin(itemId);
}
}
// Don't mark as dirty since this might be the expected state
getState(false).selectedKeys = getKeyMapper().getKeys(
actualSelection);
JsonObject diffState = getUI().getConnectorTracker()
.getDiffState(Grid.this);
final String diffstateKey = "selectedKeys";
assert diffState.hasKey(diffstateKey) : "Field name has changed";
if (receivedSelection.equals(actualSelection)) {
/*
* We ended up with the same selection state that the client
* sent us. There's nothing to send back to the client, just
* update the diffstate so subsequent changes will be
* detected.
*/
JsonArray diffSelected = Json.createArray();
for (String rowKey : getState(false).selectedKeys) {
diffSelected.set(diffSelected.length(), rowKey);
}
diffState.put(diffstateKey, diffSelected);
} else {
/*
* Actual selection is not what the client expects. Make
* sure the client gets a state change event by clearing the
* diffstate and marking as dirty
*/
diffState.remove(diffstateKey);
markAsDirty();
}
}
@Override
public void sort(String[] columnIds, SortDirection[] directions,
boolean userOriginated) {
assert columnIds.length == directions.length;
List order = new ArrayList(
columnIds.length);
for (int i = 0; i < columnIds.length; i++) {
Object propertyId = getPropertyIdByColumnId(columnIds[i]);
order.add(new SortOrder(propertyId, directions[i]));
}
setSortOrder(order, userOriginated);
}
@Override
public void selectAll() {
assert getSelectionModel() instanceof SelectionModel.Multi : "Not a multi selection model!";
((SelectionModel.Multi) getSelectionModel()).selectAll();
}
});
registerRpc(new EditorRowServerRpc() {
@Override
public void bind(int rowIndex) {
try {
Object id = getContainerDataSource().getIdByIndex(rowIndex);
doEditItem(id);
getEditorRowRpc().confirmBind();
} catch (Exception e) {
handleError(e);
}
}
@Override
public void cancel(int rowIndex) {
try {
// For future proofing even though cannot currently fail
doCancelEditorRow();
} catch (Exception e) {
handleError(e);
}
}
@Override
public void save(int rowIndex) {
try {
saveEditorRow();
getEditorRowRpc().confirmSave();
} catch (Exception e) {
handleError(e);
}
}
private void handleError(Exception e) {
ErrorHandler handler = getEditorRowErrorHandler();
if (handler == null) {
handler = com.vaadin.server.ErrorEvent
.findErrorHandler(Grid.this);
}
handler.error(new ConnectorErrorEvent(Grid.this, e));
}
});
}
@Override
public void beforeClientResponse(boolean initial) {
try {
header.sanityCheck();
footer.sanityCheck();
} catch (Exception e) {
e.printStackTrace();
setComponentError(new ErrorMessage() {
@Override
public ErrorLevel getErrorLevel() {
return ErrorLevel.CRITICAL;
}
@Override
public String getFormattedHtmlMessage() {
return "Incorrectly merged cells";
}
});
}
super.beforeClientResponse(initial);
}
/**
* 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) {
defaultContainer = false;
internalSetContainerDataSource(container);
}
private void internalSetContainerDataSource(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);
}
columnKeys.removeAll();
datasource = container;
resetEditorRow();
//
// 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 i = sortOrder.iterator();
while (i.hasNext()) {
if (!sortableProps.contains(i.next().getPropertyId())) {
i.remove();
}
}
sort(false);
} 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, columnKeys);
/*
* 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.
*/
setFrozenColumnCount(0);
if (columns.isEmpty()) {
// Add columns
for (Object propertyId : datasource.getContainerPropertyIds()) {
Column column = appendColumn(propertyId);
// Initial sorting is defined by container
if (datasource instanceof Sortable) {
column.setSortable(((Sortable) datasource)
.getSortableContainerPropertyIds().contains(
propertyId));
}
}
} else {
Collection> properties = datasource.getContainerPropertyIds();
for (Object property : columns.keySet()) {
if (!properties.contains(property)) {
throw new IllegalStateException(
"Found at least one column in Grid that does not exist in the given container: "
+ property
+ " with the header \""
+ getColumn(property).getHeaderCaption()
+ "\"");
}
}
}
}
/**
* 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 null
if not found
*/
public Column getColumn(Object propertyId) {
return columns.get(propertyId);
}
/**
* Returns a copy of currently configures columns in their current visual
* order in this Grid.
*
* @return unmodifiable copy of current columns in visual order
*/
public List getColumns() {
List columns = new ArrayList();
for (String columnId : getState(false).columnOrder) {
columns.add(getColumnByColumnId(columnId));
}
return Collections.unmodifiableList(columns);
}
/**
* Adds a new Column to Grid. Also adds the property to container with data
* type String, if property for column does not exist in it. Default value
* for the new property is an empty String.
*
* Note that adding a new property is only done for the default container
* that Grid sets up with the default constructor.
*
* @param propertyId
* the property id of the new column
* @return the new column
*
* @throws IllegalStateException
* if column for given property already exists in this grid
*/
public Column addColumn(Object propertyId) throws IllegalStateException {
if (datasource.getContainerPropertyIds().contains(propertyId)
&& !columns.containsKey(propertyId)) {
appendColumn(propertyId);
} else {
addColumnProperty(propertyId, String.class, "");
}
return getColumn(propertyId);
}
/**
* Adds a new Column to Grid. This function makes sure that the property
* with the given id and data type exists in the container. If property does
* not exists, it will be created.
*
* Default value for the new property is 0 if type is Integer, Double and
* Float. If type is String, default value is an empty string. For all other
* types the default value is null.
*
* Note that adding a new property is only done for the default container
* that Grid sets up with the default constructor.
*
* @param propertyId
* the property id of the new column
* @param type
* the data type for the new property
* @return the new column
*
* @throws IllegalStateException
* if column for given property already exists in this grid or
* property already exists in the container with wrong type
*/
public Column addColumn(Object propertyId, Class> type) {
addColumnProperty(propertyId, type, null);
return getColumn(propertyId);
}
protected void addColumnProperty(Object propertyId, Class> type,
Object defaultValue) throws IllegalStateException {
if (!defaultContainer) {
throw new IllegalStateException(
"Container for this Grid is not a default container from Grid() constructor");
}
if (!columns.containsKey(propertyId)) {
if (!datasource.getContainerPropertyIds().contains(propertyId)) {
datasource.addContainerProperty(propertyId, type, defaultValue);
} else {
Property> containerProperty = datasource
.getContainerProperty(datasource.firstItemId(),
propertyId);
if (containerProperty.getType() == type) {
appendColumn(propertyId);
} else {
throw new IllegalStateException(
"DataSource already has the given property "
+ propertyId + " with a different type");
}
}
} else {
throw new IllegalStateException(
"Grid already has a column for property " + propertyId);
}
}
/**
* Removes all columns from this Grid.
*/
public void removeAllColumns() {
Set properties = new HashSet(columns.keySet());
for (Object propertyId : properties) {
removeColumn(propertyId);
}
}
/**
* Used internally by the {@link Grid} to get a {@link Column} by
* referencing its generated state id. Also used by {@link Column} 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 null
if not found
*/
Column 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 Column 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);
Column column = new Column(this, columnState, datasourcePropertyId);
columns.put(datasourcePropertyId, column);
getState().columns.add(columnState);
getState().columnOrder.add(columnState.id);
header.addColumn(datasourcePropertyId);
footer.addColumn(datasourcePropertyId);
column.setHeaderCaption(SharedUtil.camelCaseToHumanFriendly(String
.valueOf(datasourcePropertyId)));
return column;
}
/**
* Removes a column from Grid based on a property id.
*
* @param propertyId
* The property id of column to be removed
*/
public void removeColumn(Object propertyId) {
header.removeColumn(propertyId);
footer.removeColumn(propertyId);
Column column = columns.remove(propertyId);
getState().columnOrder.remove(columnKeys.key(propertyId));
getState().columns.remove(column.getState());
removeExtension(column.getRenderer());
}
/**
* Sets a new column order for the grid. All columns which are not ordered
* here will remain in the order they were before as the last columns of
* grid.
*
* @param propertyIds
* properties in the order columns should be
*/
public void setColumnOrder(Object... propertyIds) {
List columnOrder = new ArrayList();
for (Object propertyId : propertyIds) {
if (columns.containsKey(propertyId)) {
columnOrder.add(columnKeys.key(propertyId));
} else {
throw new IllegalArgumentException(
"Grid does not contain column for property "
+ String.valueOf(propertyId));
}
}
List stateColumnOrder = getState().columnOrder;
if (stateColumnOrder.size() != columnOrder.size()) {
stateColumnOrder.removeAll(columnOrder);
columnOrder.addAll(stateColumnOrder);
}
getState().columnOrder = columnOrder;
}
/**
* Sets the number of frozen columns in this grid. Setting the count to 0
* means that no data columns will be frozen, but the built-in selection
* checkbox column will still be frozen if it's in use. Setting the count to
* -1 will also disable the selection column.
*
* The default value is 0.
*
* @param numberOfColumns
* the number of columns that should be frozen
*
* @throws IllegalArgumentException
* if the column count is < 0 or > the number of visible columns
*/
public void setFrozenColumnCount(int numberOfColumns) {
if (numberOfColumns < -1 || numberOfColumns > columns.size()) {
throw new IllegalArgumentException(
"count must be between -1 and the current number of columns ("
+ columns + ")");
}
getState().frozenColumnCount = numberOfColumns;
}
/**
* Gets the number of frozen columns in this grid. 0 means that no data
* columns will be frozen, but the built-in selection checkbox column will
* still be frozen if it's in use. -1 means that not even the selection
* column is frozen.
*
* @see #setFrozenColumnCount(int)
*
* @return the number of frozen columns
*/
public int getFrozenColumnCount() {
return getState(false).frozenColumnCount;
}
/**
* 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}.
*
* 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 null
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}
*
* Note: 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.
*
* If {@link HeightMode#CSS} is given, Grid will respect the values given
* via a {@code setHeight}-method, and behave as a traditional Component.
*
* If {@link HeightMode#ROW} is given, Grid will make sure that the body
* will display as many rows as {@link #getHeightByRows()} defines.
* Note: 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.
*
* 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.
*
* The SelectionModel that is previously in use will have all its items
* deselected.
*
* 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 null
*/
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.
*
* Grid supports three selection modes: multiselect, single select and no
* selection, and this is a conveniency method for choosing between one of
* them.
*
* 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.
*
* Essentially, the two following method calls are equivalent:
*
*
* grid.setSelectionMode(SelectionMode.MULTI);
* grid.setSelectionModel(new MultiSelectionMode());
*
*
*
* @param selectionMode
* the selection mode to switch to
* @return The {@link SelectionModel} instance that was taken into use
* @throws IllegalArgumentException
* if {@code selectionMode} is null
* @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 true
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.
*
* 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 getSelectedRows() {
return getSelectionModel().getSelectedRows();
}
/**
* Gets the item id of the currently selected item.
*
* 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 null
* 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.
*
* 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 true
if the selection state changed.
* false
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.
*
* 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 true
if the selection state changed.
* false
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) {
if (isSelected(itemId)) {
return ((SelectionModel.Single) selectionModel).select(null);
}
return false;
} 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.
*
* Note: 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 oldSelection,
Collection 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 order) {
setSortOrder(order, false);
}
private void setSortOrder(List order, boolean userOriginated) {
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(false);
}
/**
* Get the current sort order list.
*
* @return a sort order list
*/
public List getSortOrder() {
return Collections.unmodifiableList(sortOrder);
}
/**
* Apply sorting to data source.
*/
private void sort(boolean userOriginated) {
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), userOriginated));
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
*/
@Override
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
*/
@Override
public void removeSortOrderChangeListener(SortOrderChangeListener listener) {
removeListener(SortOrderChangeEvent.class, listener,
SORT_ORDER_CHANGE_METHOD);
}
/* Grid Headers */
/**
* Returns the header section of this grid. The default header contains a
* single row displaying the column captions.
*
* @return the header
*/
protected Header getHeader() {
return header;
}
/**
* Gets the header row at given index.
*
* @param rowIndex
* 0 based index for row. Counted from top to bottom
* @return header row at given index
* @throws IllegalArgumentException
* if no row exists at given index
*/
public HeaderRow getHeaderRow(int rowIndex) {
return header.getRow(rowIndex);
}
/**
* Inserts a new row at the given position to the header section. Shifts the
* row currently at that position and any subsequent rows down (adds one to
* their indices).
*
* @param index
* the position at which to insert the row
* @return the new row
*
* @throws IllegalArgumentException
* if the index is less than 0 or greater than row count
* @see #appendHeaderRow()
* @see #prependHeaderRow()
* @see #removeHeaderRow(HeaderRow)
* @see #removeHeaderRow(int)
*/
public HeaderRow addHeaderRowAt(int index) {
return header.addRowAt(index);
}
/**
* Adds a new row at the bottom of the header section.
*
* @return the new row
* @see #prependHeaderRow()
* @see #addHeaderRowAt(int)
* @see #removeHeaderRow(HeaderRow)
* @see #removeHeaderRow(int)
*/
public HeaderRow appendHeaderRow() {
return header.appendRow();
}
/**
* Returns the current default row of the header section. The default row is
* a special header row providing a user interface for sorting columns.
* Setting a header text for column updates cells in the default header.
*
* @return the default row or null if no default row set
*/
public HeaderRow getDefaultHeaderRow() {
return header.getDefaultRow();
}
/**
* Gets the row count for the header section.
*
* @return row count
*/
public int getHeaderRowCount() {
return header.getRowCount();
}
/**
* Adds a new row at the top of the header section.
*
* @return the new row
* @see #appendHeaderRow()
* @see #addHeaderRowAt(int)
* @see #removeHeaderRow(HeaderRow)
* @see #removeHeaderRow(int)
*/
public HeaderRow prependHeaderRow() {
return header.prependRow();
}
/**
* Removes the given row from the header section.
*
* @param row
* the row to be removed
*
* @throws IllegalArgumentException
* if the row does not exist in this section
* @see #removeHeaderRow(int)
* @see #addHeaderRowAt(int)
* @see #appendHeaderRow()
* @see #prependHeaderRow()
*/
public void removeHeaderRow(HeaderRow row) {
header.removeRow(row);
}
/**
* Removes the row at the given position from the header section.
*
* @param index
* the position of the row
*
* @throws IllegalArgumentException
* if no row exists at given index
* @see #removeHeaderRow(HeaderRow)
* @see #addHeaderRowAt(int)
* @see #appendHeaderRow()
* @see #prependHeaderRow()
*/
public void removeHeaderRow(int rowIndex) {
header.removeRow(rowIndex);
}
/**
* Sets the default row of the 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
* header does not contain the row
*/
public void setDefaultHeaderRow(HeaderRow row) {
header.setDefaultRow(row);
}
/**
* Sets the visibility of the header section.
*
* @param visible
* true to show header section, false to hide
*/
public void setHeaderVisible(boolean visible) {
header.setVisible(visible);
}
/**
* Returns the visibility of the header section.
*
* @return true if visible, false otherwise.
*/
public boolean isHeaderVisible() {
return header.isVisible();
}
/* Grid Footers */
/**
* Returns the footer section of this grid. The default header contains a
* single row displaying the column captions.
*
* @return the footer
*/
protected Footer getFooter() {
return footer;
}
/**
* Gets the footer row at given index.
*
* @param rowIndex
* 0 based index for row. Counted from top to bottom
* @return footer row at given index
* @throws IllegalArgumentException
* if no row exists at given index
*/
public FooterRow getFooterRow(int rowIndex) {
return footer.getRow(rowIndex);
}
/**
* Inserts a new row at the given position to the footer section. Shifts the
* row currently at that position and any subsequent rows down (adds one to
* their indices).
*
* @param index
* the position at which to insert the row
* @return the new row
*
* @throws IllegalArgumentException
* if the index is less than 0 or greater than row count
* @see #appendFooterRow()
* @see #prependFooterRow()
* @see #removeFooterRow(FooterRow)
* @see #removeFooterRow(int)
*/
public FooterRow addFooterRowAt(int index) {
return footer.addRowAt(index);
}
/**
* Adds a new row at the bottom of the footer section.
*
* @return the new row
* @see #prependFooterRow()
* @see #addFooterRowAt(int)
* @see #removeFooterRow(FooterRow)
* @see #removeFooterRow(int)
*/
public FooterRow appendFooterRow() {
return footer.appendRow();
}
/**
* Gets the row count for the footer.
*
* @return row count
*/
public int getFooterRowCount() {
return footer.getRowCount();
}
/**
* Adds a new row at the top of the footer section.
*
* @return the new row
* @see #appendFooterRow()
* @see #addFooterRowAt(int)
* @see #removeFooterRow(FooterRow)
* @see #removeFooterRow(int)
*/
public FooterRow prependFooterRow() {
return footer.prependRow();
}
/**
* Removes the given row from the footer section.
*
* @param row
* the row to be removed
*
* @throws IllegalArgumentException
* if the row does not exist in this section
* @see #removeFooterRow(int)
* @see #addFooterRowAt(int)
* @see #appendFooterRow()
* @see #prependFooterRow()
*/
public void removeFooterRow(FooterRow row) {
footer.removeRow(row);
}
/**
* Removes the row at the given position from the footer section.
*
* @param index
* the position of the row
*
* @throws IllegalArgumentException
* if no row exists at given index
* @see #removeFooterRow(FooterRow)
* @see #addFooterRowAt(int)
* @see #appendFooterRow()
* @see #prependFooterRow()
*/
public void removeFooterRow(int rowIndex) {
footer.removeRow(rowIndex);
}
/**
* Sets the visibility of the footer section.
*
* @param visible
* true to show footer section, false to hide
*/
public void setFooterVisible(boolean visible) {
footer.setVisible(visible);
}
/**
* Returns the visibility of the footer section.
*
* @return true if visible, false otherwise.
*/
public boolean isFooterVisible() {
return footer.isVisible();
}
@Override
public Iterator iterator() {
List componentList = new ArrayList();
Header header = getHeader();
for (int i = 0; i < header.getRowCount(); ++i) {
HeaderRow row = header.getRow(i);
for (Object propId : columns.keySet()) {
HeaderCell cell = row.getCell(propId);
if (cell.getCellState().type == GridStaticCellType.WIDGET) {
componentList.add(cell.getComponent());
}
}
}
Footer footer = getFooter();
for (int i = 0; i < footer.getRowCount(); ++i) {
FooterRow row = footer.getRow(i);
for (Object propId : columns.keySet()) {
FooterCell cell = row.getCell(propId);
if (cell.getCellState().type == GridStaticCellType.WIDGET) {
componentList.add(cell.getComponent());
}
}
}
componentList.addAll(getEditorRowFields());
return componentList.iterator();
}
@Override
public boolean isRendered(Component childComponent) {
if (getEditorRowFields().contains(childComponent)) {
// Only render editor row fields if the editor is open
return isEditorRowActive();
} else {
// TODO Header and footer components should also only be rendered if
// the header/footer is visible
return true;
}
}
EditorRowClientRpc getEditorRowRpc() {
return getRpcProxy(EditorRowClientRpc.class);
}
/**
* Sets the cell style generator that is used for generating styles for rows
* and cells.
*
* @param cellStyleGenerator
* the cell style generator to set, or null
to
* remove a previously set generator
*/
public void setCellStyleGenerator(CellStyleGenerator cellStyleGenerator) {
this.cellStyleGenerator = cellStyleGenerator;
getState().hasCellStyleGenerator = (cellStyleGenerator != null);
datasourceExtension.refreshCache();
}
/**
* Gets the cell style generator that is used for generating styles for rows
* and cells.
*
* @return the cell style generator, or null
if no generator is
* set
*/
public CellStyleGenerator getCellStyleGenerator() {
return cellStyleGenerator;
}
/**
* Adds a row to the underlying container. The order of the parameters
* should match the current visible column order.
*
* Please note that it's generally only safe to use this method during
* initialization. After Grid has been initialized and the visible column
* order might have been changed, it's better to instead add items directly
* to the underlying container and use {@link Item#getItemProperty(Object)}
* to make sure each value is assigned to the intended property.
*
* @param values
* the cell values of the new row, in the same order as the
* visible column order, not null
.
* @return the item id of the new row
* @throws IllegalArgumentException
* if values is null
* @throws IllegalArgumentException
* if its length does not match the number of visible columns
* @throws IllegalArgumentException
* if a parameter value is not an instance of the corresponding
* property type
* @throws UnsupportedOperationException
* if the container does not support adding new items
*/
public Object addRow(Object... values) {
if (values == null) {
throw new IllegalArgumentException("Values cannot be null");
}
Indexed dataSource = getContainerDataSource();
List columnOrder = getState(false).columnOrder;
if (values.length != columnOrder.size()) {
throw new IllegalArgumentException("There are "
+ columnOrder.size() + " visible columns, but "
+ values.length + " cell values were provided.");
}
// First verify all parameter types
for (int i = 0; i < columnOrder.size(); i++) {
Object propertyId = getPropertyIdByColumnId(columnOrder.get(i));
Class> propertyType = dataSource.getType(propertyId);
if (values[i] != null && !propertyType.isInstance(values[i])) {
throw new IllegalArgumentException("Parameter " + i + "("
+ values[i] + ") is not an instance of "
+ propertyType.getCanonicalName());
}
}
Object itemId = dataSource.addItem();
try {
Item item = dataSource.getItem(itemId);
for (int i = 0; i < columnOrder.size(); i++) {
Object propertyId = getPropertyIdByColumnId(columnOrder.get(i));
Property property = item.getItemProperty(propertyId);
property.setValue(values[i]);
}
} catch (RuntimeException e) {
try {
dataSource.removeItem(itemId);
} catch (Exception e2) {
getLogger().log(Level.SEVERE,
"Error recovering from exception in addRow", e);
}
throw e;
}
return itemId;
}
private static Logger getLogger() {
return Logger.getLogger(Grid.class.getName());
}
/**
* Sets whether or not the editor row feature is enabled for this grid.
*
* @param isEnabled
* true
to enable the feature, false
* otherwise
* @throws IllegalStateException
* if an item is currently being edited
* @see #getEditedItemId()
*/
public void setEditorRowEnabled(boolean isEnabled)
throws IllegalStateException {
if (isEditorRowActive()) {
throw new IllegalStateException(
"Cannot disable the editor row while an item ("
+ getEditedItemId() + ") is being edited.");
}
if (isEditorRowEnabled() != isEnabled) {
getState().editorRowEnabled = isEnabled;
}
}
/**
* Checks whether the editor row feature is enabled for this grid.
*
* @return true
iff the editor row feature is enabled for this
* grid
* @see #getEditedItemId()
*/
public boolean isEditorRowEnabled() {
return getState(false).editorRowEnabled;
}
/**
* Gets the id of the item that is currently being edited.
*
* @return the id of the item that is currently being edited, or
* null
if no item is being edited at the moment
*/
public Object getEditedItemId() {
return editedItemId;
}
/**
* Gets the field group that is backing the editor row of this grid.
*
* @return the backing field group
*/
public FieldGroup getEditorRowFieldGroup() {
return editorRowFieldGroup;
}
/**
* Sets the field group that is backing this editor row.
*
* @param fieldGroup
* the backing field group
*/
public void setEditorRowFieldGroup(FieldGroup fieldGroup) {
editorRowFieldGroup = fieldGroup;
if (isEditorRowActive()) {
editorRowFieldGroup.setItemDataSource(getContainerDataSource()
.getItem(editedItemId));
}
}
/**
* Returns whether an item is currently being edited in the editor row.
*
* @return true iff the editor row is editing an item
*/
public boolean isEditorRowActive() {
return editedItemId != null;
}
/**
* Sets a property editable or not.
*
* In order for a user to edit a particular value with a Field, it needs to
* be both non-readonly and editable.
*
* The difference between read-only and uneditable is that the read-only
* state is propagated back into the property, while the editable property
* is internal metadata for the editor row.
*
* @param propertyId
* the id of the property to set as editable state
* @param editable
* whether or not {@code propertyId} chould be editable
*/
public void setPropertyEditable(Object propertyId, boolean editable) {
checkPropertyExists(propertyId);
if (getEditorRowField(propertyId) != null) {
getEditorRowField(propertyId).setReadOnly(!editable);
}
if (editable) {
uneditableProperties.remove(propertyId);
} else {
uneditableProperties.add(propertyId);
}
}
/**
* Checks whether a property is editable through the editor row.
*
* This only checks whether the property is configured as uneditable in the
* editor row. The property's or field's readonly status will ultimately
* decide whether the value can be edited or not.
*
* @param propertyId
* the id of the property to check for editable status
* @return true
iff the property is editable according to this
* editor row
*/
public boolean isPropertyEditable(Object propertyId) {
checkPropertyExists(propertyId);
return !uneditableProperties.contains(propertyId);
}
private void checkPropertyExists(Object propertyId) {
if (!getContainerDataSource().getContainerPropertyIds().contains(
propertyId)) {
throw new IllegalArgumentException("Property with id " + propertyId
+ " is not in the current Container");
}
}
/**
* Gets the field component that represents a property in the editor row. If
* the property is not yet bound to a field, null is returned.
*
* When {@link #editItem(Object) editItem} is called, fields are
* automatically created and bound for any unbound properties.
*
* @param propertyId
* the property id of the property for which to find the field
* @return the bound field or null if not bound
*/
public Field> getEditorRowField(Object propertyId) {
return editorRowFieldGroup.getField(propertyId);
}
/**
* Opens the editor row for the provided item.
*
* @param itemId
* the id of the item to edit
* @throws IllegalStateException
* if the editor row is not enabled
* @throws IllegalArgumentException
* if the {@code itemId} is not in the backing container
* @see #setEditorRowEnabled(boolean)
*/
public void editItem(Object itemId) throws IllegalStateException,
IllegalArgumentException {
doEditItem(itemId);
getEditorRowRpc().bind(getContainerDataSource().indexOfId(itemId));
}
protected void doEditItem(Object itemId) {
if (!isEditorRowEnabled()) {
throw new IllegalStateException("Editor row is not enabled");
}
Item item = getContainerDataSource().getItem(itemId);
if (item == null) {
throw new IllegalArgumentException("Item with id " + itemId
+ " not found in current container");
}
editorRowFieldGroup.setItemDataSource(item);
editedItemId = itemId;
for (Object propertyId : item.getItemPropertyIds()) {
final Field> editor;
if (editorRowFieldGroup.getUnboundPropertyIds()
.contains(propertyId)) {
editor = editorRowFieldGroup.buildAndBind(propertyId);
} else {
editor = editorRowFieldGroup.getField(propertyId);
}
getColumn(propertyId).getState().editorConnector = editor;
if (editor != null) {
editor.setReadOnly(!isPropertyEditable(propertyId));
if (editor.getParent() != Grid.this) {
assert editor.getParent() == null;
editor.setParent(Grid.this);
}
}
}
}
/**
* Binds the field with the given propertyId from the current item. If an
* item has not been set then the binding is postponed until the item is set
* using {@link #editItem(Object)}.
*
* This method also adds validators when applicable.
*
* Note: This is a pass-through call to the backing field group.
*
* @param field
* The field to bind
* @param propertyId
* The propertyId to bind to the field
* @throws BindException
* If the property id is already bound to another field by this
* field binder
*/
public void bindEditorRowField(Object propertyId, Field> field)
throws BindException {
editorRowFieldGroup.bind(field, propertyId);
}
/**
* Saves all changes done to the bound fields.
*
* Note: This is a pass-through call to the backing field group.
*
* @throws CommitException
* If the commit was aborted
*
* @see FieldGroup#commit()
*/
public void saveEditorRow() throws CommitException {
editorRowFieldGroup.commit();
}
/**
* Cancels the currently active edit if any.
*/
public void cancelEditorRow() {
if (isEditorRowActive()) {
getEditorRowRpc().cancel(
getContainerDataSource().indexOfId(editedItemId));
doCancelEditorRow();
}
}
protected void doCancelEditorRow() {
editedItemId = null;
}
void resetEditorRow() {
if (isEditorRowActive()) {
/*
* Simply force cancel the editing; throwing here would just make
* Grid.setContainerDataSource semantics more complicated.
*/
cancelEditorRow();
}
for (Field> editor : getEditorRowFields()) {
editor.setParent(null);
}
editedItemId = null;
editorRowFieldGroup = new FieldGroup();
uneditableProperties = new HashSet();
}
/**
* Gets a collection of all fields bound to the editor row of this grid.
*
* All non-editable fields (either readonly or uneditable) are in read-only
* mode.
*
* When {@link #editItem(Object) editItem} is called, fields are
* automatically created and bound to any unbound properties.
*
* @return a collection of all the fields bound to this editor row
*/
Collection> getEditorRowFields() {
return editorRowFieldGroup.getFields();
}
/**
* Sets the field factory for the {@link FieldGroup}. The field factory is
* only used when {@link FieldGroup} creates a new field.
*
* Note: This is a pass-through call to the backing field group.
*
* @param fieldFactory
* The field factory to use
*/
public void setEditorRowFieldFactory(FieldGroupFieldFactory fieldFactory) {
editorRowFieldGroup.setFieldFactory(fieldFactory);
}
/**
* Returns the error handler of this editor row.
*
* @return the error handler or null if there is no dedicated error handler
*
* @see #setEditorRowErrorHandler(ErrorHandler)
* @see ClientConnector#getErrorHandler()
*/
public ErrorHandler getEditorRowErrorHandler() {
return editorRowErrorHandler;
}
/**
* Sets the error handler for this editor row. The error handler is invoked
* for exceptions thrown while processing client requests; specifically when
* {@link #saveEditorRow()} triggered by the client throws a
* CommitException. If the error handler is not set, one is looked up via
* Grid.
*
* @param errorHandler
* the error handler to use
*
* @see ClientConnector#setErrorHandler(ErrorHandler)
* @see ErrorEvent#findErrorHandler(ClientConnector)
*/
public void setEditorRowErrorHandler(ErrorHandler errorHandler) {
editorRowErrorHandler = errorHandler;
}
/**
* Builds a field using the given caption and binds it to the given property
* id using the field binder. Ensures the new field is of the given type.
*
* Note: This is a pass-through call to the backing field group.
*
* @param propertyId
* The property id to bind to. Must be present in the field
* finder
* @param fieldType
* The type of field that we want to create
* @throws BindException
* If the field could not be created
* @return The created and bound field. Can be any type of {@link Field} .
*/
public > T buildAndBind(Object propertyId,
Class fieldType) throws BindException {
return editorRowFieldGroup.buildAndBind(null, propertyId, fieldType);
}
}