/*
* Copyright 2000-2016 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.v7.ui;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
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.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jsoup.nodes.Attributes;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import com.vaadin.event.ContextClickEvent;
import com.vaadin.event.FieldEvents.BlurEvent;
import com.vaadin.event.FieldEvents.BlurListener;
import com.vaadin.event.FieldEvents.FocusEvent;
import com.vaadin.event.FieldEvents.FocusListener;
import com.vaadin.server.AbstractClientConnector;
import com.vaadin.server.AbstractExtension;
import com.vaadin.server.EncodeResult;
import com.vaadin.server.ErrorMessage;
import com.vaadin.server.Extension;
import com.vaadin.server.JsonCodec;
import com.vaadin.server.KeyMapper;
import com.vaadin.server.VaadinSession;
import com.vaadin.shared.MouseEventDetails;
import com.vaadin.shared.Registration;
import com.vaadin.shared.data.sort.SortDirection;
import com.vaadin.shared.util.SharedUtil;
import com.vaadin.ui.AbstractComponent;
import com.vaadin.ui.Component;
import com.vaadin.ui.Component.Focusable;
import com.vaadin.ui.ConnectorTracker;
import com.vaadin.ui.SelectiveRenderer;
import com.vaadin.ui.UI;
import com.vaadin.ui.declarative.DesignAttributeHandler;
import com.vaadin.ui.declarative.DesignContext;
import com.vaadin.ui.declarative.DesignException;
import com.vaadin.ui.declarative.DesignFormatter;
import com.vaadin.util.ReflectTools;
import com.vaadin.v7.data.Container;
import com.vaadin.v7.data.Container.Indexed;
import com.vaadin.v7.data.Container.ItemSetChangeEvent;
import com.vaadin.v7.data.Container.ItemSetChangeListener;
import com.vaadin.v7.data.Container.ItemSetChangeNotifier;
import com.vaadin.v7.data.Container.PropertySetChangeEvent;
import com.vaadin.v7.data.Container.PropertySetChangeListener;
import com.vaadin.v7.data.Container.PropertySetChangeNotifier;
import com.vaadin.v7.data.Container.Sortable;
import com.vaadin.v7.data.Item;
import com.vaadin.v7.data.Property;
import com.vaadin.v7.data.Validator.InvalidValueException;
import com.vaadin.v7.data.fieldgroup.DefaultFieldGroupFieldFactory;
import com.vaadin.v7.data.fieldgroup.FieldGroup;
import com.vaadin.v7.data.fieldgroup.FieldGroup.CommitException;
import com.vaadin.v7.data.fieldgroup.FieldGroupFieldFactory;
import com.vaadin.v7.data.sort.Sort;
import com.vaadin.v7.data.sort.SortOrder;
import com.vaadin.v7.data.util.IndexedContainer;
import com.vaadin.v7.data.util.converter.Converter;
import com.vaadin.v7.data.util.converter.ConverterUtil;
import com.vaadin.v7.event.FieldEvents.BlurNotifier;
import com.vaadin.v7.event.FieldEvents.FocusNotifier;
import com.vaadin.v7.event.ItemClickEvent;
import com.vaadin.v7.event.ItemClickEvent.ItemClickListener;
import com.vaadin.v7.event.ItemClickEvent.ItemClickNotifier;
import com.vaadin.v7.event.SelectionEvent;
import com.vaadin.v7.event.SelectionEvent.SelectionListener;
import com.vaadin.v7.event.SelectionEvent.SelectionNotifier;
import com.vaadin.v7.event.SortEvent;
import com.vaadin.v7.event.SortEvent.SortListener;
import com.vaadin.v7.event.SortEvent.SortNotifier;
import com.vaadin.v7.server.communication.data.DataGenerator;
import com.vaadin.v7.server.communication.data.RpcDataProviderExtension;
import com.vaadin.v7.shared.ui.grid.ColumnResizeMode;
import com.vaadin.v7.shared.ui.grid.EditorClientRpc;
import com.vaadin.v7.shared.ui.grid.EditorServerRpc;
import com.vaadin.v7.shared.ui.grid.GridClientRpc;
import com.vaadin.v7.shared.ui.grid.GridColumnState;
import com.vaadin.v7.shared.ui.grid.GridConstants;
import com.vaadin.v7.shared.ui.grid.GridConstants.Section;
import com.vaadin.v7.shared.ui.grid.GridServerRpc;
import com.vaadin.v7.shared.ui.grid.GridState;
import com.vaadin.v7.shared.ui.grid.GridStaticCellType;
import com.vaadin.v7.shared.ui.grid.GridStaticSectionState;
import com.vaadin.v7.shared.ui.grid.GridStaticSectionState.CellState;
import com.vaadin.v7.shared.ui.grid.GridStaticSectionState.RowState;
import com.vaadin.v7.shared.ui.grid.HeightMode;
import com.vaadin.v7.shared.ui.grid.ScrollDestination;
import com.vaadin.v7.shared.ui.grid.selection.MultiSelectionModelServerRpc;
import com.vaadin.v7.shared.ui.grid.selection.MultiSelectionModelState;
import com.vaadin.v7.shared.ui.grid.selection.SingleSelectionModelServerRpc;
import com.vaadin.v7.shared.ui.grid.selection.SingleSelectionModelState;
import com.vaadin.v7.ui.Grid.SelectionModel.HasUserSelectionAllowed;
import com.vaadin.v7.ui.renderers.HtmlRenderer;
import com.vaadin.v7.ui.renderers.Renderer;
import com.vaadin.v7.ui.renderers.TextRenderer;
import elemental.json.Json;
import elemental.json.JsonObject;
import elemental.json.JsonValue;
/**
* A grid component for displaying tabular data.
* <p>
* Grid is always bound to a {@link Container.Indexed}, but is not a
* {@code Container} of any kind in of itself. The contents of the given
* Container is displayed with the help of {@link Renderer Renderers}.
*
* <h3 id="grid-headers-and-footers">Headers and Footers</h3>
* <p>
*
*
* <h3 id="grid-converters-and-renderers">Converters and Renderers</h3>
* <p>
* Each column has its own {@link Renderer} that displays data into something
* that can be displayed in the browser. That data is first converted with a
* {@link Converter} into something that the Renderer can process. This can also
* be an implicit step - if a column has a simple data type, like a String, no
* explicit assignment is needed.
* <p>
* Usually a renderer takes some kind of object, and converts it into a
* HTML-formatted string.
* <p>
* <code><pre>
* Grid grid = new Grid(myContainer);
* Column column = grid.getColumn(STRING_DATE_PROPERTY);
* column.setConverter(new StringToDateConverter());
* column.setRenderer(new MyColorfulDateRenderer());
* </pre></code>
*
* <h3 id="grid-lazyloading">Lazy Loading</h3>
* <p>
* The data is accessed as it is needed by Grid and not any sooner. In other
* words, if the given Container is huge, but only the first few rows are
* displayed to the user, only those (and a few more, for caching purposes) are
* accessed.
*
* <h3 id="grid-selection-modes-and-models">Selection Modes and Models</h3>
* <p>
* Grid supports three selection <em>{@link SelectionMode modes}</em> (single,
* multi, none), and comes bundled with one <em>{@link SelectionModel
* model}</em> for each of the modes. The distinction between a selection mode
* and selection model is as follows: a <em>mode</em> essentially says whether
* you can have one, many or no rows selected. The model, however, has the
* behavioral details of each. A single selection model may require that the
* user deselects one row before selecting another one. A variant of a
* multiselect might have a configurable maximum of rows that may be selected.
* And so on.
* <p>
* <code><pre>
* Grid grid = new Grid(myContainer);
*
* // uses the bundled SingleSelectionModel class
* grid.setSelectionMode(SelectionMode.SINGLE);
*
* // changes the behavior to a custom selection model
* grid.setSelectionModel(new MyTwoSelectionModel());
* </pre></code>
*
* @since 7.4
* @author Vaadin Ltd
*
* @deprecated As of 8.0 replaced by {@link com.vaadin.ui.Grid} based on the new
* data binding API
*/
@Deprecated
public class Grid extends AbstractComponent
implements SelectionNotifier, SortNotifier, SelectiveRenderer,
ItemClickNotifier, Focusable, FocusNotifier, BlurNotifier {
/**
* An event listener for column visibility change events in the Grid.
*
* @since 7.5.0
*/
@Deprecated
public interface ColumnVisibilityChangeListener extends Serializable {
/**
* Called when a column has become hidden or unhidden.
*
* @param event
*/
void columnVisibilityChanged(ColumnVisibilityChangeEvent event);
}
/**
* An event that is fired when a column's visibility changes.
*
* @since 7.5.0
*/
@Deprecated
public static class ColumnVisibilityChangeEvent extends Component.Event {
private final Column column;
private final boolean userOriginated;
private final boolean hidden;
/**
* Constructor for a column visibility change event.
*
* @param source
* the grid from which this event originates
* @param column
* the column that changed its visibility
* @param hidden
* <code>true</code> if the column was hidden,
* <code>false</code> if it became visible
* @param isUserOriginated
* <code>true</code> iff the event was triggered by an UI
* interaction
*/
public ColumnVisibilityChangeEvent(Grid source, Column column,
boolean hidden, boolean isUserOriginated) {
super(source);
this.column = column;
this.hidden = hidden;
userOriginated = isUserOriginated;
}
/**
* Gets the column that became hidden or visible.
*
* @return the column that became hidden or visible.
* @see Column#isHidden()
*/
public Column getColumn() {
return column;
}
/**
* Was the column set hidden or visible.
*
* @return <code>true</code> if the column was hidden <code>false</code>
* if it was set visible
*/
public boolean isHidden() {
return hidden;
}
/**
* Returns <code>true</code> if the column reorder was done by the user,
* <code>false</code> if not and it was triggered by server side code.
*
* @return <code>true</code> if event is a result of user interaction
*/
public boolean isUserOriginated() {
return userOriginated;
}
}
/**
* A callback interface for generating details for a particular row in Grid.
*
* @since 7.5.0
* @author Vaadin Ltd
* @see DetailsGenerator#NULL
*/
@Deprecated
public interface DetailsGenerator extends Serializable {
/** A details generator that provides no details */
public DetailsGenerator NULL = new DetailsGenerator() {
@Override
public Component getDetails(RowReference rowReference) {
return null;
}
};
/**
* This method is called for whenever a details row needs to be shown on
* the client. Grid removes all of its references to details components
* when they are no longer displayed on the client-side and will
* re-request once needed again.
* <p>
* <em>Note:</em> If a component gets generated, it may not be manually
* attached anywhere. The same details component can not be displayed
* for multiple different rows.
*
* @param rowReference
* the reference for the row for which to generate details
* @return the details for the given row, or <code>null</code> to leave
* the details empty.
*/
Component getDetails(RowReference rowReference);
}
/**
* A class that manages details components by calling
* {@link DetailsGenerator} as needed. Details components are attached by
* this class when the {@link RpcDataProviderExtension} is sending data to
* the client. Details components are detached and forgotten when client
* informs that it has dropped the corresponding item.
*
* @since 7.6.1
*/
@Deprecated
public final static class DetailComponentManager
extends AbstractGridExtension implements DataGenerator {
/**
* The user-defined details generator.
*
* @see #setDetailsGenerator(DetailsGenerator)
*/
private DetailsGenerator detailsGenerator;
/**
* This map represents all details that are currently visible on the
* client. Details components get destroyed once they scroll out of
* view.
*/
private final Map<Object, Component> itemIdToDetailsComponent = new HashMap<Object, Component>();
/**
* Set of item ids that got <code>null</code> from DetailsGenerator when
* {@link DetailsGenerator#getDetails(RowReference)} was called.
*/
private final Set<Object> emptyDetails = new HashSet<Object>();
/**
* Set of item IDs for all open details rows. Contains even the ones
* that are not currently visible on the client.
*/
private final Set<Object> openDetails = new HashSet<Object>();
public DetailComponentManager(Grid grid) {
this(grid, DetailsGenerator.NULL);
}
public DetailComponentManager(Grid grid,
DetailsGenerator detailsGenerator) {
super(grid);
setDetailsGenerator(detailsGenerator);
}
/**
* Creates a details component with the help of the user-defined
* {@link DetailsGenerator}.
* <p>
* This method attaches created components to the parent {@link Grid}.
*
* @param itemId
* the item id for which to create the details component.
* @throws IllegalStateException
* if the current details generator provides a component
* that was manually attached.
*/
private void createDetails(Object itemId) throws IllegalStateException {
assert itemId != null : "itemId was null";
if (itemIdToDetailsComponent.containsKey(itemId)
|| emptyDetails.contains(itemId)) {
// Don't overwrite existing components
return;
}
RowReference rowReference = new RowReference(getParentGrid());
rowReference.set(itemId);
DetailsGenerator detailsGenerator = getParentGrid()
.getDetailsGenerator();
Component details = detailsGenerator.getDetails(rowReference);
if (details != null) {
if (details.getParent() != null) {
String name = detailsGenerator.getClass().getName();
throw new IllegalStateException(
name + " generated a details component that already "
+ "was attached. (itemId: " + itemId
+ ", component: " + details + ")");
}
itemIdToDetailsComponent.put(itemId, details);
addComponentToGrid(details);
assert !emptyDetails.contains(itemId) : "Bookeeping thinks "
+ "itemId is empty even though we just created a "
+ "component for it (" + itemId + ")";
} else {
emptyDetails.add(itemId);
}
}
/**
* Destroys a details component correctly.
* <p>
* This method will detach the component from parent {@link Grid}.
*
* @param itemId
* the item id for which to destroy the details component
*/
private void destroyDetails(Object itemId) {
emptyDetails.remove(itemId);
Component removedComponent = itemIdToDetailsComponent
.remove(itemId);
if (removedComponent == null) {
return;
}
removeComponentFromGrid(removedComponent);
}
/**
* Recreates all visible details components.
*/
public void refreshDetails() {
Set<Object> visibleItemIds = new HashSet<Object>(
itemIdToDetailsComponent.keySet());
for (Object itemId : visibleItemIds) {
destroyDetails(itemId);
createDetails(itemId);
refreshRow(itemId);
}
}
/**
* Sets details visiblity status of given item id.
*
* @param itemId
* item id to set
* @param visible
* <code>true</code> if visible; <code>false</code> if not
*/
public void setDetailsVisible(Object itemId, boolean visible) {
if ((visible && openDetails.contains(itemId))
|| (!visible && !openDetails.contains(itemId))) {
return;
}
if (visible) {
openDetails.add(itemId);
refreshRow(itemId);
} else {
openDetails.remove(itemId);
destroyDetails(itemId);
refreshRow(itemId);
}
}
@Override
public void generateData(Object itemId, Item item, JsonObject rowData) {
// DetailComponentManager should not send anything if details
// generator is the default null version.
if (openDetails.contains(itemId)
&& !detailsGenerator.equals(DetailsGenerator.NULL)) {
// Double check to be sure details component exists.
createDetails(itemId);
Component detailsComponent = itemIdToDetailsComponent
.get(itemId);
rowData.put(GridState.JSONKEY_DETAILS_VISIBLE,
(detailsComponent != null
? detailsComponent.getConnectorId() : ""));
}
}
@Override
public void destroyData(Object itemId) {
if (openDetails.contains(itemId)) {
destroyDetails(itemId);
}
}
/**
* Sets a new details generator for row details.
* <p>
* The currently opened row details will be re-rendered.
*
* @param detailsGenerator
* the details generator to set
* @throws IllegalArgumentException
* if detailsGenerator is <code>null</code>;
*/
public void setDetailsGenerator(DetailsGenerator detailsGenerator)
throws IllegalArgumentException {
if (detailsGenerator == null) {
throw new IllegalArgumentException(
"Details generator may not be null");
} else if (detailsGenerator == this.detailsGenerator) {
return;
}
this.detailsGenerator = detailsGenerator;
refreshDetails();
}
/**
* Gets the current details generator for row details.
*
* @return the detailsGenerator the current details generator
*/
public DetailsGenerator getDetailsGenerator() {
return detailsGenerator;
}
/**
* Checks whether details are visible for the given item.
*
* @param itemId
* the id of the item for which to check details visibility
* @return <code>true</code> iff the details are visible
*/
public boolean isDetailsVisible(Object itemId) {
return openDetails.contains(itemId);
}
}
/**
* Custom field group that allows finding property types before an item has
* been bound.
*/
private final class CustomFieldGroup extends FieldGroup {
public CustomFieldGroup() {
setFieldFactory(EditorFieldFactory.get());
}
@Override
protected Class<?> getPropertyType(Object propertyId)
throws BindException {
if (getItemDataSource() == null) {
return datasource.getType(propertyId);
} else {
return super.getPropertyType(propertyId);
}
}
@Override
protected <T extends Field> T build(String caption, Class<?> dataType,
Class<T> fieldType) throws BindException {
T field = super.build(caption, dataType, fieldType);
if (field instanceof CheckBox) {
field.setCaption(null);
}
return field;
}
@Override
protected void bindFields() {
List<Field<?>> fields = new ArrayList<Field<?>>(getFields());
Item itemDataSource = getItemDataSource();
if (itemDataSource == null) {
unbindFields(fields);
} else {
bindFields(fields, itemDataSource);
}
}
private void unbindFields(List<Field<?>> fields) {
for (Field<?> field : fields) {
clearField(field);
unbind(field);
field.setParent(null);
}
}
private void bindFields(List<Field<?>> fields, Item itemDataSource) {
for (Field<?> field : fields) {
if (itemDataSource
.getItemProperty(getPropertyId(field)) != null) {
bind(field, getPropertyId(field));
}
}
}
}
/**
* Field factory used by default in the editor.
*
* Aims to fields of suitable type and with suitable size for use in the
* editor row.
*/
@Deprecated
public static class EditorFieldFactory
extends DefaultFieldGroupFieldFactory {
private static final EditorFieldFactory INSTANCE = new EditorFieldFactory();
protected EditorFieldFactory() {
}
/**
* Returns the singleton instance
*
* @return the singleton instance
*/
public static EditorFieldFactory get() {
return INSTANCE;
}
@Override
public <T extends Field> T createField(Class<?> type,
Class<T> fieldType) {
T f = super.createField(type, fieldType);
if (f != null) {
f.setWidth("100%");
}
return f;
}
@Override
protected AbstractSelect createCompatibleSelect(
Class<? extends AbstractSelect> fieldType) {
if (anySelect(fieldType)) {
return super.createCompatibleSelect(ComboBox.class);
}
return super.createCompatibleSelect(fieldType);
}
@Override
protected void populateWithEnumData(AbstractSelect select,
Class<? extends Enum> enumClass) {
// Use enums directly and the EnumToStringConverter to be consistent
// with what is shown in the Grid
@SuppressWarnings("unchecked")
EnumSet<?> enumSet = EnumSet.allOf(enumClass);
for (Object r : enumSet) {
select.addItem(r);
}
}
}
/**
* Error handler for the editor
*/
@Deprecated
public interface EditorErrorHandler extends Serializable {
/**
* Called when an exception occurs while the editor row is being saved
*
* @param event
* An event providing more information about the error
*/
void commitError(CommitErrorEvent event);
}
/**
* ContextClickEvent for the Grid Component.
*
* @since 7.6
*/
@Deprecated
public static class GridContextClickEvent extends ContextClickEvent {
private final Object itemId;
private final int rowIndex;
private final Object propertyId;
private final Section section;
public GridContextClickEvent(Grid source,
MouseEventDetails mouseEventDetails, Section section,
int rowIndex, Object itemId, Object propertyId) {
super(source, mouseEventDetails);
this.itemId = itemId;
this.propertyId = propertyId;
this.section = section;
this.rowIndex = rowIndex;
}
/**
* Returns the item id of context clicked row.
*
* @return item id of clicked row; <code>null</code> if header or footer
*/
public Object getItemId() {
return itemId;
}
/**
* Returns property id of clicked column.
*
* @return property id
*/
public Object getPropertyId() {
return propertyId;
}
/**
* Return the clicked section of Grid.
*
* @return section of grid
*/
public Section getSection() {
return section;
}
/**
* Returns the clicked row index relative to Grid section. In the body
* of the Grid the index is the item index in the Container. Header and
* Footer rows for index can be fetched with
* {@link Grid#getHeaderRow(int)} and {@link Grid#getFooterRow(int)}.
*
* @return row index in section
*/
public int getRowIndex() {
return rowIndex;
}
@Override
public Grid getComponent() {
return (Grid) super.getComponent();
}
}
/**
* An event which is fired when saving the editor fails
*/
@Deprecated
public static class CommitErrorEvent extends Component.Event {
private CommitException cause;
private Set<Column> errorColumns = new HashSet<Column>();
private String userErrorMessage;
public CommitErrorEvent(Grid grid, CommitException cause) {
super(grid);
this.cause = cause;
userErrorMessage = cause.getLocalizedMessage();
}
/**
* Retrieves the cause of the failure
*
* @return the cause of the failure
*/
public CommitException getCause() {
return cause;
}
@Override
public Grid getComponent() {
return (Grid) super.getComponent();
}
/**
* Checks if validation exceptions caused this error
*
* @return true if the problem was caused by a validation error
*/
public boolean isValidationFailure() {
return cause.getCause() instanceof InvalidValueException;
}
/**
* Marks that an error indicator should be shown for the editor of a
* column.
*
* @param column
* the column to show an error for
*/
public void addErrorColumn(Column column) {
errorColumns.add(column);
}
/**
* Gets all the columns that have been marked as erroneous.
*
* @return an umodifiable collection of erroneous columns
*/
public Collection<Column> getErrorColumns() {
return Collections.unmodifiableCollection(errorColumns);
}
/**
* Gets the error message to show to the user.
*
* @return error message to show
*/
public String getUserErrorMessage() {
return userErrorMessage;
}
/**
* Sets the error message to show to the user.
*
* @param userErrorMessage
* the user error message to set
*/
public void setUserErrorMessage(String userErrorMessage) {
this.userErrorMessage = userErrorMessage;
}
}
/**
* An event listener for column reorder events in the Grid.
*
* @since 7.5.0
*/
@Deprecated
public interface ColumnReorderListener extends Serializable {
/**
* Called when the columns of the grid have been reordered.
*
* @param event
* An event providing more information
*/
void columnReorder(ColumnReorderEvent event);
}
/**
* An event that is fired when the columns are reordered.
*
* @since 7.5.0
*/
@Deprecated
public static class ColumnReorderEvent extends Component.Event {
private final boolean userOriginated;
/**
*
* @param source
* the grid where the event originated from
* @param userOriginated
* <code>true</code> if event is a result of user
* interaction, <code>false</code> if from API call
*/
public ColumnReorderEvent(Grid source, boolean userOriginated) {
super(source);
this.userOriginated = userOriginated;
}
/**
* Returns <code>true</code> if the column reorder was done by the user,
* <code>false</code> if not and it was triggered by server side code.
*
* @return <code>true</code> if event is a result of user interaction
*/
public boolean isUserOriginated() {
return userOriginated;
}
}
/**
* An event listener for column resize events in the Grid.
*
* @since 7.6
*/
@Deprecated
public interface ColumnResizeListener extends Serializable {
/**
* Called when the columns of the grid have been resized.
*
* @param event
* An event providing more information
*/
void columnResize(ColumnResizeEvent event);
}
/**
* An event that is fired when a column is resized, either programmatically
* or by the user.
*
* @since 7.6
*/
@Deprecated
public static class ColumnResizeEvent extends Component.Event {
private final Column column;
private final boolean userOriginated;
/**
*
* @param source
* the grid where the event originated from
* @param userOriginated
* <code>true</code> if event is a result of user
* interaction, <code>false</code> if from API call
*/
public ColumnResizeEvent(Grid source, Column column,
boolean userOriginated) {
super(source);
this.column = column;
this.userOriginated = userOriginated;
}
/**
* Returns the column that was resized.
*
* @return the resized column.
*/
public Column getColumn() {
return column;
}
/**
* Returns <code>true</code> if the column resize was done by the user,
* <code>false</code> if not and it was triggered by server side code.
*
* @return <code>true</code> if event is a result of user interaction
*/
public boolean isUserOriginated() {
return userOriginated;
}
}
/**
* Interface for an editor event listener
*/
@Deprecated
public interface EditorListener extends Serializable {
public static final Method EDITOR_OPEN_METHOD = ReflectTools.findMethod(
EditorListener.class, "editorOpened", EditorOpenEvent.class);
public static final Method EDITOR_MOVE_METHOD = ReflectTools.findMethod(
EditorListener.class, "editorMoved", EditorMoveEvent.class);
public static final Method EDITOR_CLOSE_METHOD = ReflectTools
.findMethod(EditorListener.class, "editorClosed",
EditorCloseEvent.class);
/**
* Called when an editor is opened
*
* @param e
* an editor open event object
*/
public void editorOpened(EditorOpenEvent e);
/**
* Called when an editor is reopened without closing it first
*
* @param e
* an editor move event object
*/
public void editorMoved(EditorMoveEvent e);
/**
* Called when an editor is closed
*
* @param e
* an editor close event object
*/
public void editorClosed(EditorCloseEvent e);
}
/**
* Base class for editor related events
*/
@Deprecated
public static abstract class EditorEvent extends Component.Event {
private Object itemID;
protected EditorEvent(Grid source, Object itemID) {
super(source);
this.itemID = itemID;
}
/**
* Get the item (row) for which this editor was opened
*/
public Object getItem() {
return itemID;
}
}
/**
* This event gets fired when an editor is opened
*/
@Deprecated
public static class EditorOpenEvent extends EditorEvent {
public EditorOpenEvent(Grid source, Object itemID) {
super(source, itemID);
}
}
/**
* This event gets fired when an editor is opened while another row is being
* edited (i.e. editor focus moves elsewhere)
*/
@Deprecated
public static class EditorMoveEvent extends EditorEvent {
public EditorMoveEvent(Grid source, Object itemID) {
super(source, itemID);
}
}
/**
* This event gets fired when an editor is dismissed or closed by other
* means.
*/
@Deprecated
public static class EditorCloseEvent extends EditorEvent {
public EditorCloseEvent(Grid source, Object itemID) {
super(source, itemID);
}
}
/**
* Default error handler for the editor
*
*/
@Deprecated
public class DefaultEditorErrorHandler implements EditorErrorHandler {
@Override
public void commitError(CommitErrorEvent event) {
Map<Field<?>, InvalidValueException> invalidFields = event
.getCause().getInvalidFields();
if (!invalidFields.isEmpty()) {
Object firstErrorPropertyId = null;
Field<?> firstErrorField = null;
FieldGroup fieldGroup = event.getCause().getFieldGroup();
for (Column column : getColumns()) {
Object propertyId = column.getPropertyId();
Field<?> field = fieldGroup.getField(propertyId);
if (invalidFields.keySet().contains(field)) {
event.addErrorColumn(column);
if (firstErrorPropertyId == null) {
firstErrorPropertyId = propertyId;
firstErrorField = field;
}
}
}
/*
* Validation error, show first failure as
* "<Column header>: <message>"
*/
String caption = getColumn(firstErrorPropertyId)
.getHeaderCaption();
String message = invalidFields.get(firstErrorField)
.getLocalizedMessage();
event.setUserErrorMessage(caption + ": " + message);
} else {
com.vaadin.server.ErrorEvent.findErrorHandler(Grid.this).error(
new ConnectorErrorEvent(Grid.this, event.getCause()));
}
}
private Object getFirstPropertyId(FieldGroup fieldGroup,
Set<Field<?>> keySet) {
for (Column c : getColumns()) {
Object propertyId = c.getPropertyId();
Field<?> f = fieldGroup.getField(propertyId);
if (keySet.contains(f)) {
return propertyId;
}
}
return null;
}
}
/**
* Selection modes representing built-in {@link SelectionModel
* SelectionModels} that come bundled with {@link Grid}.
* <p>
* Passing one of these enums into
* {@link Grid#setSelectionMode(SelectionMode)} is equivalent to calling
* {@link Grid#setSelectionModel(SelectionModel)} with one of the built-in
* implementations of {@link SelectionModel}.
*
* @see Grid#setSelectionMode(SelectionMode)
* @see Grid#setSelectionModel(SelectionModel)
*/
@Deprecated
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.
* SelectionModel should extend {@link AbstractGridExtension}.
*/
@Deprecated
public interface SelectionModel extends Serializable, Extension {
/**
* Interface implemented by selection models which support disabling
* client side selection while still allowing programmatic selection on
* the server.
*
* @since 7.7.7
*/
@Deprecated
public interface HasUserSelectionAllowed extends SelectionModel {
/**
* Checks if the user is allowed to change the selection.
*
* @return <code>true</code> if the user is allowed to change the
* selection, <code>false</code> otherwise
*/
public boolean isUserSelectionAllowed();
/**
* Sets whether the user is allowed to change the selection.
*
* @param userSelectionAllowed
* <code>true</code> if the user is allowed to change the
* selection, <code>false</code> otherwise
*/
public void setUserSelectionAllowed(boolean userSelectionAllowed);
}
/**
* Checks whether an item is selected or not.
*
* @param itemId
* the item id to check for
* @return <code>true</code> iff the item is selected
*/
boolean isSelected(Object itemId);
/**
* Returns a collection of all the currently selected itemIds.
*
* @return a collection of all the currently selected itemIds
*/
Collection<Object> getSelectedRows();
/**
* Injects the current {@link Grid} instance into the SelectionModel.
* This method should usually call the extend method of
* {@link AbstractExtension}.
* <p>
* <em>Note:</em> This method should not be called manually.
*
* @param grid
* the Grid in which the SelectionModel currently is, or
* <code>null</code> when a selection model is being detached
* from a Grid.
*/
void setGrid(Grid grid);
/**
* Resets the SelectiomModel to an initial state.
* <p>
* Most often this means that the selection state is cleared, but
* implementations are free to interpret the "initial state" as they
* wish. Some, for example, may want to keep the first selected item as
* selected.
*/
void reset();
/**
* A SelectionModel that supports multiple selections to be made.
* <p>
* This interface has a contract of having the same behavior, no matter
* how the selection model is interacted with. In other words, if
* something is forbidden to do in e.g. the user interface, it must also
* be forbidden to do in the server-side and client-side APIs.
*/
@Deprecated
public interface Multi extends SelectionModel {
/**
* Marks items as selected.
* <p>
* This method does not clear any previous selection state, only
* adds to it.
*
* @param itemIds
* the itemId(s) to mark as selected
* @return <code>true</code> if the selection state changed.
* <code>false</code> if all the given itemIds already were
* selected
* @throws IllegalArgumentException
* if the <code>itemIds</code> varargs array is
* <code>null</code> or given itemIds don't exist in the
* container of Grid
* @see #deselect(Object...)
*/
boolean select(Object... itemIds) throws IllegalArgumentException;
/**
* Marks items as selected.
* <p>
* This method does not clear any previous selection state, only
* adds to it.
*
* @param itemIds
* the itemIds to mark as selected
* @return <code>true</code> if the selection state changed.
* <code>false</code> if all the given itemIds already were
* selected
* @throws IllegalArgumentException
* if <code>itemIds</code> is <code>null</code> 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 <code>true</code> if the selection state changed.
* <code>false</code> if none the given itemIds were
* selected previously
* @throws IllegalArgumentException
* if the <code>itemIds</code> varargs array is
* <code>null</code>
* @see #select(Object...)
*/
boolean deselect(Object... itemIds) throws IllegalArgumentException;
/**
* Marks items as deselected.
*
* @param itemIds
* the itemId(s) to remove from being selected
* @return <code>true</code> if the selection state changed.
* <code>false</code> if none the given itemIds were
* selected previously
* @throws IllegalArgumentException
* if <code>itemIds</code> is <code>null</code>
* @see #select(Collection)
*/
boolean deselect(Collection<?> itemIds)
throws IllegalArgumentException;
/**
* Marks all the items in the current Container as selected
*
* @return <code>true</code> iff some items were previously not
* selected
* @see #deselectAll()
*/
boolean selectAll();
/**
* Marks all the items in the current Container as deselected
*
* @return <code>true</code> iff some items were previously selected
* @see #selectAll()
*/
boolean deselectAll();
/**
* Marks items as selected while deselecting all items not in the
* given Collection.
*
* @param itemIds
* the itemIds to mark as selected
* @return <code>true</code> if the selection state changed.
* <code>false</code> if all the given itemIds already were
* selected
* @throws IllegalArgumentException
* if <code>itemIds</code> is <code>null</code> or given
* itemIds don't exist in the container of Grid
*/
boolean setSelected(Collection<?> itemIds)
throws IllegalArgumentException;
/**
* Marks items as selected while deselecting all items not in the
* varargs array.
*
* @param itemIds
* the itemIds to mark as selected
* @return <code>true</code> if the selection state changed.
* <code>false</code> if all the given itemIds already were
* selected
* @throws IllegalArgumentException
* if the <code>itemIds</code> varargs array is
* <code>null</code> or given itemIds don't exist in the
* container of Grid
*/
boolean setSelected(Object... itemIds)
throws IllegalArgumentException;
}
/**
* A SelectionModel that supports for only single rows to be selected at
* a time.
* <p>
* This interface has a contract of having the same behavior, no matter
* how the selection model is interacted with. In other words, if
* something is forbidden to do in e.g. the user interface, it must also
* be forbidden to do in the server-side and client-side APIs.
*/
@Deprecated
public interface Single extends SelectionModel {
/**
* Marks an item as selected.
*
* @param itemId
* the itemId to mark as selected; <code>null</code> for
* deselect
* @return <code>true</code> if the selection state changed.
* <code>false</code> if the itemId already was selected
* @throws IllegalStateException
* if the selection was illegal. One such reason might
* be that the 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
* <code>null</code> if nothing is selected
*/
Object getSelectedRow();
/**
* Sets whether it's allowed to deselect the selected row through
* the UI. Deselection is allowed by default.
*
* @param deselectAllowed
* <code>true</code> if the selected row can be
* deselected without selecting another row instead;
* otherwise <code>false</code>.
*/
public void setDeselectAllowed(boolean deselectAllowed);
/**
* Sets whether it's allowed to deselect the selected row through
* the UI.
*
* @return <code>true</code> if deselection is allowed; otherwise
* <code>false</code>
*/
public boolean isDeselectAllowed();
}
/**
* A SelectionModel that does not allow for rows to be selected.
* <p>
* This interface has a contract of having the same behavior, no matter
* how the selection model is interacted with. In other words, if the
* developer is unable to select something programmatically, it is not
* allowed for the end-user to select anything, either.
*/
@Deprecated
public interface None extends SelectionModel {
/**
* {@inheritDoc}
*
* @return always <code>false</code>.
*/
@Override
public boolean isSelected(Object itemId);
/**
* {@inheritDoc}
*
* @return always an empty collection.
*/
@Override
public Collection<Object> getSelectedRows();
}
}
/**
* A base class for SelectionModels that contains some of the logic that is
* reusable.
*/
@Deprecated
public static abstract class AbstractSelectionModel extends
AbstractGridExtension implements SelectionModel, DataGenerator {
protected final LinkedHashSet<Object> selection = new LinkedHashSet<Object>();
@Override
public boolean isSelected(final Object itemId) {
return selection.contains(itemId);
}
@Override
public Collection<Object> getSelectedRows() {
return new ArrayList<Object>(selection);
}
@Override
public void setGrid(final Grid grid) {
if (grid != null) {
extend(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 (!getParentGrid().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 SelectionEvent} to all the {@link SelectionListener
* SelectionListeners} currently added to the Grid in which this
* SelectionModel is.
* <p>
* Note that this is only a helper method, and routes the call all the
* way to Grid. A {@link SelectionModel} is not a
* {@link SelectionNotifier}
*
* @param oldSelection
* the complete {@link Collection} of the itemIds that were
* selected <em>before</em> this event happened
* @param newSelection
* the complete {@link Collection} of the itemIds that are
* selected <em>after</em> this event happened
*/
protected void fireSelectionEvent(final Collection<Object> oldSelection,
final Collection<Object> newSelection) {
getParentGrid().fireSelectionEvent(oldSelection, newSelection);
}
@Override
public void generateData(Object itemId, Item item, JsonObject rowData) {
if (isSelected(itemId)) {
rowData.put(GridState.JSONKEY_SELECTED, true);
}
}
@Override
public void destroyData(Object itemId) {
// NO-OP
}
@Override
protected Object getItemId(String rowKey) {
return rowKey != null ? super.getItemId(rowKey) : null;
}
}
/**
* A default implementation of a {@link SelectionModel.Single}
*/
@Deprecated
public static class SingleSelectionModel extends AbstractSelectionModel
implements SelectionModel.Single, HasUserSelectionAllowed {
@Override
protected void extend(AbstractClientConnector target) {
super.extend(target);
registerRpc(new SingleSelectionModelServerRpc() {
@Override
public void select(String rowKey) {
if (!isUserSelectionAllowed()) {
throw new IllegalStateException(
"Client tried to select '" + rowKey
+ "' although user selection is disallowed");
}
SingleSelectionModel.this.select(getItemId(rowKey), false);
}
});
}
@Override
public boolean select(final Object itemId) {
return select(itemId, true);
}
protected boolean select(final Object itemId, boolean refresh) {
if (itemId == null) {
return deselect(getSelectedRow());
}
checkItemIdExists(itemId);
final Object selectedRow = getSelectedRow();
final boolean modified = selection.add(itemId);
if (modified) {
final Collection<Object> deselected;
if (selectedRow != null) {
deselectInternal(selectedRow, false, true);
deselected = Collections.singleton(selectedRow);
} else {
deselected = Collections.emptySet();
}
fireSelectionEvent(deselected, selection);
}
if (refresh) {
refreshRow(itemId);
}
return modified;
}
private boolean deselect(final Object itemId) {
return deselectInternal(itemId, true, true);
}
private boolean deselectInternal(final Object itemId,
boolean fireEventIfNeeded, boolean refresh) {
final boolean modified = selection.remove(itemId);
if (modified) {
if (refresh) {
refreshRow(itemId);
}
if (fireEventIfNeeded) {
fireSelectionEvent(Collections.singleton(itemId),
Collections.emptySet());
}
}
return modified;
}
@Override
public Object getSelectedRow() {
if (selection.isEmpty()) {
return null;
} else {
return selection.iterator().next();
}
}
/**
* Resets the selection state.
* <p>
* If an item is selected, it will become deselected.
*/
@Override
public void reset() {
deselect(getSelectedRow());
}
@Override
public void setDeselectAllowed(boolean deselectAllowed) {
getState().deselectAllowed = deselectAllowed;
}
@Override
public boolean isDeselectAllowed() {
return getState().deselectAllowed;
}
@Override
protected SingleSelectionModelState getState() {
return (SingleSelectionModelState) super.getState();
}
@Override
protected SingleSelectionModelState getState(boolean markAsDirty) {
return (SingleSelectionModelState) super.getState(markAsDirty);
}
@Override
public boolean isUserSelectionAllowed() {
return getState(false).userSelectionAllowed;
}
@Override
public void setUserSelectionAllowed(boolean userSelectionAllowed) {
getState().userSelectionAllowed = userSelectionAllowed;
}
}
/**
* A default implementation for a {@link SelectionModel.None}
*/
@Deprecated
public static class NoSelectionModel extends AbstractSelectionModel
implements SelectionModel.None {
@Override
public boolean isSelected(final Object itemId) {
return false;
}
@Override
public Collection<Object> getSelectedRows() {
return Collections.emptyList();
}
/**
* Semantically resets the selection model.
* <p>
* Effectively a no-op.
*/
@Override
public void reset() {
// NOOP
}
}
/**
* A default implementation of a {@link SelectionModel.Multi}
*/
@Deprecated
public static class MultiSelectionModel extends AbstractSelectionModel
implements SelectionModel.Multi,
SelectionModel.HasUserSelectionAllowed {
/**
* The default selection size limit.
*
* @see #setSelectionLimit(int)
*/
public static final int DEFAULT_MAX_SELECTIONS = 1000;
private int selectionLimit = DEFAULT_MAX_SELECTIONS;
@Override
protected void extend(AbstractClientConnector target) {
super.extend(target);
registerRpc(new MultiSelectionModelServerRpc() {
@Override
public void select(List<String> rowKeys) {
if (!isUserSelectionAllowed()) {
throw new IllegalStateException(
"Client tried to select '" + rowKeys
+ "' although user selection is disallowed");
}
List<Object> items = new ArrayList<Object>();
for (String rowKey : rowKeys) {
items.add(getItemId(rowKey));
}
MultiSelectionModel.this.select(items, false);
}
@Override
public void deselect(List<String> rowKeys) {
if (!isUserSelectionAllowed()) {
throw new IllegalStateException(
"Client tried to deselect '" + rowKeys
+ "' although user selection is disallowed");
}
List<Object> items = new ArrayList<Object>();
for (String rowKey : rowKeys) {
items.add(getItemId(rowKey));
}
MultiSelectionModel.this.deselect(items, false);
}
@Override
public void selectAll() {
if (!isUserSelectionAllowed()) {
throw new IllegalStateException(
"Client tried to select all although user selection is disallowed");
}
MultiSelectionModel.this.selectAll(false);
}
@Override
public void deselectAll() {
if (!isUserSelectionAllowed()) {
throw new IllegalStateException(
"Client tried to deselect all although user selection is disallowed");
}
MultiSelectionModel.this.deselectAll(false);
}
});
}
@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}
* <p>
* 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 {
return select(itemIds, true);
}
protected boolean select(final Collection<?> itemIds, boolean refresh) {
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<Object> oldSelection = new HashSet<Object>(
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);
}
fireSelectionEvent(oldSelection, selection);
}
updateAllSelectedState();
if (refresh) {
for (Object itemId : itemIds) {
refreshRow(itemId);
}
}
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.
* <p>
* Old selections are not discarded if the current number of selected
* row exceeds the new limit.
* <p>
* 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 {
return deselect(itemIds, true);
}
protected boolean deselect(final Collection<?> itemIds,
boolean refresh) {
if (itemIds == null) {
throw new IllegalArgumentException("itemIds may not be null");
}
final boolean hasCommonElements = !Collections.disjoint(itemIds,
selection);
if (hasCommonElements) {
final HashSet<Object> oldSelection = new HashSet<Object>(
selection);
selection.removeAll(itemIds);
fireSelectionEvent(oldSelection, selection);
}
updateAllSelectedState();
if (refresh) {
for (Object itemId : itemIds) {
refreshRow(itemId);
}
}
return hasCommonElements;
}
@Override
public boolean selectAll() {
return selectAll(true);
}
protected boolean selectAll(boolean refresh) {
// select will fire the event
final Indexed container = getParentGrid().getContainerDataSource();
if (container != null) {
return select(container.getItemIds(), refresh);
} 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(false);
}
}
@Override
public boolean deselectAll() {
return deselectAll(true);
}
protected boolean deselectAll(boolean refresh) {
// deselect will fire the event
return deselect(getSelectedRows(), refresh);
}
/**
* {@inheritDoc}
* <p>
* The returned Collection is in <strong>order of selection</strong>
* – the item that was first selected will be first in the
* collection, and so on. Should an item have been selected twice
* without being deselected in between, it will have remained in its
* original position.
*/
@Override
public Collection<Object> getSelectedRows() {
// overridden only for JavaDoc
return super.getSelectedRows();
}
/**
* Resets the selection model.
* <p>
* Equivalent to calling {@link #deselectAll()}
*/
@Override
public void reset() {
deselectAll();
}
@Override
public boolean setSelected(Collection<?> itemIds)
throws IllegalArgumentException {
if (itemIds == null) {
throw new IllegalArgumentException("itemIds may not be null");
}
checkItemIdsExist(itemIds);
boolean changed = false;
Set<Object> selectedRows = new HashSet<Object>(itemIds);
final Collection<Object> oldSelection = getSelectedRows();
Set<Object> added = getDifference(selectedRows, selection);
if (!added.isEmpty()) {
changed = true;
selection.addAll(added);
for (Object id : added) {
refreshRow(id);
}
}
Set<Object> removed = getDifference(selection, selectedRows);
if (!removed.isEmpty()) {
changed = true;
selection.removeAll(removed);
for (Object id : removed) {
refreshRow(id);
}
}
if (changed) {
fireSelectionEvent(oldSelection, selection);
}
updateAllSelectedState();
return changed;
}
/**
* Compares two sets and returns a set containing all values that are
* present in the first, but not in the second.
*
* @param set1
* first item set
* @param set2
* second item set
* @return all values from set1 which are not present in set2
*/
private static Set<Object> getDifference(Set<Object> set1,
Set<Object> set2) {
Set<Object> diff = new HashSet<Object>(set1);
diff.removeAll(set2);
return diff;
}
@Override
public boolean setSelected(Object... itemIds)
throws IllegalArgumentException {
if (itemIds != null) {
return setSelected(Arrays.asList(itemIds));
} else {
throw new IllegalArgumentException(
"Vararg array of itemIds may not be null");
}
}
private void updateAllSelectedState() {
int totalRowCount = getParentGrid().datasource.size();
int rows = Math.min(totalRowCount, selectionLimit);
if (totalRowCount == 0) {
getState().allSelected = false;
} else {
getState().allSelected = selection.size() >= rows;
}
}
@Override
protected MultiSelectionModelState getState() {
return (MultiSelectionModelState) super.getState();
}
@Override
protected MultiSelectionModelState getState(boolean markAsDirty) {
return (MultiSelectionModelState) super.getState(markAsDirty);
}
@Override
public boolean isUserSelectionAllowed() {
return getState(false).userSelectionAllowed;
}
@Override
public void setUserSelectionAllowed(boolean userSelectionAllowed) {
getState().userSelectionAllowed = userSelectionAllowed;
}
}
/**
* A data class which contains information which identifies a row in a
* {@link Grid}.
* <p>
* Since this class follows the <code>Flyweight</code>-pattern any instance
* of this object is subject to change without the user knowing it and so
* should not be stored anywhere outside of the method providing these
* instances.
*/
@Deprecated
public static class RowReference implements Serializable {
private final Grid grid;
private Object itemId;
/**
* Creates a new row reference for the given grid.
*
* @param grid
* the grid that the row belongs to
*/
public RowReference(Grid grid) {
this.grid = grid;
}
/**
* Sets the identifying information for this row
*
* @param itemId
* the item id of the row
*/
public void set(Object itemId) {
this.itemId = itemId;
}
/**
* Gets the grid that contains the referenced row.
*
* @return the grid that contains referenced row
*/
public Grid getGrid() {
return grid;
}
/**
* Gets the item id of the row.
*
* @return the item id of the row
*/
public Object getItemId() {
return itemId;
}
/**
* Gets the item for the row.
*
* @return the item for the row
*/
public Item getItem() {
return grid.getContainerDataSource().getItem(itemId);
}
}
/**
* A data class which contains information which identifies a cell in a
* {@link Grid}.
* <p>
* Since this class follows the <code>Flyweight</code>-pattern any instance
* of this object is subject to change without the user knowing it and so
* should not be stored anywhere outside of the method providing these
* instances.
*/
@Deprecated
public static class CellReference implements Serializable {
private final RowReference rowReference;
private Object propertyId;
public CellReference(RowReference rowReference) {
this.rowReference = rowReference;
}
/**
* Sets the identifying information for this cell
*
* @param propertyId
* the property id of the column
*/
public void set(Object propertyId) {
this.propertyId = propertyId;
}
/**
* Gets the grid that contains the referenced cell.
*
* @return the grid that contains referenced cell
*/
public Grid getGrid() {
return rowReference.getGrid();
}
/**
* @return the property id of the column
*/
public Object getPropertyId() {
return propertyId;
}
/**
* @return the property for the cell
*/
public Property<?> getProperty() {
return getItem().getItemProperty(propertyId);
}
/**
* Gets the item id of the row of the cell.
*
* @return the item id of the row
*/
public Object getItemId() {
return rowReference.getItemId();
}
/**
* Gets the item for the row of the cell.
*
* @return the item for the row
*/
public Item getItem() {
return rowReference.getItem();
}
/**
* Gets the value of the cell.
*
* @return the value of the cell
*/
public Object getValue() {
return getProperty().getValue();
}
}
/**
* A callback interface for generating custom style names for Grid rows.
*
* @see Grid#setRowStyleGenerator(RowStyleGenerator)
*/
@Deprecated
public interface RowStyleGenerator extends Serializable {
/**
* Called by Grid to generate a style name for a row.
*
* @param row
* the row to generate a style for
* @return the style name to add to this row, or {@code null} to not set
* any style
*/
public String getStyle(RowReference row);
}
/**
* A callback interface for generating custom style names for Grid cells.
*
* @see Grid#setCellStyleGenerator(CellStyleGenerator)
*/
@Deprecated
public interface CellStyleGenerator extends Serializable {
/**
* Called by Grid to generate a style name for a column.
*
* @param cell
* the cell to generate a style for
* @return the style name to add to this cell, or {@code null} to not
* set any style
*/
public String getStyle(CellReference cell);
}
/**
* A callback interface for generating optional descriptions (tooltips) for
* Grid rows. If a description is generated for a row, it is used for all
* the cells in the row for which a {@link CellDescriptionGenerator cell
* description} is not generated.
*
* @see Grid#setRowDescriptionGenerator
*
* @since 7.6
*/
@Deprecated
public interface RowDescriptionGenerator extends Serializable {
/**
* Called by Grid to generate a description (tooltip) for a row. The
* description may contain HTML which is rendered directly; if this is
* not desired the returned string must be escaped by the implementing
* method.
*
* @param row
* the row to generate a description for
* @return the row description or {@code null} for no description
*/
public String getDescription(RowReference row);
}
/**
* A callback interface for generating optional descriptions (tooltips) for
* Grid cells. If a cell has both a {@link RowDescriptionGenerator row
* description}Â and a cell description, the latter has precedence.
*
* @see Grid#setCellDescriptionGenerator(CellDescriptionGenerator)
*
* @since 7.6
*/
@Deprecated
public interface CellDescriptionGenerator extends Serializable {
/**
* Called by Grid to generate a description (tooltip) for a cell. The
* description may contain HTML which is rendered directly; if this is
* not desired the returned string must be escaped by the implementing
* method.
*
* @param cell
* the cell to generate a description for
* @return the cell description or {@code null} for no description
*/
public String getDescription(CellReference cell);
}
/**
* Class for generating all row and cell related data for the essential
* parts of Grid.
*/
private class RowDataGenerator implements DataGenerator {
private void put(String key, String value, JsonObject object) {
if (value != null && !value.isEmpty()) {
object.put(key, value);
}
}
@Override
public void generateData(Object itemId, Item item, JsonObject rowData) {
RowReference row = new RowReference(Grid.this);
row.set(itemId);
if (rowStyleGenerator != null) {
String style = rowStyleGenerator.getStyle(row);
put(GridState.JSONKEY_ROWSTYLE, style, rowData);
}
if (rowDescriptionGenerator != null) {
String description = rowDescriptionGenerator
.getDescription(row);
put(GridState.JSONKEY_ROWDESCRIPTION, description, rowData);
}
JsonObject cellStyles = Json.createObject();
JsonObject cellData = Json.createObject();
JsonObject cellDescriptions = Json.createObject();
CellReference cell = new CellReference(row);
for (Column column : getColumns()) {
cell.set(column.getPropertyId());
writeData(cell, cellData);
writeStyles(cell, cellStyles);
writeDescriptions(cell, cellDescriptions);
}
if (cellDescriptionGenerator != null
&& cellDescriptions.keys().length > 0) {
rowData.put(GridState.JSONKEY_CELLDESCRIPTION,
cellDescriptions);
}
if (cellStyleGenerator != null && cellStyles.keys().length > 0) {
rowData.put(GridState.JSONKEY_CELLSTYLES, cellStyles);
}
rowData.put(GridState.JSONKEY_DATA, cellData);
}
private void writeStyles(CellReference cell, JsonObject styles) {
if (cellStyleGenerator != null) {
String style = cellStyleGenerator.getStyle(cell);
put(columnKeys.key(cell.getPropertyId()), style, styles);
}
}
private void writeDescriptions(CellReference cell,
JsonObject descriptions) {
if (cellDescriptionGenerator != null) {
String description = cellDescriptionGenerator
.getDescription(cell);
put(columnKeys.key(cell.getPropertyId()), description,
descriptions);
}
}
private void writeData(CellReference cell, JsonObject data) {
Column column = getColumn(cell.getPropertyId());
Converter<?, ?> converter = column.getConverter();
Renderer<?> renderer = column.getRenderer();
Item item = cell.getItem();
Property itemProperty = item.getItemProperty(cell.getPropertyId());
Object modelValue = itemProperty == null ? null
: itemProperty.getValue();
data.put(columnKeys.key(cell.getPropertyId()), AbstractRenderer
.encodeValue(modelValue, renderer, converter, getLocale()));
}
@Override
public void destroyData(Object itemId) {
// NO-OP
}
}
/**
* Abstract base class for Grid header and footer sections.
*
* @since 7.6
* @param <ROWTYPE>
* the type of the rows in the section
*/
@Deprecated
public abstract static class StaticSection<ROWTYPE extends StaticSection.StaticRow<?>>
implements Serializable {
/**
* Abstract base class for Grid header and footer rows.
*
* @param <CELLTYPE>
* the type of the cells in the row
*/
@Deprecated
public abstract static class StaticRow<CELLTYPE extends StaticCell>
implements Serializable {
private RowState rowState = new RowState();
protected StaticSection<?> section;
private Map<Object, CELLTYPE> cells = new LinkedHashMap<Object, CELLTYPE>();
private Map<Set<CELLTYPE>, CELLTYPE> cellGroups = new HashMap<Set<CELLTYPE>, 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<CELLTYPE> cellGroupForCell = getCellGroupForCell(cell);
if (cellGroupForCell != null) {
removeCellFromGroup(cell, cellGroupForCell);
}
rowState.cells.remove(cell.getCellState());
}
}
private void removeCellFromGroup(CELLTYPE cell,
Set<CELLTYPE> cellGroup) {
String columnId = cell.getColumnId();
for (Set<String> 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<CELLTYPE> 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) {
if (propertyIds.length < 2) {
throw new IllegalArgumentException(
"You need to merge at least 2 properties");
}
Set<CELLTYPE> cells = new HashSet<CELLTYPE>();
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) {
if (cells.length < 2) {
throw new IllegalArgumentException(
"You need to merge at least 2 cells");
}
return join(new HashSet<CELLTYPE>(Arrays.asList(cells)));
}
protected CELLTYPE join(Set<CELLTYPE> 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<String> columnGroup = new HashSet<String>();
for (CELLTYPE cell : cells) {
columnGroup.add(cell.getColumnId());
}
rowState.cellGroups.put(columnGroup, newCell.getCellState());
cellGroups.put(cells, newCell);
return newCell;
}
private Set<CELLTYPE> getCellGroupForCell(CELLTYPE cell) {
for (Set<CELLTYPE> 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;
}
/**
* Writes the declarative design to the given table row element.
*
* @since 7.5.0
* @param trElement
* Element to write design to
* @param designContext
* the design context
*/
protected void writeDesign(Element trElement,
DesignContext designContext) {
Set<CELLTYPE> visited = new HashSet<CELLTYPE>();
for (Grid.Column column : section.grid.getColumns()) {
CELLTYPE cell = getCell(column.getPropertyId());
if (visited.contains(cell)) {
continue;
}
visited.add(cell);
Element cellElement = trElement
.appendElement(getCellTagName());
cell.writeDesign(cellElement, designContext);
for (Entry<Set<CELLTYPE>, CELLTYPE> entry : cellGroups
.entrySet()) {
if (entry.getValue() == cell) {
cellElement.attr("colspan",
"" + entry.getKey().size());
break;
}
}
}
}
/**
* Reads the declarative design from the given table row element.
*
* @since 7.5.0
* @param trElement
* Element to read design from
* @param designContext
* the design context
* @throws DesignException
* if the given table row contains unexpected children
*/
protected void readDesign(Element trElement,
DesignContext designContext) throws DesignException {
Elements cellElements = trElement.children();
int totalColSpans = 0;
for (int i = 0; i < cellElements.size(); ++i) {
Element element = cellElements.get(i);
if (!element.tagName().equals(getCellTagName())) {
throw new DesignException(
"Unexpected element in tr while expecting "
+ getCellTagName() + ": "
+ element.tagName());
}
int columnIndex = i + totalColSpans;
int colspan = DesignAttributeHandler.readAttribute(
"colspan", element.attributes(), 1, int.class);
Set<CELLTYPE> cells = new HashSet<CELLTYPE>();
for (int c = 0; c < colspan; ++c) {
cells.add(getCell(section.grid.getColumns()
.get(columnIndex + c).getPropertyId()));
}
if (colspan > 1) {
totalColSpans += colspan - 1;
join(cells).readDesign(element, designContext);
} else {
cells.iterator().next().readDesign(element,
designContext);
}
}
}
abstract protected String getCellTagName();
void detach() {
for (CELLTYPE cell : cells.values()) {
cell.detach();
}
for (CELLTYPE cell : cellGroups.values()) {
cell.detach();
}
}
}
/**
* A header or footer cell. Has a simple textual caption.
*/
@Deprecated
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) {
removeComponentIfPresent();
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) {
removeComponentIfPresent();
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) {
removeComponentIfPresent();
component.setParent(row.section.grid);
cellState.connector = component;
cellState.type = GridStaticCellType.WIDGET;
row.section.markAsDirty();
}
/**
* Returns the type of content stored in this cell.
*
* @return cell content type
*/
public GridStaticCellType getCellType() {
return cellState.type;
}
/**
* 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();
}
private void removeComponentIfPresent() {
Component component = (Component) cellState.connector;
if (component != null) {
component.setParent(null);
cellState.connector = null;
}
}
/**
* Writes the declarative design to the given table cell element.
*
* @since 7.5.0
* @param cellElement
* Element to write design to
* @param designContext
* the design context
*/
protected void writeDesign(Element cellElement,
DesignContext designContext) {
switch (cellState.type) {
case TEXT:
cellElement.attr("plain-text", true);
cellElement.appendText(getText());
break;
case HTML:
cellElement.append(getHtml());
break;
case WIDGET:
cellElement.appendChild(
designContext.createElement(getComponent()));
break;
}
}
/**
* Reads the declarative design from the given table cell element.
*
* @since 7.5.0
* @param cellElement
* Element to read design from
* @param designContext
* the design context
*/
protected void readDesign(Element cellElement,
DesignContext designContext) {
if (!cellElement.hasAttr("plain-text")) {
if (cellElement.children().size() > 0
&& cellElement.child(0).tagName().contains("-")) {
setComponent(
designContext.readDesign(cellElement.child(0)));
} else {
setHtml(cellElement.html());
}
} else {
// text – need to unescape HTML entities
setText(DesignFormatter
.decodeFromTextNode(cellElement.html()));
}
}
void detach() {
removeComponentIfPresent();
}
}
protected Grid grid;
protected List<ROWTYPE> rows = new ArrayList<ROWTYPE>();
/**
* Sets the visibility of the whole section.
*
* @param visible
* true to show this section, false to hide
*/
public void setVisible(boolean visible) {
if (getSectionState().visible != visible) {
getSectionState().visible = visible;
markAsDirty();
}
}
/**
* Returns the visibility of this section.
*
* @return true if visible, false otherwise.
*/
public boolean isVisible() {
return getSectionState().visible;
}
/**
* Removes the row at the given position.
*
* @param rowIndex
* 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);
row.detach();
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());
for (Object id : grid.columns.keySet()) {
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<String> columnOrder = grid.getState().columnOrder;
for (ROWTYPE row : rows) {
for (Set<String> 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<String> columnOrder,
Set<String> 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;
}
/**
* Writes the declarative design to the given table section element.
*
* @since 7.5.0
* @param tableSectionElement
* Element to write design to
* @param designContext
* the design context
*/
protected void writeDesign(Element tableSectionElement,
DesignContext designContext) {
for (ROWTYPE row : rows) {
row.writeDesign(tableSectionElement.appendElement("tr"),
designContext);
}
}
/**
* Writes the declarative design from the given table section element.
*
* @since 7.5.0
* @param tableSectionElement
* Element to read design from
* @param designContext
* the design context
* @throws DesignException
* if the table section contains unexpected children
*/
protected void readDesign(Element tableSectionElement,
DesignContext designContext) throws DesignException {
while (rows.size() > 0) {
removeRow(0);
}
for (Element row : tableSectionElement.children()) {
if (!row.tagName().equals("tr")) {
throw new DesignException("Unexpected element in "
+ tableSectionElement.tagName() + ": "
+ row.tagName());
}
appendRow().readDesign(row, designContext);
}
}
}
/**
* Represents the header section of a Grid.
*/
@Deprecated
protected static class Header extends StaticSection<HeaderRow> {
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");
}
}
}
}
@Override
protected void readDesign(Element tableSectionElement,
DesignContext designContext) {
super.readDesign(tableSectionElement, designContext);
if (defaultRow == null && !rows.isEmpty()) {
grid.setDefaultHeaderRow(rows.get(0));
}
}
}
/**
* Represents a header row in Grid.
*/
@Deprecated
public static class HeaderRow extends StaticSection.StaticRow<HeaderCell> {
protected HeaderRow(StaticSection<?> section) {
super(section);
}
private void setDefaultRow(boolean value) {
getRowState().defaultRow = value;
}
private boolean isDefaultRow() {
return getRowState().defaultRow;
}
@Override
protected HeaderCell createCell() {
return new HeaderCell(this);
}
@Override
protected String getCellTagName() {
return "th";
}
@Override
protected void writeDesign(Element trElement,
DesignContext designContext) {
super.writeDesign(trElement, designContext);
if (section.grid.getDefaultHeaderRow() == this) {
DesignAttributeHandler.writeAttribute("default",
trElement.attributes(), true, null, boolean.class,
designContext);
}
}
@Override
protected void readDesign(Element trElement,
DesignContext designContext) {
super.readDesign(trElement, designContext);
boolean defaultRow = DesignAttributeHandler.readAttribute("default",
trElement.attributes(), false, boolean.class);
if (defaultRow) {
section.grid.setDefaultHeaderRow(this);
}
}
}
/**
* Represents a header cell in Grid. Can be a merged cell for multiple
* columns.
*/
@Deprecated
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.
*/
@Deprecated
protected static class Footer extends StaticSection<FooterRow> {
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.
*/
@Deprecated
public static class FooterRow extends StaticSection.StaticRow<FooterCell> {
protected FooterRow(StaticSection<?> section) {
super(section);
}
@Override
protected FooterCell createCell() {
return new FooterCell(this);
}
@Override
protected String getCellTagName() {
return "td";
}
}
/**
* Represents a footer cell in Grid.
*/
@Deprecated
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)}.
*/
@Deprecated
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 propertyId;
private Converter<?, Object> converter;
/**
* A check for allowing the
* {@link #Column(Grid, GridColumnState, Object) constructor} to call
* {@link #setConverter(Converter)} with a <code>null</code>, even if
* model and renderer aren't compatible.
*/
private boolean isFirstConverterAssignment = true;
/**
* Internally used constructor.
*
* @param grid
* The grid this column belongs to. Should not be null.
* @param state
* the shared state of this column
* @param propertyId
* the backing property id for this column
*/
Column(Grid grid, GridColumnState state, Object propertyId) {
this.grid = grid;
this.state = state;
this.propertyId = propertyId;
internalSetRenderer(new TextRenderer());
}
/**
* Returns the serializable state of this column that is sent to the
* client side connector.
*
* @return the internal state of the column
*/
GridColumnState getState() {
return state;
}
/**
* Returns the property id for the backing property of this Column
*
* @return property id
*/
public Object getPropertyId() {
return propertyId;
}
/**
* 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.
*
* @throws IllegalStateException
* if the column no longer is attached to the grid
*/
public String getHeaderCaption() throws IllegalStateException {
checkColumnIsAttached();
return state.headerCaption;
}
/**
* Sets the caption of the header. This caption is also used as the
* hiding toggle caption, unless it is explicitly set via
* {@link #setHidingToggleCaption(String)}.
*
* @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();
if (caption == null) {
caption = ""; // Render null as empty
}
state.headerCaption = caption;
HeaderRow row = grid.getHeader().getDefaultRow();
if (row != null) {
row.getCell(grid.getPropertyIdByColumnId(state.id))
.setText(caption);
}
return this;
}
/**
* Gets the caption of the hiding toggle for this column.
*
* @since 7.5.0
* @see #setHidingToggleCaption(String)
* @return the caption for the hiding toggle for this column
* @throws IllegalStateException
* if the column is no longer attached to any grid
*/
public String getHidingToggleCaption() throws IllegalStateException {
checkColumnIsAttached();
return state.hidingToggleCaption;
}
/**
* Sets the caption of the hiding toggle for this column. Shown in the
* toggle for this column in the grid's sidebar when the column is
* {@link #isHidable() hidable}.
* <p>
* The default value is <code>null</code>, and in that case the column's
* {@link #getHeaderCaption() header caption} is used.
* <p>
* <em>NOTE:</em> setting this to empty string might cause the hiding
* toggle to not render correctly.
*
* @since 7.5.0
* @param hidingToggleCaption
* the text to show in the column hiding toggle
* @return the column itself
* @throws IllegalStateException
* if the column is no longer attached to any grid
*/
public Column setHidingToggleCaption(String hidingToggleCaption)
throws IllegalStateException {
checkColumnIsAttached();
state.hidingToggleCaption = hidingToggleCaption;
grid.markAsDirty();
return this;
}
/**
* Returns the width (in pixels). By default a column is 100px wide.
*
* @return the width in pixels of the column
* @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).
* <p>
* This overrides any configuration set by any of
* {@link #setExpandRatio(int)}, {@link #setMinimumWidth(double)} or
* {@link #setMaximumWidth(double)}.
*
* @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()
+ ")");
}
if (state.width != pixelWidth) {
state.width = pixelWidth;
grid.markAsDirty();
grid.fireColumnResizeEvent(this, false);
}
return this;
}
/**
* Returns whether this column has an undefined width.
*
* @since 7.6
* @return whether the width is undefined
* @throws IllegalStateException
* if the column is no longer attached to any grid
*/
public boolean isWidthUndefined() {
checkColumnIsAttached();
return state.width < 0;
}
/**
* Marks the column width as undefined. An undefined width means 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();
if (!isWidthUndefined()) {
state.width = -1;
grid.markAsDirty();
grid.fireColumnResizeEvent(this, false);
}
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(getState().id)
+ 1);
return this;
}
/**
* Sets the renderer for this column.
* <p>
* If a suitable converter isn't defined explicitly, the session
* converter factory is used to find a compatible converter.
*
* @param renderer
* the renderer to use
* @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 <T> Column setRenderer(Renderer<T> renderer,
Converter<? extends T, ?> converter) {
if (renderer.getParent() != null) {
throw new IllegalArgumentException(
"Cannot set a renderer that is already connected to a grid column (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 <T> boolean internalSetRenderer(Renderer<T> renderer) {
Converter<? extends T, ?> converter;
if (isCompatibleWithProperty(renderer, getConverter())) {
// Use the existing converter (possibly none) if types
// compatible
converter = (Converter<? extends T, ?>) getConverter();
} else {
converter = ConverterUtil.getConverter(
renderer.getPresentationType(), getModelType(),
getSession());
}
setRenderer(renderer, converter);
return isCompatibleWithProperty(renderer, converter);
}
private VaadinSession getSession() {
UI ui = grid.getUI();
return ui != null ? ui.getSession() : null;
}
private boolean isCompatibleWithProperty(Renderer<?> renderer,
Converter<?, ?> converter) {
Class<?> type;
if (converter == null) {
type = getModelType();
} else {
type = converter.getPresentationType();
}
return renderer.getPresentationType().isAssignableFrom(type);
}
private Class<?> getModelType() {
return grid.getContainerDataSource()
.getType(grid.getPropertyIdByColumnId(state.id));
}
/**
* Sets whether this column is sortable by the user. The grid can be
* sorted by a sortable column by clicking or tapping the column's
* default header. Programmatic sorting using the Grid#sort methods is
* not affected by this setting.
*
* @param sortable
* {@code true} if the user should be able to sort the
* column, {@code false} otherwise
* @return the column itself
*
* @throws IllegalStateException
* if the data source of the Grid does not implement
* {@link Sortable}
* @throws IllegalStateException
* if the data source does not support sorting by the
* property associated with this column
*/
public Column setSortable(boolean sortable) {
checkColumnIsAttached();
if (sortable) {
if (!(grid.datasource instanceof Sortable)) {
throw new IllegalStateException("Can't set column "
+ toString()
+ " sortable. The Container of Grid does not implement Sortable");
} else if (!((Sortable) grid.datasource)
.getSortableContainerPropertyIds()
.contains(propertyId)) {
throw new IllegalStateException(
"Can't set column " + toString()
+ " sortable. Container doesn't support sorting by property "
+ propertyId);
}
}
state.sortable = sortable;
grid.markAsDirty();
return this;
}
/**
* Returns whether the user can sort the grid by this column.
* <p>
* <em>Note:</em> it is possible to sort by this column programmatically
* using the Grid#sort methods regardless of the returned value.
*
* @return {@code true} if the column is sortable by the user,
* {@code false} otherwise
*/
public boolean isSortable() {
return state.sortable;
}
@Override
public String toString() {
return getClass().getSimpleName() + "[propertyId:"
+ grid.getPropertyIdByColumnId(state.id) + "]";
}
/**
* Sets the ratio with which the column expands.
* <p>
* By default, all columns expand equally (treated as if all of them had
* an expand ratio of 1). Once at least one column gets a defined expand
* ratio, the implicit expand ratio is removed, and only the defined
* expand ratios are taken into account.
* <p>
* If a column has a defined width ({@link #setWidth(double)}), it
* overrides this method's effects.
* <p>
* <em>Example:</em> A grid with three columns, with expand ratios 0, 1
* and 2, respectively. The column with a <strong>ratio of 0 is exactly
* as wide as its contents requires</strong>. The column with a ratio of
* 1 is as wide as it needs, <strong>plus a third of any excess
* space</strong>, because we have 3 parts total, and this column
* reserves only one of those. The column with a ratio of 2, is as wide
* as it needs to be, <strong>plus two thirds</strong> of the excess
* width.
*
* @param expandRatio
* the expand ratio of this column. {@code 0} to not have it
* expand at all. A negative number to clear the expand
* value.
* @throws IllegalStateException
* if the column is no longer attached to any grid
* @see #setWidth(double)
*/
public Column setExpandRatio(int expandRatio)
throws IllegalStateException {
checkColumnIsAttached();
getState().expandRatio = expandRatio;
grid.markAsDirty();
return this;
}
/**
* Returns the column's expand ratio.
*
* @return the column's expand ratio
* @see #setExpandRatio(int)
*/
public int getExpandRatio() {
return getState().expandRatio;
}
/**
* Clears the expand ratio for this column.
* <p>
* Equal to calling {@link #setExpandRatio(int) setExpandRatio(-1)}
*
* @throws IllegalStateException
* if the column is no longer attached to any grid
*/
public Column clearExpandRatio() throws IllegalStateException {
return setExpandRatio(-1);
}
/**
* Sets the minimum width for this column.
* <p>
* This defines the minimum guaranteed pixel width of the column
* <em>when it is set to expand</em>.
*
* @throws IllegalStateException
* if the column is no longer attached to any grid
* @see #setExpandRatio(int)
*/
public Column setMinimumWidth(double pixels)
throws IllegalStateException {
checkColumnIsAttached();
final double maxwidth = getMaximumWidth();
if (pixels >= 0 && pixels > maxwidth && maxwidth >= 0) {
throw new IllegalArgumentException("New minimum width ("
+ pixels + ") was greater than maximum width ("
+ maxwidth + ")");
}
getState().minWidth = pixels;
grid.markAsDirty();
return this;
}
/**
* Return the minimum width for this column.
*
* @return the minimum width for this column
* @see #setMinimumWidth(double)
*/
public double getMinimumWidth() {
return getState().minWidth;
}
/**
* Sets the maximum width for this column.
* <p>
* This defines the maximum allowed pixel width of the column <em>when
* it is set to expand</em>.
*
* @param pixels
* the maximum width
* @throws IllegalStateException
* if the column is no longer attached to any grid
* @see #setExpandRatio(int)
*/
public Column setMaximumWidth(double pixels) {
checkColumnIsAttached();
final double minwidth = getMinimumWidth();
if (pixels >= 0 && pixels < minwidth && minwidth >= 0) {
throw new IllegalArgumentException("New maximum width ("
+ pixels + ") was less than minimum width (" + minwidth
+ ")");
}
getState().maxWidth = pixels;
grid.markAsDirty();
return this;
}
/**
* Returns the maximum width for this column.
*
* @return the maximum width for this column
* @see #setMaximumWidth(double)
*/
public double getMaximumWidth() {
return getState().maxWidth;
}
/**
* Sets whether the properties corresponding to this column should be
* editable when the item editor is active. By default columns are
* editable.
* <p>
* Values in non-editable columns are currently not displayed when the
* editor is active, but this will probably change in the future. They
* are not automatically assigned an editor field and, if one is
* manually assigned, it is not used. Columns that cannot (or should
* not) be edited even in principle should be set non-editable.
*
* @param editable
* {@code true} if this column should be editable,
* {@code false} otherwise
* @return this column
*
* @throws IllegalStateException
* if the editor is currently active
*
* @see Grid#editItem(Object)
* @see Grid#isEditorActive()
*/
public Column setEditable(boolean editable) {
checkColumnIsAttached();
if (grid.isEditorActive()) {
throw new IllegalStateException(
"Cannot change column editable status while the editor is active");
}
getState().editable = editable;
grid.markAsDirty();
return this;
}
/**
* Returns whether the properties corresponding to this column should be
* editable when the item editor is active.
*
* @return {@code true} if this column is editable, {@code false}
* otherwise
*
* @see Grid#editItem(Object)
* @see #setEditable(boolean)
*/
public boolean isEditable() {
return getState().editable;
}
/**
* Sets the field component used to edit the properties in this column
* when the item editor is active. If an item has not been set, then the
* binding is postponed until the item is set using
* {@link #editItem(Object)}.
* <p>
* Setting the field to <code>null</code> clears any previously set
* field, causing a new field to be created the next time the item
* editor is opened.
*
* @param editor
* the editor field
* @return this column
*/
public Column setEditorField(Field<?> editor) {
grid.setEditorField(getPropertyId(), editor);
return this;
}
/**
* Returns the editor field used to edit the properties in this column
* when the item editor is active. Returns null if the column is not
* {@link Column#isEditable() editable}.
* <p>
* When {@link #editItem(Object) editItem} is called, fields are
* automatically created and bound for any unbound properties.
* <p>
* Getting a field before the editor has been opened depends on special
* support from the {@link FieldGroup} in use. Using this method with a
* user-provided <code>FieldGroup</code> might cause
* {@link FieldGroup.BindException BindException} to be thrown.
*
* @return the bound field; or <code>null</code> if the respective
* column is not editable
*
* @throws IllegalArgumentException
* if there is no column for the provided property id
* @throws FieldGroup.BindException
* if no field has been configured and there is a problem
* building or binding
*/
public Field<?> getEditorField() {
return grid.getEditorField(getPropertyId());
}
/**
* Hides or shows the column. By default columns are visible before
* explicitly hiding them.
*
* @since 7.5.0
* @param hidden
* <code>true</code> to hide the column, <code>false</code>
* to show
* @return this column
*/
public Column setHidden(boolean hidden) {
if (hidden != getState().hidden) {
getState().hidden = hidden;
grid.markAsDirty();
grid.fireColumnVisibilityChangeEvent(this, hidden, false);
}
return this;
}
/**
* Returns whether this column is hidden. Default is {@code false}.
*
* @since 7.5.0
* @return <code>true</code> if the column is currently hidden,
* <code>false</code> otherwise
*/
public boolean isHidden() {
return getState().hidden;
}
/**
* Sets whether this column can be hidden by the user. Hidable columns
* can be hidden and shown via the sidebar menu.
*
* @since 7.5.0
* @param hidable
* <code>true</code> iff the column may be hidable by the
* user via UI interaction
* @return this column
*/
public Column setHidable(boolean hidable) {
if (hidable != getState().hidable) {
getState().hidable = hidable;
grid.markAsDirty();
}
return this;
}
/**
* Returns whether this column can be hidden by the user. Default is
* {@code false}.
* <p>
* <em>Note:</em> the column can be programmatically hidden using
* {@link #setHidden(boolean)} regardless of the returned value.
*
* @since 7.5.0
* @return <code>true</code> if the user can hide the column,
* <code>false</code> if not
*/
public boolean isHidable() {
return getState().hidable;
}
/**
* Sets whether this column can be resized by the user.
*
* @since 7.6
* @param resizable
* {@code true} if this column should be resizable,
* {@code false} otherwise
*/
public Column setResizable(boolean resizable) {
if (resizable != getState().resizable) {
getState().resizable = resizable;
grid.markAsDirty();
}
return this;
}
/**
* Returns whether this column can be resized by the user. Default is
* {@code true}.
* <p>
* <em>Note:</em> the column can be programmatically resized using
* {@link #setWidth(double)} and {@link #setWidthUndefined()} regardless
* of the returned value.
*
* @since 7.6
* @return {@code true} if this column is resizable, {@code false}
* otherwise
*/
public boolean isResizable() {
return getState().resizable;
}
/**
* Writes the design attributes for this column into given element.
*
* @since 7.5.0
*
* @param design
* Element to write attributes into
*
* @param designContext
* the design context
*/
protected void writeDesign(Element design,
DesignContext designContext) {
Attributes attributes = design.attributes();
GridColumnState def = new GridColumnState();
DesignAttributeHandler.writeAttribute("property-id", attributes,
getPropertyId(), null, Object.class, designContext);
// Sortable is a special attribute that depends on the container.
DesignAttributeHandler.writeAttribute("sortable", attributes,
isSortable(), null, boolean.class, designContext);
DesignAttributeHandler.writeAttribute("editable", attributes,
isEditable(), def.editable, boolean.class, designContext);
DesignAttributeHandler.writeAttribute("resizable", attributes,
isResizable(), def.resizable, boolean.class, designContext);
DesignAttributeHandler.writeAttribute("hidable", attributes,
isHidable(), def.hidable, boolean.class, designContext);
DesignAttributeHandler.writeAttribute("hidden", attributes,
isHidden(), def.hidden, boolean.class, designContext);
DesignAttributeHandler.writeAttribute("hiding-toggle-caption",
attributes, getHidingToggleCaption(), null, String.class,
designContext);
DesignAttributeHandler.writeAttribute("width", attributes,
getWidth(), def.width, Double.class, designContext);
DesignAttributeHandler.writeAttribute("min-width", attributes,
getMinimumWidth(), def.minWidth, Double.class,
designContext);
DesignAttributeHandler.writeAttribute("max-width", attributes,
getMaximumWidth(), def.maxWidth, Double.class,
designContext);
DesignAttributeHandler.writeAttribute("expand", attributes,
getExpandRatio(), def.expandRatio, Integer.class,
designContext);
}
/**
* Reads the design attributes for this column from given element.
*
* @since 7.5.0
* @param design
* Element to read attributes from
* @param designContext
* the design context
*/
protected void readDesign(Element design, DesignContext designContext) {
Attributes attributes = design.attributes();
if (design.hasAttr("sortable")) {
setSortable(DesignAttributeHandler.readAttribute("sortable",
attributes, boolean.class));
}
if (design.hasAttr("editable")) {
setEditable(DesignAttributeHandler.readAttribute("editable",
attributes, boolean.class));
}
if (design.hasAttr("resizable")) {
setResizable(DesignAttributeHandler.readAttribute("resizable",
attributes, boolean.class));
}
if (design.hasAttr("hidable")) {
setHidable(DesignAttributeHandler.readAttribute("hidable",
attributes, boolean.class));
}
if (design.hasAttr("hidden")) {
setHidden(DesignAttributeHandler.readAttribute("hidden",
attributes, boolean.class));
}
if (design.hasAttr("hiding-toggle-caption")) {
setHidingToggleCaption(DesignAttributeHandler.readAttribute(
"hiding-toggle-caption", attributes, String.class));
}
// Read size info where necessary.
if (design.hasAttr("width")) {
setWidth(DesignAttributeHandler.readAttribute("width",
attributes, Double.class));
}
if (design.hasAttr("min-width")) {
setMinimumWidth(DesignAttributeHandler
.readAttribute("min-width", attributes, Double.class));
}
if (design.hasAttr("max-width")) {
setMaximumWidth(DesignAttributeHandler
.readAttribute("max-width", attributes, Double.class));
}
if (design.hasAttr("expand")) {
if (design.attr("expand").isEmpty()) {
setExpandRatio(1);
} else {
setExpandRatio(DesignAttributeHandler.readAttribute(
"expand", attributes, Integer.class));
}
}
}
}
/**
* An abstract base class for server-side {@link Renderer Grid renderers}.
* This class currently extends the AbstractExtension superclass, but this
* fact should be regarded as an implementation detail and subject to change
* in a future major or minor Vaadin revision.
*
* @param <T>
* the type this renderer knows how to present
*/
@Deprecated
public static abstract class AbstractRenderer<T>
extends AbstractGridExtension implements Renderer<T> {
private final Class<T> presentationType;
private final String nullRepresentation;
protected AbstractRenderer(Class<T> presentationType,
String nullRepresentation) {
this.presentationType = presentationType;
this.nullRepresentation = nullRepresentation;
}
protected AbstractRenderer(Class<T> presentationType) {
this(presentationType, null);
}
/**
* This method is inherited from AbstractExtension but should never be
* called directly with an AbstractRenderer.
*/
@Deprecated
@Override
protected Class<Grid> getSupportedParentType() {
return Grid.class;
}
/**
* This method is inherited from AbstractExtension but should never be
* called directly with an AbstractRenderer.
*/
@Deprecated
@Override
protected void extend(AbstractClientConnector target) {
super.extend(target);
}
@Override
public Class<T> getPresentationType() {
return presentationType;
}
@Override
public JsonValue encode(T value) {
if (value == null) {
return encode(getNullRepresentation(), String.class);
} else {
return encode(value, getPresentationType());
}
}
/**
* Null representation for the renderer
*
* @return a textual representation of {@code null}
*/
protected String getNullRepresentation() {
return nullRepresentation;
}
/**
* Encodes the given value to JSON.
* <p>
* 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<Date>} 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 <U> JsonValue encode(U value, Class<U> type) {
return JsonCodec
.encode(value, null, type, getUI().getConnectorTracker())
.getEncodedValue();
}
/**
* Converts and encodes the given data model property value using the
* given converter and renderer. This method is public only for testing
* purposes.
*
* @since 7.6
* @param renderer
* the renderer to use
* @param converter
* the converter to use
* @param modelValue
* the value to convert and encode
* @param locale
* the locale to use in conversion
* @return an encoded value ready to be sent to the client
*/
public static <T> JsonValue encodeValue(Object modelValue,
Renderer<T> renderer, Converter<?, ?> converter,
Locale locale) {
Class<T> presentationType = renderer.getPresentationType();
T presentationValue;
if (converter == null) {
try {
presentationValue = presentationType.cast(modelValue);
} catch (ClassCastException e) {
if (presentationType == String.class) {
// If there is no converter, just fallback to using
// toString(). modelValue can't be null as
// Class.cast(null) will always succeed
presentationValue = (T) modelValue.toString();
} else {
throw new Converter.ConversionException(
"Unable to convert value of type "
+ modelValue.getClass().getName()
+ " to presentation type "
+ presentationType.getName()
+ ". No converter is set and the types are not compatible.");
}
}
} else {
assert presentationType
.isAssignableFrom(converter.getPresentationType());
@SuppressWarnings("unchecked")
Converter<T, Object> safeConverter = (Converter<T, Object>) converter;
presentationValue = safeConverter.convertToPresentation(
modelValue, safeConverter.getPresentationType(),
locale);
}
JsonValue encodedValue;
try {
encodedValue = renderer.encode(presentationValue);
} catch (Exception e) {
getLogger().log(Level.SEVERE, "Unable to encode data", e);
encodedValue = renderer.encode(null);
}
return encodedValue;
}
private static Logger getLogger() {
return Logger.getLogger(AbstractRenderer.class.getName());
}
}
/**
* An abstract base class for server-side Grid extensions.
* <p>
* Note: If the extension is an instance of {@link DataGenerator} it will
* automatically register itself to {@link RpcDataProviderExtension} of
* extended Grid. On remove this registration is automatically removed.
*
* @since 7.5
*/
@Deprecated
public static abstract class AbstractGridExtension
extends AbstractExtension {
/**
* Constructs a new Grid extension.
*/
public AbstractGridExtension() {
super();
}
/**
* Constructs a new Grid extension and extends given Grid.
*
* @param grid
* a grid instance
*/
public AbstractGridExtension(Grid grid) {
super();
extend(grid);
}
@Override
protected void extend(AbstractClientConnector target) {
super.extend(target);
if (this instanceof DataGenerator) {
getParentGrid().datasourceExtension
.addDataGenerator((DataGenerator) this);
}
}
@Override
public void remove() {
if (this instanceof DataGenerator) {
getParentGrid().datasourceExtension
.removeDataGenerator((DataGenerator) this);
}
super.remove();
}
/**
* Gets the item id for a row key.
* <p>
* A key is used to identify a particular row on both a server and a
* client. This method can be used to get the item id for the row key
* that the client has sent.
*
* @param rowKey
* the row key for which to retrieve an item id
* @return the item id corresponding to {@code key}
*/
protected Object getItemId(String rowKey) {
return getParentGrid().getKeyMapper().get(rowKey);
}
/**
* Gets the column for a column id.
* <p>
* An id is used to identify a particular column on both a server and a
* client. This method can be used to get the column for the column id
* that the client has sent.
*
* @param columnId
* the column id for which to retrieve a column
* @return the column corresponding to {@code columnId}
*/
protected Column getColumn(String columnId) {
return getParentGrid().getColumnByColumnId(columnId);
}
/**
* Gets the parent Grid of the renderer.
*
* @return parent grid
* @throws IllegalStateException
* if parent is not Grid
*/
protected Grid getParentGrid() {
if (getParent() instanceof Grid) {
Grid grid = (Grid) getParent();
return grid;
} else if (getParent() == null) {
throw new IllegalStateException(
"Renderer is not attached to any parent");
} else {
throw new IllegalStateException(
"Renderers can be used only with Grid. Extended "
+ getParent().getClass().getSimpleName()
+ " instead");
}
}
/**
* Resends the row data for given item id to the client.
*
* @since 7.6
* @param itemId
* row to refresh
*/
protected void refreshRow(Object itemId) {
getParentGrid().datasourceExtension.updateRowData(itemId);
}
/**
* Informs the parent Grid that this Extension wants to add a child
* component to it.
*
* @since 7.6
* @param c
* component
*/
protected void addComponentToGrid(Component c) {
getParentGrid().addComponent(c);
}
/**
* Informs the parent Grid that this Extension wants to remove a child
* component from it.
*
* @since 7.6
* @param c
* component
*/
protected void removeComponentFromGrid(Component c) {
getParentGrid().removeComponent(c);
}
}
/**
* The data source attached to the grid
*/
private Container.Indexed datasource;
/**
* Property id to column instance mapping
*/
private final Map<Object, Column> columns = new HashMap<Object, Column>();
/**
* Key generator for column server-to-client communication
*/
private final KeyMapper<Object> columnKeys = new KeyMapper<Object>();
/**
* The current sort order
*/
private final List<SortOrder> sortOrder = new ArrayList<SortOrder>();
/**
* Property listener for listening to changes in data source properties.
*/
private final PropertySetChangeListener propertyListener = new PropertySetChangeListener() {
@Override
public void containerPropertySetChange(PropertySetChangeEvent event) {
Collection<?> properties = new HashSet<Object>(
event.getContainer().getContainerPropertyIds());
// Find columns that need to be removed.
List<Column> removedColumns = new LinkedList<Column>();
for (Object propertyId : columns.keySet()) {
if (!properties.contains(propertyId)) {
removedColumns.add(getColumn(propertyId));
}
}
// Actually remove columns.
for (Column column : removedColumns) {
Object propertyId = column.getPropertyId();
internalRemoveColumn(propertyId);
columnKeys.remove(propertyId);
}
datasourceExtension.columnsRemoved(removedColumns);
// Add new columns
List<Column> addedColumns = new LinkedList<Column>();
for (Object propertyId : properties) {
if (!columns.containsKey(propertyId)) {
addedColumns.add(appendColumn(propertyId));
}
}
datasourceExtension.columnsAdded(addedColumns);
if (getFrozenColumnCount() > columns.size()) {
setFrozenColumnCount(columns.size());
}
// Unset sortable for non-sortable columns.
if (datasource instanceof Sortable) {
Collection<?> sortables = ((Sortable) datasource)
.getSortableContainerPropertyIds();
for (Object propertyId : columns.keySet()) {
Column column = columns.get(propertyId);
if (!sortables.contains(propertyId)
&& column.isSortable()) {
column.setSortable(false);
}
}
}
}
};
private final ItemSetChangeListener editorClosingItemSetListener = new ItemSetChangeListener() {
@Override
public void containerItemSetChange(ItemSetChangeEvent event) {
cancelEditor();
}
};
private RpcDataProviderExtension datasourceExtension;
/**
* The selection model that is currently in use. Never <code>null</code>
* 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 boolean editorActive = false;
/**
* True while the editor is storing the field values, i.e. commiting the
* field group.
*/
private boolean editorSaving = false;
private FieldGroup editorFieldGroup = new CustomFieldGroup();
/**
* Poperty ID to Field mapping that stores editor fields set by
* {@link #setEditorField(Object, Field)}.
*/
private Map<Object, Field<?>> editorFields = new HashMap<Object, Field<?>>();
private CellStyleGenerator cellStyleGenerator;
private RowStyleGenerator rowStyleGenerator;
private CellDescriptionGenerator cellDescriptionGenerator;
private RowDescriptionGenerator rowDescriptionGenerator;
/**
* <code>true</code> if Grid is using the internal IndexedContainer created
* in Grid() constructor, or <code>false</code> if the user has set their
* own Container.
*
* @see #setContainerDataSource(Indexed)
* @see #Grid()
*/
private boolean defaultContainer = true;
private EditorErrorHandler editorErrorHandler = new DefaultEditorErrorHandler();
private DetailComponentManager detailComponentManager = null;
private Set<Component> extensionComponents = new HashSet<Component>();
private static final Method SELECTION_CHANGE_METHOD = ReflectTools
.findMethod(SelectionListener.class, "select",
SelectionEvent.class);
private static final Method SORT_ORDER_CHANGE_METHOD = ReflectTools
.findMethod(SortListener.class, "sort", SortEvent.class);
private static final Method COLUMN_REORDER_METHOD = ReflectTools.findMethod(
ColumnReorderListener.class, "columnReorder",
ColumnReorderEvent.class);
private static final Method COLUMN_RESIZE_METHOD = ReflectTools.findMethod(
ColumnResizeListener.class, "columnResize",
ColumnResizeEvent.class);
private static final Method COLUMN_VISIBILITY_METHOD = ReflectTools
.findMethod(ColumnVisibilityChangeListener.class,
"columnVisibilityChanged",
ColumnVisibilityChangeEvent.class);
/**
* Creates a new Grid with a new {@link IndexedContainer} as the data
* source.
*/
public Grid() {
this(null, null);
}
/**
* Creates a new Grid using the given data source.
*
* @param dataSource
* the indexed container to use as a data source
*/
public Grid(final Container.Indexed dataSource) {
this(null, dataSource);
}
/**
* Creates a new Grid with the given caption and a new
* {@link IndexedContainer} data source.
*
* @param caption
* the caption of the grid
*/
public Grid(String caption) {
this(caption, null);
}
/**
* Creates a new Grid with the given caption and data source. If the data
* source is null, a new {@link IndexedContainer} will be used.
*
* @param caption
* the caption of the grid
* @param dataSource
* the indexed container to use as a data source
*/
public Grid(String caption, Container.Indexed dataSource) {
if (dataSource == null) {
internalSetContainerDataSource(new IndexedContainer());
} else {
setContainerDataSource(dataSource);
}
setCaption(caption);
initGrid();
}
/**
* Grid initial setup
*/
private void initGrid() {
setSelectionMode(getDefaultSelectionMode());
registerRpc(new GridServerRpc() {
@Override
public void sort(String[] columnIds, SortDirection[] directions,
boolean userOriginated) {
assert columnIds.length == directions.length;
List<SortOrder> order = new ArrayList<SortOrder>(
columnIds.length);
for (int i = 0; i < columnIds.length; i++) {
Object propertyId = getPropertyIdByColumnId(columnIds[i]);
order.add(new SortOrder(propertyId, directions[i]));
}
setSortOrder(order, userOriginated);
if (!order.equals(getSortOrder())) {
/*
* Actual sort order is not what the client expects. Make
* sure the client gets a state change event by clearing the
* diffstate and marking as dirty
*/
ConnectorTracker connectorTracker = getUI()
.getConnectorTracker();
JsonObject diffState = connectorTracker
.getDiffState(Grid.this);
diffState.remove("sortColumns");
diffState.remove("sortDirs");
markAsDirty();
}
}
@Override
public void itemClick(String rowKey, String columnId,
MouseEventDetails details) {
Object itemId = getKeyMapper().get(rowKey);
Item item = datasource.getItem(itemId);
Object propertyId = getPropertyIdByColumnId(columnId);
fireEvent(new ItemClickEvent(Grid.this, item, itemId,
propertyId, details));
}
@Override
public void columnsReordered(List<String> newColumnOrder,
List<String> oldColumnOrder) {
final String diffStateKey = "columnOrder";
ConnectorTracker connectorTracker = getUI()
.getConnectorTracker();
JsonObject diffState = connectorTracker.getDiffState(Grid.this);
// discard the change if the columns have been reordered from
// the server side, as the server side is always right
if (getState(false).columnOrder.equals(oldColumnOrder)) {
// Don't mark as dirty since client has the state already
getState(false).columnOrder = newColumnOrder;
// write changes to diffState so that possible reverting the
// column order is sent to client
assert diffState
.hasKey(diffStateKey) : "Field name has changed";
Type type = null;
try {
type = (getState(false).getClass()
.getDeclaredField(diffStateKey)
.getGenericType());
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
}
EncodeResult encodeResult = JsonCodec.encode(
getState(false).columnOrder, diffState, type,
connectorTracker);
diffState.put(diffStateKey, encodeResult.getEncodedValue());
fireColumnReorderEvent(true);
} else {
// make sure the client is reverted to the order that the
// server thinks it is
diffState.remove(diffStateKey);
markAsDirty();
}
}
@Override
public void columnVisibilityChanged(String id, boolean hidden,
boolean userOriginated) {
final Column column = getColumnByColumnId(id);
final GridColumnState columnState = column.getState();
if (columnState.hidden != hidden) {
columnState.hidden = hidden;
final String diffStateKey = "columns";
ConnectorTracker connectorTracker = getUI()
.getConnectorTracker();
JsonObject diffState = connectorTracker
.getDiffState(Grid.this);
assert diffState
.hasKey(diffStateKey) : "Field name has changed";
Type type = null;
try {
type = (getState(false).getClass()
.getDeclaredField(diffStateKey)
.getGenericType());
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
}
EncodeResult encodeResult = JsonCodec.encode(
getState(false).columns, diffState, type,
connectorTracker);
diffState.put(diffStateKey, encodeResult.getEncodedValue());
fireColumnVisibilityChangeEvent(column, hidden,
userOriginated);
}
}
@Override
public void contextClick(int rowIndex, String rowKey,
String columnId, Section section,
MouseEventDetails details) {
Object itemId = null;
if (rowKey != null) {
itemId = getKeyMapper().get(rowKey);
}
fireEvent(new GridContextClickEvent(Grid.this, details, section,
rowIndex, itemId, getPropertyIdByColumnId(columnId)));
}
@Override
public void columnResized(String id, double pixels) {
final Column column = getColumnByColumnId(id);
if (column != null && column.isResizable()) {
column.getState().width = pixels;
fireColumnResizeEvent(column, true);
markAsDirty();
}
}
});
registerRpc(new EditorServerRpc() {
@Override
public void bind(int rowIndex) {
try {
Object id = getContainerDataSource().getIdByIndex(rowIndex);
final boolean opening = editedItemId == null;
final boolean moving = !opening && !editedItemId.equals(id);
final boolean allowMove = !isEditorBuffered()
&& getEditorFieldGroup().isValid();
if (opening || !moving || allowMove) {
doBind(id);
} else {
failBind(null);
}
} catch (Exception e) {
failBind(e);
}
}
private void doBind(Object id) {
editedItemId = id;
doEditItem();
getEditorRpc().confirmBind(true);
}
private void failBind(Exception e) {
if (e != null) {
handleError(e);
}
getEditorRpc().confirmBind(false);
}
@Override
public void cancel(int rowIndex) {
try {
// For future proofing even though cannot currently fail
doCancelEditor();
} catch (Exception e) {
handleError(e);
}
}
@Override
public void save(int rowIndex) {
List<String> errorColumnIds = null;
String errorMessage = null;
boolean success = false;
try {
saveEditor();
success = true;
} catch (CommitException e) {
try {
CommitErrorEvent event = new CommitErrorEvent(Grid.this,
e);
getEditorErrorHandler().commitError(event);
errorMessage = event.getUserErrorMessage();
errorColumnIds = new ArrayList<String>();
for (Column column : event.getErrorColumns()) {
errorColumnIds.add(column.state.id);
}
} catch (Exception ee) {
// A badly written error handler can throw an exception,
// which would lock up the Grid
handleError(ee);
}
} catch (Exception e) {
handleError(e);
}
getEditorRpc().confirmSave(success, errorMessage,
errorColumnIds);
}
private void handleError(Exception e) {
com.vaadin.server.ErrorEvent.findErrorHandler(Grid.this)
.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.
* <p>
*
* <strong>Note</strong> Grid columns are based on properties and try to
* detect a correct converter for the data type. The columns are not
* reinitialized automatically if the container is changed, and if the same
* properties are present after container change, the columns are reused.
* Properties with same names, but different data types will lead to
* unpredictable behaviour.
*
* @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);
}
// Remove old DetailComponentManager
if (detailComponentManager != null) {
detailComponentManager.remove();
}
resetEditor();
datasource = container;
//
// Adjust sort order
//
if (container instanceof Container.Sortable) {
// If the container is sortable, go through the current sort order
// and match each item to the sortable properties of the new
// container. If the new container does not support an item in the
// current sort order, that item is removed from the current sort
// order list.
Collection<?> sortableProps = ((Container.Sortable) getContainerDataSource())
.getSortableContainerPropertyIds();
Iterator<SortOrder> i = sortOrder.iterator();
while (i.hasNext()) {
if (!sortableProps.contains(i.next().getPropertyId())) {
i.remove();
}
}
sort(false);
} else {
// Clear sorting order. Don't sort.
sortOrder.clear();
}
datasourceExtension = new RpcDataProviderExtension(container);
datasourceExtension.extend(this);
datasourceExtension.addDataGenerator(new RowDataGenerator());
for (Extension e : getExtensions()) {
if (e instanceof DataGenerator) {
datasourceExtension.addDataGenerator((DataGenerator) e);
}
}
if (detailComponentManager != null) {
detailComponentManager = new DetailComponentManager(this,
detailComponentManager.getDetailsGenerator());
} else {
detailComponentManager = new DetailComponentManager(this);
}
/*
* selectionModel == null when the invocation comes from the
* constructor.
*/
if (selectionModel != null) {
selectionModel.reset();
}
// Listen to changes in properties and remove columns if needed
if (datasource instanceof PropertySetChangeNotifier) {
((PropertySetChangeNotifier) datasource)
.addPropertySetChangeListener(propertyListener);
}
/*
* activeRowHandler will be updated by the client-side request that
* occurs on container change - no need to actively re-insert any
* ValueChangeListeners at this point.
*/
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 {
column.setSortable(false);
}
}
} 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()
+ "\". "
+ "Call removeAllColumns() before setContainerDataSource() if you want to reconfigure the columns based on the new container.");
}
if (!(datasource instanceof Sortable)
|| !((Sortable) datasource)
.getSortableContainerPropertyIds()
.contains(property)) {
columns.get(property).setSortable(false);
}
}
}
}
/**
* Returns the grid data source.
*
* @return the container data source of the grid
*/
public Container.Indexed getContainerDataSource() {
return datasource;
}
/**
* Returns a column based on the property id
*
* @param propertyId
* the property id of the column
* @return the column or <code>null</code> if not found
*/
public 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<Column> getColumns() {
List<Column> columns = new ArrayList<Grid.Column>();
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.
* <p>
* 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 if (defaultContainer) {
addColumnProperty(propertyId, String.class, "");
} else {
if (columns.containsKey(propertyId)) {
throw new IllegalStateException(
"A column for property id '" + propertyId.toString()
+ "' already exists in this grid");
} else {
throw new IllegalStateException(
"Property id '" + propertyId.toString()
+ "' does not exist in the container");
}
}
// Inform the data provider of this new column.
Column column = getColumn(propertyId);
List<Column> addedColumns = new ArrayList<Column>();
addedColumns.add(column);
datasourceExtension.columnsAdded(addedColumns);
return column;
}
/**
* 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.
* <p>
* 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.
* <p>
* 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() {
List<Column> removed = new ArrayList<Column>(columns.values());
Set<Object> properties = new HashSet<Object>(columns.keySet());
for (Object propertyId : properties) {
removeColumn(propertyId);
}
datasourceExtension.columnsRemoved(removed);
}
/**
* 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 <code>null</code> 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);
}
/**
* Returns whether column reordering is allowed. Default value is
* <code>false</code>.
*
* @since 7.5.0
* @return true if reordering is allowed
*/
public boolean isColumnReorderingAllowed() {
return getState(false).columnReorderingAllowed;
}
/**
* Sets whether or not column reordering is allowed. Default value is
* <code>false</code>.
*
* @since 7.5.0
* @param columnReorderingAllowed
* specifies whether column reordering is allowed
*/
public void setColumnReorderingAllowed(boolean columnReorderingAllowed) {
if (isColumnReorderingAllowed() != columnReorderingAllowed) {
getState().columnReorderingAllowed = columnReorderingAllowed;
}
}
@Override
protected GridState getState() {
return (GridState) super.getState();
}
@Override
protected GridState getState(boolean markAsDirty) {
return (GridState) super.getState(markAsDirty);
}
/**
* Sets the column resize mode to use. The default mode is
* {@link ColumnResizeMode#ANIMATED}.
*
* @param mode
* a ColumnResizeMode value
* @since 7.7.5
*/
public void setColumnResizeMode(ColumnResizeMode mode) {
getState().columnResizeMode = mode;
}
/**
* Returns the current column resize mode. The default mode is
* {@link ColumnResizeMode#ANIMATED}.
*
* @return a ColumnResizeMode value
* @since 7.7.5
*/
public ColumnResizeMode getColumnResizeMode() {
return getState(false).columnResizeMode;
}
/**
* 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);
String humanFriendlyPropertyId = SharedUtil.propertyIdToHumanFriendly(
String.valueOf(datasourcePropertyId));
column.setHeaderCaption(humanFriendlyPropertyId);
if (datasource instanceof Sortable
&& ((Sortable) datasource).getSortableContainerPropertyIds()
.contains(datasourcePropertyId)) {
column.setSortable(true);
}
return column;
}
/**
* Removes a column from Grid based on a property id.
*
* @param propertyId
* The property id of column to be removed
*
* @throws IllegalArgumentException
* if there is no column for given property id in this grid
*/
public void removeColumn(Object propertyId)
throws IllegalArgumentException {
if (!columns.keySet().contains(propertyId)) {
throw new IllegalArgumentException(
"There is no column for given property id " + propertyId);
}
List<Column> removed = new ArrayList<Column>();
removed.add(getColumn(propertyId));
internalRemoveColumn(propertyId);
datasourceExtension.columnsRemoved(removed);
}
private void internalRemoveColumn(Object propertyId) {
setEditorField(propertyId, null);
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 the columns and their order for the grid. Current columns whose
* property id is not in propertyIds are removed. Similarly, a column is
* added for any property id in propertyIds that has no corresponding column
* in this Grid.
*
* @since 7.5.0
*
* @param propertyIds
* properties in the desired column order
*/
public void setColumns(Object... propertyIds) {
if (SharedUtil.containsDuplicates(propertyIds)) {
throw new IllegalArgumentException(
"The propertyIds array contains duplicates: "
+ SharedUtil.getDuplicates(propertyIds));
}
Set<?> removePids = new HashSet<Object>(columns.keySet());
removePids.removeAll(Arrays.asList(propertyIds));
for (Object removePid : removePids) {
removeColumn(removePid);
}
Set<?> addPids = new HashSet<Object>(Arrays.asList(propertyIds));
addPids.removeAll(columns.keySet());
for (Object propertyId : addPids) {
addColumn(propertyId);
}
setColumnOrder(propertyIds);
}
/**
* 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) {
if (SharedUtil.containsDuplicates(propertyIds)) {
throw new IllegalArgumentException(
"The propertyIds array contains duplicates: "
+ SharedUtil.getDuplicates(propertyIds));
}
List<String> columnOrder = new ArrayList<String>();
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<String> stateColumnOrder = getState().columnOrder;
if (stateColumnOrder.size() != columnOrder.size()) {
stateColumnOrder.removeAll(columnOrder);
columnOrder.addAll(stateColumnOrder);
}
getState().columnOrder = columnOrder;
fireColumnReorderEvent(false);
}
/**
* 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.
* <p>
* 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.size() + "): " + numberOfColumns);
}
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.
* <p>
* <em>NOTE:</em> this count includes {@link Column#isHidden() hidden
* columns} in the count.
*
* @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}.
* <p>
* If the item has visible details, its size will also be taken into
* account.
*
* @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.
* <p>
* If the item has visible details, its size will also be taken into
* account.
*
* @param itemId
* id of item to scroll to.
* @param destination
* value specifying desired position of scrolled-to row.
* @throws IllegalArgumentException
* if the provided id is not recognized by the data source.
*/
public void scrollTo(Object itemId, ScrollDestination destination)
throws IllegalArgumentException {
int row = datasource.indexOfId(itemId);
if (row == -1) {
throw new IllegalArgumentException(
"Item with specified ID does not exist in data source");
}
GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class);
clientRPC.scrollToRow(row, destination);
}
/**
* Scrolls to the beginning of the first data row.
*/
public void scrollToStart() {
GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class);
clientRPC.scrollToStart();
}
/**
* Scrolls to the end of the last data row.
*/
public void scrollToEnd() {
GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class);
clientRPC.scrollToEnd();
}
/**
* Sets the number of rows that should be visible in Grid's body, while
* {@link #getHeightMode()} is {@link HeightMode#ROW}.
* <p>
* If Grid is currently not in {@link HeightMode#ROW}, the given value is
* remembered, and applied once the mode is applied.
*
* @param rows
* The height in terms of number of rows displayed in Grid's
* body. If Grid doesn't contain enough rows, white space is
* displayed instead. If <code>null</code> is given, then Grid's
* height is undefined
* @throws IllegalArgumentException
* if {@code rows} is zero or less
* @throws IllegalArgumentException
* if {@code rows} is {@link Double#isInfinite(double) infinite}
* @throws IllegalArgumentException
* if {@code rows} is {@link Double#isNaN(double) NaN}
*/
public void setHeightByRows(double rows) {
if (rows <= 0.0d) {
throw new IllegalArgumentException(
"More than zero rows must be shown.");
} else if (Double.isInfinite(rows)) {
throw new IllegalArgumentException(
"Grid doesn't support infinite heights");
} else if (Double.isNaN(rows)) {
throw new IllegalArgumentException("NaN is not a valid row count");
}
getState().heightByRows = rows;
}
/**
* Gets the amount of rows in Grid's body that are shown, while
* {@link #getHeightMode()} is {@link HeightMode#ROW}.
*
* @return the amount of rows that are being shown in Grid's body
* @see #setHeightByRows(double)
*/
public double getHeightByRows() {
return getState(false).heightByRows;
}
/**
* {@inheritDoc}
* <p>
* <em>Note:</em> This method will change the widget's size in the browser
* only if {@link #getHeightMode()} returns {@link HeightMode#CSS}.
*
* @see #setHeightMode(HeightMode)
*/
@Override
public void setHeight(float height, Unit unit) {
super.setHeight(height, unit);
}
/**
* Defines the mode in which the Grid widget's height is calculated.
* <p>
* If {@link HeightMode#CSS} is given, Grid will respect the values given
* via a {@code setHeight}-method, and behave as a traditional Component.
* <p>
* If {@link HeightMode#ROW} is given, Grid will make sure that the body
* will display as many rows as {@link #getHeightByRows()} defines.
* <em>Note:</em> If headers/footers are inserted or removed, the widget
* will resize itself to still display the required amount of rows in its
* body. It also takes the horizontal scrollbar into account.
*
* @param heightMode
* the mode in to which Grid should be set
*/
public void setHeightMode(HeightMode heightMode) {
/*
* This method is a workaround for the fact that Vaadin re-applies
* widget dimensions (height/width) on each state change event. The
* original design was to have setHeight and setHeightByRow be equals,
* and whichever was called the latest was considered in effect.
*
* But, because of Vaadin always calling setHeight on the widget, this
* approach doesn't work.
*/
getState().heightMode = heightMode;
}
/**
* Returns the current {@link HeightMode} the Grid is in.
* <p>
* Defaults to {@link HeightMode#CSS}.
*
* @return the current HeightMode
*/
public HeightMode getHeightMode() {
return getState(false).heightMode;
}
/* Selection related methods: */
/**
* Takes a new {@link SelectionModel} into use.
* <p>
* The SelectionModel that is previously in use will have all its items
* deselected. If any items were selected, this will fire a
* {@link SelectionEvent}.
* <p>
* If the given SelectionModel is already in use, this method does nothing.
*
* @param selectionModel
* the new SelectionModel to use
* @throws IllegalArgumentException
* if {@code selectionModel} is <code>null</code>
*/
public void setSelectionModel(SelectionModel selectionModel)
throws IllegalArgumentException {
if (selectionModel == null) {
throw new IllegalArgumentException(
"Selection model may not be null");
}
if (this.selectionModel != selectionModel) {
Collection<Object> oldSelection;
// this.selectionModel is null on init
if (this.selectionModel != null) {
oldSelection = this.selectionModel.getSelectedRows();
this.selectionModel.remove();
} else {
oldSelection = Collections.emptyList();
}
this.selectionModel = selectionModel;
selectionModel.setGrid(this);
Collection<Object> newSelection = this.selectionModel
.getSelectedRows();
if (!SharedUtil.equals(oldSelection, newSelection)) {
fireSelectionEvent(oldSelection, newSelection);
}
// selection is included in the row data, so the client needs to be
// updated
datasourceExtension.refreshCache();
}
}
/**
* Returns the currently used {@link SelectionModel}.
*
* @return the currently used SelectionModel
*/
public SelectionModel getSelectionModel() {
return selectionModel;
}
/**
* Sets the Grid's selection mode.
* <p>
* Grid supports three selection modes: multiselect, single select and no
* selection, and this is a convenience method for choosing between one of
* them.
* <p>
* Technically, this method is a shortcut that can be used instead of
* calling {@code setSelectionModel} with a specific SelectionModel
* instance. Grid comes with three built-in SelectionModel classes, and the
* {@link SelectionMode} enum represents each of them.
* <p>
* Essentially, the two following method calls are equivalent:
* <p>
* <code><pre>
* grid.setSelectionMode(SelectionMode.MULTI);
* grid.setSelectionModel(new MultiSelectionMode());
* </pre></code>
*
*
* @param selectionMode
* the selection mode to switch to
* @return The {@link SelectionModel} instance that was taken into use
* @throws IllegalArgumentException
* if {@code selectionMode} is <code>null</code>
* @see SelectionModel
*/
public SelectionModel setSelectionMode(final SelectionMode selectionMode)
throws IllegalArgumentException {
if (selectionMode == null) {
throw new IllegalArgumentException(
"selection mode may not be null");
}
final SelectionModel newSelectionModel = selectionMode.createModel();
setSelectionModel(newSelectionModel);
return newSelectionModel;
}
/**
* Checks whether an item is selected or not.
*
* @param itemId
* the item id to check for
* @return <code>true</code> iff the item is selected
*/
// keep this javadoc in sync with SelectionModel.isSelected
public boolean isSelected(Object itemId) {
return selectionModel.isSelected(itemId);
}
/**
* Returns a collection of all the currently selected itemIds.
* <p>
* This method is a shorthand that delegates to the
* {@link #getSelectionModel() selection model}.
*
* @return a collection of all the currently selected itemIds
*/
// keep this javadoc in sync with SelectionModel.getSelectedRows
public Collection<Object> getSelectedRows() {
return getSelectionModel().getSelectedRows();
}
/**
* Gets the item id of the currently selected item.
* <p>
* This method is a shorthand that delegates to the
* {@link #getSelectionModel() selection model}. Only
* {@link SelectionModel.Single} is supported.
*
* @return the item id of the currently selected item, or <code>null</code>
* if nothing is selected
* @throws IllegalStateException
* if the selection model does not implement
* {@code 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 if (selectionModel instanceof SelectionModel.Multi) {
throw new IllegalStateException("Cannot get unique selected row: "
+ "Grid is in multiselect mode "
+ "(the current selection model is "
+ selectionModel.getClass().getName() + ").");
} else if (selectionModel instanceof SelectionModel.None) {
throw new IllegalStateException(
"Cannot get selected row: " + "Grid selection is disabled "
+ "(the current selection model is "
+ selectionModel.getClass().getName() + ").");
} else {
throw new IllegalStateException("Cannot get selected row: "
+ "Grid selection model does not implement "
+ SelectionModel.Single.class.getName() + " or "
+ SelectionModel.Multi.class.getName()
+ "(the current model is "
+ selectionModel.getClass().getName() + ").");
}
}
/**
* Marks an item as selected.
* <p>
* This method is a shorthand that delegates to the
* {@link #getSelectionModel() selection model}. Only
* {@link SelectionModel.Single} and {@link SelectionModel.Multi} are
* supported.
*
* @param itemId
* the itemId to mark as selected
* @return <code>true</code> if the selection state changed,
* <code>false</code> if the itemId already was selected
* @throws IllegalArgumentException
* if the {@code itemId} doesn't exist in the currently active
* Container
* @throws IllegalStateException
* if the selection was illegal. One such reason might be that
* the implementation already had an item selected, and that
* needs to be explicitly deselected before re-selecting
* something.
* @throws IllegalStateException
* if the selection model does not implement
* {@code SelectionModel.Single} or {@code 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 if (selectionModel instanceof SelectionModel.None) {
throw new IllegalStateException("Cannot select row '" + itemId
+ "': Grid selection is disabled "
+ "(the current selection model is "
+ selectionModel.getClass().getName() + ").");
} else {
throw new IllegalStateException("Cannot select row '" + itemId
+ "': Grid selection model does not implement "
+ SelectionModel.Single.class.getName() + " or "
+ SelectionModel.Multi.class.getName()
+ "(the current model is "
+ selectionModel.getClass().getName() + ").");
}
}
/**
* Marks an item as unselected.
* <p>
* This method is a shorthand that delegates to the
* {@link #getSelectionModel() selection model}. Only
* {@link SelectionModel.Single} and {@link SelectionModel.Multi} are
* supported.
*
* @param itemId
* the itemId to remove from being selected
* @return <code>true</code> if the selection state changed,
* <code>false</code> if the itemId was already 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 requires one or more items to be selected
* at all times.
* @throws IllegalStateException
* if the selection model does not implement
* {@code SelectionModel.Single} or {code 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 if (selectionModel instanceof SelectionModel.None) {
throw new IllegalStateException("Cannot deselect row '" + itemId
+ "': Grid selection is disabled "
+ "(the current selection model is "
+ selectionModel.getClass().getName() + ").");
} else {
throw new IllegalStateException("Cannot deselect row '" + itemId
+ "': Grid selection model does not implement "
+ SelectionModel.Single.class.getName() + " or "
+ SelectionModel.Multi.class.getName()
+ "(the current model is "
+ selectionModel.getClass().getName() + ").");
}
}
/**
* Marks all items as unselected.
* <p>
* This method is a shorthand that delegates to the
* {@link #getSelectionModel() selection model}. Only
* {@link SelectionModel.Single} and {@link SelectionModel.Multi} are
* supported.
*
* @return <code>true</code> if the selection state changed,
* <code>false</code> if the itemId was already selected
* @throws IllegalStateException
* if the deselection was illegal. One such reason might be that
* the implementation requires one or more items to be selected
* at all times.
* @throws IllegalStateException
* if the selection model does not implement
* {@code SelectionModel.Single} or {code SelectionModel.Multi}
*/
public boolean deselectAll() throws IllegalStateException {
if (selectionModel instanceof SelectionModel.Single) {
if (getSelectedRow() != null) {
return deselect(getSelectedRow());
}
return false;
} else if (selectionModel instanceof SelectionModel.Multi) {
return ((SelectionModel.Multi) selectionModel).deselectAll();
} else if (selectionModel instanceof SelectionModel.None) {
throw new IllegalStateException(
"Cannot deselect all rows" + ": Grid selection is disabled "
+ "(the current selection model is "
+ selectionModel.getClass().getName() + ").");
} else {
throw new IllegalStateException("Cannot deselect all rows:"
+ " Grid selection model does not implement "
+ SelectionModel.Single.class.getName() + " or "
+ SelectionModel.Multi.class.getName()
+ "(the current model is "
+ selectionModel.getClass().getName() + ").");
}
}
/**
* Fires a selection change event.
* <p>
* <strong>Note:</strong> This is not a method that should be called by
* application logic. This method is publicly accessible only so that
* {@link SelectionModel SelectionModels} would be able to inform Grid of
* these events.
*
* @param newSelection
* the selection that was added by this event
* @param oldSelection
* the selection that was removed by this event
*/
public void fireSelectionEvent(Collection<Object> oldSelection,
Collection<Object> newSelection) {
fireEvent(new SelectionEvent(this, oldSelection, newSelection));
}
@Override
public void addSelectionListener(SelectionListener listener) {
addListener(SelectionEvent.class, listener, SELECTION_CHANGE_METHOD);
}
@Override
public void removeSelectionListener(SelectionListener listener) {
removeListener(SelectionEvent.class, listener, SELECTION_CHANGE_METHOD);
}
private void fireColumnReorderEvent(boolean userOriginated) {
fireEvent(new ColumnReorderEvent(this, userOriginated));
}
/**
* Registers a new column reorder listener.
*
* @since 7.5.0
* @param listener
* the listener to register
*/
public void addColumnReorderListener(ColumnReorderListener listener) {
addListener(ColumnReorderEvent.class, listener, COLUMN_REORDER_METHOD);
}
/**
* Removes a previously registered column reorder listener.
*
* @since 7.5.0
* @param listener
* the listener to remove
*/
public void removeColumnReorderListener(ColumnReorderListener listener) {
removeListener(ColumnReorderEvent.class, listener,
COLUMN_REORDER_METHOD);
}
private void fireColumnResizeEvent(Column column, boolean userOriginated) {
fireEvent(new ColumnResizeEvent(this, column, userOriginated));
}
/**
* Registers a new column resize listener.
*
* @param listener
* the listener to register
*/
public void addColumnResizeListener(ColumnResizeListener listener) {
addListener(ColumnResizeEvent.class, listener, COLUMN_RESIZE_METHOD);
}
/**
* Removes a previously registered column resize listener.
*
* @param listener
* the listener to remove
*/
public void removeColumnResizeListener(ColumnResizeListener listener) {
removeListener(ColumnResizeEvent.class, listener, COLUMN_RESIZE_METHOD);
}
/**
* Gets the {@link KeyMapper } being used by the data source.
*
* @return the key mapper being used by the data source
*/
KeyMapper<Object> 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.
* <p>
* <em>Note:</em> Sorting by a property that has no column in Grid will hide
* all possible sorting indicators.
*
* @param s
* a sort instance
*
* @throws IllegalStateException
* if container is not sortable (does not implement
* Container.Sortable)
* @throws IllegalArgumentException
* if trying to sort by non-existing property
*/
public void sort(Sort s) {
setSortOrder(s.build());
}
/**
* Sort this Grid in ascending order by a specified property.
* <p>
* <em>Note:</em> Sorting by a property that has no column in Grid will hide
* all possible sorting indicators.
*
* @param propertyId
* a property ID
*
* @throws IllegalStateException
* if container is not sortable (does not implement
* Container.Sortable)
* @throws IllegalArgumentException
* if trying to sort by non-existing property
*/
public void sort(Object propertyId) {
sort(propertyId, SortDirection.ASCENDING);
}
/**
* Sort this Grid in user-specified {@link SortOrder} by a property.
* <p>
* <em>Note:</em> Sorting by a property that has no column in Grid will hide
* all possible sorting indicators.
*
* @param propertyId
* a property ID
* @param direction
* a sort order value (ascending/descending)
*
* @throws IllegalStateException
* if container is not sortable (does not implement
* Container.Sortable)
* @throws IllegalArgumentException
* if trying to sort by non-existing property
*/
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.
* <p>
* <em>Note:</em> Sorting by a property that has no column in Grid will hide
* all possible sorting indicators.
*
* @param order
* a sort order list.
*
* @throws IllegalStateException
* if container is not sortable (does not implement
* Container.Sortable)
* @throws IllegalArgumentException
* if order is null or trying to sort by non-existing property
*/
public void setSortOrder(List<SortOrder> order) {
setSortOrder(order, false);
}
private void setSortOrder(List<SortOrder> order, boolean userOriginated)
throws IllegalStateException, IllegalArgumentException {
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(userOriginated);
}
/**
* Get the current sort order list.
*
* @return a sort order list
*/
public List<SortOrder> getSortOrder() {
return Collections.unmodifiableList(sortOrder);
}
/**
* Apply sorting to data source.
*/
private void sort(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];
SortDirection[] stateDirs = new SortDirection[items];
for (int i = 0; i < items; ++i) {
SortOrder order = sortOrder.get(i);
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);
if (columns.keySet().containsAll(Arrays.asList(propertyIds))) {
String[] columnKeys = new String[items];
for (int i = 0; i < items; ++i) {
columnKeys[i] = this.columnKeys.key(propertyIds[i]);
}
getState().sortColumns = columnKeys;
getState(false).sortDirs = stateDirs;
} else {
// Not all sorted properties are in Grid. Remove any indicators.
getState().sortColumns = new String[] {};
getState(false).sortDirs = new SortDirection[] {};
}
fireEvent(new SortEvent(this, new ArrayList<SortOrder>(sortOrder),
userOriginated));
} 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 Registration addSortListener(SortListener listener) {
addListener(SortEvent.class, listener, SORT_ORDER_CHANGE_METHOD);
return () -> removeListener(SortEvent.class, listener,
SORT_ORDER_CHANGE_METHOD);
}
/**
* Removes a sort order change listener previously added using
* {@link #addSortListener(SortListener)}.
*
* @param listener
* the sort order change listener to remove
*/
@Override
public void removeSortListener(SortListener listener) {
removeListener(SortEvent.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 rowIndex
* 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 rowIndex
* 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();
}
private void addComponent(Component c) {
extensionComponents.add(c);
c.setParent(this);
markAsDirty();
}
private void removeComponent(Component c) {
extensionComponents.remove(c);
c.setParent(null);
markAsDirty();
}
@Override
public Iterator<Component> iterator() {
// This is a hash set to avoid adding header/footer components inside
// merged cells multiple times
LinkedHashSet<Component> componentList = new LinkedHashSet<Component>();
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(getEditorFields());
componentList.addAll(extensionComponents);
return componentList.iterator();
}
@Override
public boolean isRendered(Component childComponent) {
if (getEditorFields().contains(childComponent)) {
// Only render editor fields if the editor is open
return isEditorActive();
} else {
// TODO Header and footer components should also only be rendered if
// the header/footer is visible
return true;
}
}
EditorClientRpc getEditorRpc() {
return getRpcProxy(EditorClientRpc.class);
}
/**
* Sets the {@code CellDescriptionGenerator} instance for generating
* optional descriptions (tooltips) for individual Grid cells. If a
* {@link RowDescriptionGenerator} is also set, the row description it
* generates is displayed for cells for which {@code generator} returns
* null.
*
* @param generator
* the description generator to use or {@code null} to remove a
* previously set generator if any
*
* @see #setRowDescriptionGenerator(RowDescriptionGenerator)
*
* @since 7.6
*/
public void setCellDescriptionGenerator(
CellDescriptionGenerator generator) {
cellDescriptionGenerator = generator;
getState().hasDescriptions = (generator != null
|| rowDescriptionGenerator != null);
datasourceExtension.refreshCache();
}
/**
* Returns the {@code CellDescriptionGenerator} instance used to generate
* descriptions (tooltips) for Grid cells.
*
* @return the description generator or {@code null} if no generator is set
*
* @since 7.6
*/
public CellDescriptionGenerator getCellDescriptionGenerator() {
return cellDescriptionGenerator;
}
/**
* Sets the {@code RowDescriptionGenerator} instance for generating optional
* descriptions (tooltips) for Grid rows. If a
* {@link CellDescriptionGenerator} is also set, the row description
* generated by {@code generator} is used for cells for which the cell
* description generator returns null.
*
*
* @param generator
* the description generator to use or {@code null} to remove a
* previously set generator if any
*
* @see #setCellDescriptionGenerator(CellDescriptionGenerator)
*
* @since 7.6
*/
public void setRowDescriptionGenerator(RowDescriptionGenerator generator) {
rowDescriptionGenerator = generator;
getState().hasDescriptions = (generator != null
|| cellDescriptionGenerator != null);
datasourceExtension.refreshCache();
}
/**
* Returns the {@code RowDescriptionGenerator} instance used to generate
* descriptions (tooltips) for Grid rows
*
* @return the description generator or {@code} null if no generator is set
*
* @since 7.6
*/
public RowDescriptionGenerator getRowDescriptionGenerator() {
return rowDescriptionGenerator;
}
/**
* Sets the style generator that is used for generating styles for cells
*
* @param cellStyleGenerator
* the cell style generator to set, or <code>null</code> to
* remove a previously set generator
*/
public void setCellStyleGenerator(CellStyleGenerator cellStyleGenerator) {
this.cellStyleGenerator = cellStyleGenerator;
datasourceExtension.refreshCache();
}
/**
* Gets the style generator that is used for generating styles for cells
*
* @return the cell style generator, or <code>null</code> if no generator is
* set
*/
public CellStyleGenerator getCellStyleGenerator() {
return cellStyleGenerator;
}
/**
* Sets the style generator that is used for generating styles for rows
*
* @param rowStyleGenerator
* the row style generator to set, or <code>null</code> to remove
* a previously set generator
*/
public void setRowStyleGenerator(RowStyleGenerator rowStyleGenerator) {
this.rowStyleGenerator = rowStyleGenerator;
datasourceExtension.refreshCache();
}
/**
* Gets the style generator that is used for generating styles for rows
*
* @return the row style generator, or <code>null</code> if no generator is
* set
*/
public RowStyleGenerator getRowStyleGenerator() {
return rowStyleGenerator;
}
/**
* Adds a row to the underlying container. The order of the parameters
* should match the current visible column order.
* <p>
* 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 <code>null</code>.
* @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<String> 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<Object> 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;
}
/**
* Refreshes, i.e. causes the client side to re-render the rows with the
* given item ids.
* <p>
* Calling this for a row which is not currently rendered on the client side
* has no effect.
*
* @param itemIds
* the item id(s) of the row to refresh.
*/
public void refreshRows(Object... itemIds) {
for (Object itemId : itemIds) {
datasourceExtension.updateRowData(itemId);
}
}
/**
* Refreshes, i.e. causes the client side to re-render all rows.
*
* @since 7.7.7
*/
public void refreshAllRows() {
datasourceExtension.refreshCache();
}
private static Logger getLogger() {
return Logger.getLogger(Grid.class.getName());
}
/**
* Sets whether or not the item editor UI is enabled for this grid. When the
* editor is enabled, the user can open it by double-clicking a row or
* hitting enter when a row is focused. The editor can also be opened
* programmatically using the {@link #editItem(Object)} method.
*
* @param isEnabled
* <code>true</code> to enable the feature, <code>false</code>
* otherwise
* @throws IllegalStateException
* if an item is currently being edited
*
* @see #getEditedItemId()
*/
public void setEditorEnabled(boolean isEnabled)
throws IllegalStateException {
if (isEditorActive()) {
throw new IllegalStateException(
"Cannot disable the editor while an item ("
+ getEditedItemId() + ") is being edited");
}
if (isEditorEnabled() != isEnabled) {
getState().editorEnabled = isEnabled;
}
}
/**
* Checks whether the item editor UI is enabled for this grid.
*
* @return <code>true</code> iff the editor is enabled for this grid
*
* @see #setEditorEnabled(boolean)
* @see #getEditedItemId()
*/
public boolean isEditorEnabled() {
return getState(false).editorEnabled;
}
/**
* Gets the id of the item that is currently being edited.
*
* @return the id of the item that is currently being edited, or
* <code>null</code> if no item is being edited at the moment
*/
public Object getEditedItemId() {
return editedItemId;
}
/**
* Gets the field group that is backing the item editor of this grid.
*
* @return the backing field group
*/
public FieldGroup getEditorFieldGroup() {
return editorFieldGroup;
}
/**
* Sets the field group that is backing the item editor of this grid.
*
* @param fieldGroup
* the backing field group
*
* @throws IllegalStateException
* if the editor is currently active
*/
public void setEditorFieldGroup(FieldGroup fieldGroup) {
if (isEditorActive()) {
throw new IllegalStateException(
"Cannot change field group while an item ("
+ getEditedItemId() + ") is being edited");
}
editorFieldGroup = fieldGroup;
}
/**
* Returns whether an item is currently being edited in the editor.
*
* @return true iff the editor is open
*/
public boolean isEditorActive() {
return editorActive;
}
private void checkColumnExists(Object propertyId) {
if (getColumn(propertyId) == null) {
throw new IllegalArgumentException(
"There is no column with the property id " + propertyId);
}
}
private Field<?> getEditorField(Object propertyId) {
checkColumnExists(propertyId);
if (!getColumn(propertyId).isEditable()) {
return null;
}
Field<?> editor = editorFieldGroup.getField(propertyId);
// If field group has no field for this property, see if we have it
// stored
if (editor == null) {
editor = editorFields.get(propertyId);
if (editor != null) {
editorFieldGroup.bind(editor, propertyId);
}
}
// Otherwise try to build one
try {
if (editor == null) {
editor = editorFieldGroup.buildAndBind(propertyId);
}
} finally {
if (editor == null) {
editor = editorFieldGroup.getField(propertyId);
}
if (editor != null && editor.getParent() != Grid.this) {
assert editor.getParent() == null;
editor.setParent(this);
}
}
return editor;
}
/**
* Opens the editor interface for the provided item. Scrolls the Grid to
* bring the item to view if it is not already visible.
*
* Note that any cell content rendered by a WidgetRenderer will not be
* visible in the editor row.
*
* @param itemId
* the id of the item to edit
* @throws IllegalStateException
* if the editor is not enabled or already editing an item in
* buffered mode
* @throws IllegalArgumentException
* if the {@code itemId} is not in the backing container
* @see #setEditorEnabled(boolean)
*/
public void editItem(Object itemId)
throws IllegalStateException, IllegalArgumentException {
if (!isEditorEnabled()) {
throw new IllegalStateException("Item editor is not enabled");
} else if (isEditorBuffered() && editedItemId != null) {
throw new IllegalStateException("Editing item " + itemId
+ " failed. Item editor is already editing item "
+ editedItemId);
} else if (!getContainerDataSource().containsId(itemId)) {
throw new IllegalArgumentException("Item with id " + itemId
+ " not found in current container");
}
editedItemId = itemId;
getEditorRpc().bind(getContainerDataSource().indexOfId(itemId));
}
protected void doEditItem() {
Item item = getContainerDataSource().getItem(editedItemId);
editorFieldGroup.setItemDataSource(item);
for (Column column : getColumns()) {
column.getState().editorConnector = item
.getItemProperty(column.getPropertyId()) == null ? null
: getEditorField(column.getPropertyId());
}
editorActive = true;
// Must ensure that all fields, recursively, are sent to the client
// This is needed because the fields are hidden using isRendered
for (Field<?> f : getEditorFields()) {
f.markAsDirtyRecursive();
}
if (datasource instanceof ItemSetChangeNotifier) {
((ItemSetChangeNotifier) datasource)
.addItemSetChangeListener(editorClosingItemSetListener);
}
}
private void setEditorField(Object propertyId, Field<?> field) {
checkColumnExists(propertyId);
Field<?> oldField = editorFieldGroup.getField(propertyId);
if (oldField != null) {
editorFieldGroup.unbind(oldField);
oldField.setParent(null);
}
if (field != null) {
field.setParent(this);
editorFieldGroup.bind(field, propertyId);
}
// Store field for this property for future reference
editorFields.put(propertyId, field);
}
/**
* Saves all changes done to the bound fields.
* <p>
* <em>Note:</em> This is a pass-through call to the backing field group.
*
* @throws CommitException
* If the commit was aborted
*
* @see FieldGroup#commit()
*/
public void saveEditor() throws CommitException {
try {
editorSaving = true;
editorFieldGroup.commit();
} finally {
editorSaving = false;
}
}
/**
* Cancels the currently active edit if any. Hides the editor and discards
* possible unsaved changes in the editor fields.
*/
public void cancelEditor() {
if (editorSaving) {
// If the editor is already saving the values, it's too late to
// cancel it. This prevents item set changes from propagating during
// save, causing discard to be run during commit.
return;
}
if (isEditorActive()) {
getEditorRpc()
.cancel(getContainerDataSource().indexOfId(editedItemId));
doCancelEditor();
}
}
protected void doCancelEditor() {
editedItemId = null;
editorActive = false;
editorFieldGroup.discard();
editorFieldGroup.setItemDataSource(null);
if (datasource instanceof ItemSetChangeNotifier) {
((ItemSetChangeNotifier) datasource)
.removeItemSetChangeListener(editorClosingItemSetListener);
}
// Mark Grid as dirty so the client side gets to know that the editors
// are no longer attached
markAsDirty();
}
void resetEditor() {
if (isEditorActive()) {
/*
* Simply force cancel the editing; throwing here would just make
* Grid.setContainerDataSource semantics more complicated.
*/
cancelEditor();
}
for (Field<?> editor : getEditorFields()) {
editor.setParent(null);
}
editedItemId = null;
editorActive = false;
editorFieldGroup = new CustomFieldGroup();
}
/**
* Gets a collection of all fields bound to the item editor of this grid.
* <p>
* 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 the item editor
*/
Collection<Field<?>> getEditorFields() {
Collection<Field<?>> fields = editorFieldGroup.getFields();
assert allAttached(fields);
return fields;
}
private boolean allAttached(Collection<? extends Component> components) {
for (Component component : components) {
if (component.getParent() != this) {
return false;
}
}
return true;
}
/**
* Sets the field factory for the {@link FieldGroup}. The field factory is
* only used when {@link FieldGroup} creates a new field.
* <p>
* <em>Note:</em> This is a pass-through call to the backing field group.
*
* @param fieldFactory
* The field factory to use
*/
public void setEditorFieldFactory(FieldGroupFieldFactory fieldFactory) {
editorFieldGroup.setFieldFactory(fieldFactory);
}
/**
* Sets the error handler for the editor.
*
* The error handler is called whenever there is an exception in the editor.
*
* @param editorErrorHandler
* The editor error handler to use
* @throws IllegalArgumentException
* if the error handler is null
*/
public void setEditorErrorHandler(EditorErrorHandler editorErrorHandler)
throws IllegalArgumentException {
if (editorErrorHandler == null) {
throw new IllegalArgumentException(
"The error handler cannot be null");
}
this.editorErrorHandler = editorErrorHandler;
}
/**
* Gets the error handler used for the editor
*
* @see #setErrorHandler(com.vaadin.server.ErrorHandler)
* @return the editor error handler, never null
*/
public EditorErrorHandler getEditorErrorHandler() {
return editorErrorHandler;
}
/**
* Gets the field factory for the {@link FieldGroup}. The field factory is
* only used when {@link FieldGroup} creates a new field.
* <p>
* <em>Note:</em> This is a pass-through call to the backing field group.
*
* @return The field factory in use
*/
public FieldGroupFieldFactory getEditorFieldFactory() {
return editorFieldGroup.getFieldFactory();
}
/**
* Sets the caption on the save button in the Grid editor.
*
* @param saveCaption
* the caption to set
* @throws IllegalArgumentException
* if {@code saveCaption} is {@code null}
*/
public void setEditorSaveCaption(String saveCaption)
throws IllegalArgumentException {
if (saveCaption == null) {
throw new IllegalArgumentException("Save caption cannot be null");
}
getState().editorSaveCaption = saveCaption;
}
/**
* Gets the current caption of the save button in the Grid editor.
*
* @return the current caption of the save button
*/
public String getEditorSaveCaption() {
return getState(false).editorSaveCaption;
}
/**
* Sets the caption on the cancel button in the Grid editor.
*
* @param cancelCaption
* the caption to set
* @throws IllegalArgumentException
* if {@code cancelCaption} is {@code null}
*/
public void setEditorCancelCaption(String cancelCaption)
throws IllegalArgumentException {
if (cancelCaption == null) {
throw new IllegalArgumentException("Cancel caption cannot be null");
}
getState().editorCancelCaption = cancelCaption;
}
/**
* Gets the current caption of the cancel button in the Grid editor.
*
* @return the current caption of the cancel button
*/
public String getEditorCancelCaption() {
return getState(false).editorCancelCaption;
}
/**
* Sets the buffered editor mode. The default mode is buffered (
* <code>true</code>).
*
* @since 7.6
* @param editorBuffered
* <code>true</code> to enable buffered editor,
* <code>false</code> to disable it
* @throws IllegalStateException
* If editor is active while attempting to change the buffered
* mode.
*/
public void setEditorBuffered(boolean editorBuffered)
throws IllegalStateException {
if (isEditorActive()) {
throw new IllegalStateException(
"Can't change editor unbuffered mode while editor is active.");
}
getState().editorBuffered = editorBuffered;
editorFieldGroup.setBuffered(editorBuffered);
}
/**
* Gets the buffered editor mode.
*
* @since 7.6
* @return <code>true</code> if buffered editor is enabled,
* <code>false</code> otherwise
*/
public boolean isEditorBuffered() {
return getState(false).editorBuffered;
}
@Override
public void addItemClickListener(ItemClickListener listener) {
addListener(GridConstants.ITEM_CLICK_EVENT_ID, ItemClickEvent.class,
listener, ItemClickEvent.ITEM_CLICK_METHOD);
}
@Override
@Deprecated
public void addListener(ItemClickListener listener) {
addItemClickListener(listener);
}
@Override
public void removeItemClickListener(ItemClickListener listener) {
removeListener(GridConstants.ITEM_CLICK_EVENT_ID, ItemClickEvent.class,
listener);
}
@Override
@Deprecated
public void removeListener(ItemClickListener listener) {
removeItemClickListener(listener);
}
/**
* Requests that the column widths should be recalculated.
* <p>
* In most cases Grid will know when column widths need to be recalculated
* but this method can be used to force recalculation in situations when
* grid does not recalculate automatically.
*
* @since 7.4.1
*/
public void recalculateColumnWidths() {
getRpcProxy(GridClientRpc.class).recalculateColumnWidths();
}
/**
* Registers a new column visibility change listener
*
* @since 7.5.0
* @param listener
* the listener to register
*/
public void addColumnVisibilityChangeListener(
ColumnVisibilityChangeListener listener) {
addListener(ColumnVisibilityChangeEvent.class, listener,
COLUMN_VISIBILITY_METHOD);
}
/**
* Removes a previously registered column visibility change listener
*
* @since 7.5.0
* @param listener
* the listener to remove
*/
public void removeColumnVisibilityChangeListener(
ColumnVisibilityChangeListener listener) {
removeListener(ColumnVisibilityChangeEvent.class, listener,
COLUMN_VISIBILITY_METHOD);
}
private void fireColumnVisibilityChangeEvent(Column column, boolean hidden,
boolean isUserOriginated) {
fireEvent(new ColumnVisibilityChangeEvent(this, column, hidden,
isUserOriginated));
}
/**
* Sets a new details generator for row details.
* <p>
* The currently opened row details will be re-rendered.
*
* @since 7.5.0
* @param detailsGenerator
* the details generator to set
* @throws IllegalArgumentException
* if detailsGenerator is <code>null</code>;
*/
public void setDetailsGenerator(DetailsGenerator detailsGenerator)
throws IllegalArgumentException {
detailComponentManager.setDetailsGenerator(detailsGenerator);
}
/**
* Gets the current details generator for row details.
*
* @since 7.5.0
* @return the detailsGenerator the current details generator
*/
public DetailsGenerator getDetailsGenerator() {
return detailComponentManager.getDetailsGenerator();
}
/**
* Shows or hides the details for a specific item.
*
* @since 7.5.0
* @param itemId
* the id of the item for which to set details visibility
* @param visible
* <code>true</code> to show the details, or <code>false</code>
* to hide them
*/
public void setDetailsVisible(Object itemId, boolean visible) {
detailComponentManager.setDetailsVisible(itemId, visible);
}
/**
* Checks whether details are visible for the given item.
*
* @since 7.5.0
* @param itemId
* the id of the item for which to check details visibility
* @return <code>true</code> iff the details are visible
*/
public boolean isDetailsVisible(Object itemId) {
return detailComponentManager.isDetailsVisible(itemId);
}
private static SelectionMode getDefaultSelectionMode() {
return SelectionMode.SINGLE;
}
@Override
public void readDesign(Element design, DesignContext context) {
super.readDesign(design, context);
Attributes attrs = design.attributes();
if (attrs.hasKey("editable")) {
setEditorEnabled(DesignAttributeHandler.readAttribute("editable",
attrs, boolean.class));
}
if (attrs.hasKey("rows")) {
setHeightByRows(DesignAttributeHandler.readAttribute("rows", attrs,
double.class));
setHeightMode(HeightMode.ROW);
}
if (attrs.hasKey("selection-mode")) {
setSelectionMode(DesignAttributeHandler.readAttribute(
"selection-mode", attrs, SelectionMode.class));
}
if (design.children().size() > 0) {
if (design.children().size() > 1
|| !design.child(0).tagName().equals("table")) {
throw new DesignException(
"Grid needs to have a table element as its only child");
}
Element table = design.child(0);
Elements colgroups = table.getElementsByTag("colgroup");
if (colgroups.size() != 1) {
throw new DesignException(
"Table element in declarative Grid needs to have a"
+ " colgroup defining the columns used in Grid");
}
int i = 0;
for (Element col : colgroups.get(0).getElementsByTag("col")) {
String propertyId = DesignAttributeHandler.readAttribute(
"property-id", col.attributes(), "property-" + i,
String.class);
addColumn(propertyId, String.class).readDesign(col, context);
++i;
}
for (Element child : table.children()) {
if (child.tagName().equals("thead")) {
header.readDesign(child, context);
} else if (child.tagName().equals("tbody")) {
for (Element row : child.children()) {
Elements cells = row.children();
Object[] data = new String[cells.size()];
for (int c = 0; c < cells.size(); ++c) {
data[c] = cells.get(c).html();
}
addRow(data);
}
// Since inline data is used, set HTML renderer for columns
for (Column c : getColumns()) {
c.setRenderer(new HtmlRenderer());
}
} else if (child.tagName().equals("tfoot")) {
footer.readDesign(child, context);
}
}
}
// Read frozen columns after columns are read.
if (attrs.hasKey("frozen-columns")) {
setFrozenColumnCount(DesignAttributeHandler
.readAttribute("frozen-columns", attrs, int.class));
}
}
@Override
public void writeDesign(Element design, DesignContext context) {
super.writeDesign(design, context);
Attributes attrs = design.attributes();
Grid def = context.getDefaultInstance(this);
DesignAttributeHandler.writeAttribute("editable", attrs,
isEditorEnabled(), def.isEditorEnabled(), boolean.class,
context);
DesignAttributeHandler.writeAttribute("frozen-columns", attrs,
getFrozenColumnCount(), def.getFrozenColumnCount(), int.class,
context);
if (getHeightMode() == HeightMode.ROW) {
DesignAttributeHandler.writeAttribute("rows", attrs,
getHeightByRows(), def.getHeightByRows(), double.class,
context);
}
SelectionMode selectionMode = null;
if (selectionModel.getClass().equals(SingleSelectionModel.class)) {
selectionMode = SelectionMode.SINGLE;
} else if (selectionModel.getClass()
.equals(MultiSelectionModel.class)) {
selectionMode = SelectionMode.MULTI;
} else if (selectionModel.getClass().equals(NoSelectionModel.class)) {
selectionMode = SelectionMode.NONE;
}
assert selectionMode != null : "Unexpected selection model "
+ selectionModel.getClass().getName();
DesignAttributeHandler.writeAttribute("selection-mode", attrs,
selectionMode, getDefaultSelectionMode(), SelectionMode.class,
context);
if (columns.isEmpty()) {
// Empty grid. Structure not needed.
return;
}
// Do structure.
Element tableElement = design.appendElement("table");
Element colGroup = tableElement.appendElement("colgroup");
List<Column> columnOrder = getColumns();
for (int i = 0; i < columnOrder.size(); ++i) {
Column column = columnOrder.get(i);
Element colElement = colGroup.appendElement("col");
column.writeDesign(colElement, context);
}
// Always write thead. Reads correctly when there no header rows
header.writeDesign(tableElement.appendElement("thead"), context);
if (context.shouldWriteData(this)) {
Element bodyElement = tableElement.appendElement("tbody");
for (Object itemId : datasource.getItemIds()) {
Element tableRow = bodyElement.appendElement("tr");
for (Column c : getColumns()) {
Object value = datasource.getItem(itemId)
.getItemProperty(c.getPropertyId()).getValue();
tableRow.appendElement("td")
.append((value != null ? DesignFormatter
.encodeForTextNode(value.toString()) : ""));
}
}
}
if (footer.getRowCount() > 0) {
footer.writeDesign(tableElement.appendElement("tfoot"), context);
}
}
@Override
public void addBlurListener(BlurListener listener) {
addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener,
BlurListener.blurMethod);
}
@Override
public void removeBlurListener(BlurListener listener) {
removeListener(BlurEvent.EVENT_ID, BlurEvent.class, listener);
}
@Override
public void addFocusListener(FocusListener listener) {
addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener,
FocusListener.focusMethod);
}
@Override
public void removeFocusListener(FocusListener listener) {
removeListener(FocusEvent.EVENT_ID, FocusEvent.class, listener);
}
@Override
public void focus() {
super.focus();
}
@Override
public int getTabIndex() {
return getState(false).tabIndex;
}
@Override
public void setTabIndex(int tabIndex) {
getState().tabIndex = tabIndex;
}
@Override
protected Collection<String> getCustomAttributes() {
Collection<String> result = super.getCustomAttributes();
result.add("editor-enabled");
result.add("editable");
result.add("frozen-column-count");
result.add("frozen-columns");
result.add("height-by-rows");
result.add("rows");
result.add("selection-mode");
result.add("header-visible");
result.add("footer-visible");
result.add("editor-error-handler");
result.add("height-mode");
return result;
}
}