/* * Copyright 2000-2018 Vaadin Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.vaadin.ui; import java.io.Serializable; import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.function.BinaryOperator; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; import org.jsoup.Jsoup; import org.jsoup.nodes.Attributes; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import com.vaadin.data.BeanPropertySet; import com.vaadin.data.Binder; import com.vaadin.data.Binder.Binding; import com.vaadin.data.HasDataProvider; import com.vaadin.data.HasValue; import com.vaadin.data.PropertyDefinition; import com.vaadin.data.PropertySet; import com.vaadin.data.ValueProvider; import com.vaadin.data.provider.CallbackDataProvider; import com.vaadin.data.provider.DataCommunicator; import com.vaadin.data.provider.DataGenerator; import com.vaadin.data.provider.DataProvider; import com.vaadin.data.provider.GridSortOrder; import com.vaadin.data.provider.GridSortOrderBuilder; import com.vaadin.data.provider.InMemoryDataProvider; import com.vaadin.data.provider.Query; import com.vaadin.data.provider.QuerySortOrder; import com.vaadin.event.ConnectorEvent; import com.vaadin.event.ContextClickEvent; import com.vaadin.event.HasUserOriginated; import com.vaadin.event.SortEvent; import com.vaadin.event.SortEvent.SortListener; import com.vaadin.event.SortEvent.SortNotifier; import com.vaadin.event.selection.MultiSelectionListener; import com.vaadin.event.selection.SelectionListener; import com.vaadin.event.selection.SingleSelectionListener; import com.vaadin.server.AbstractExtension; import com.vaadin.server.EncodeResult; import com.vaadin.server.Extension; import com.vaadin.server.JsonCodec; import com.vaadin.server.SerializableComparator; import com.vaadin.server.SerializableSupplier; import com.vaadin.server.Setter; import com.vaadin.server.VaadinServiceClassLoaderUtil; import com.vaadin.shared.Connector; import com.vaadin.shared.MouseEventDetails; import com.vaadin.shared.Registration; import com.vaadin.shared.data.DataCommunicatorConstants; import com.vaadin.shared.data.sort.SortDirection; import com.vaadin.shared.ui.ContentMode; import com.vaadin.shared.ui.grid.AbstractGridExtensionState; import com.vaadin.shared.ui.grid.ColumnResizeMode; import com.vaadin.shared.ui.grid.ColumnState; import com.vaadin.shared.ui.grid.DetailsManagerState; import com.vaadin.shared.ui.grid.GridClientRpc; import com.vaadin.shared.ui.grid.GridConstants; import com.vaadin.shared.ui.grid.GridConstants.Section; import com.vaadin.shared.ui.grid.GridServerRpc; import com.vaadin.shared.ui.grid.GridState; import com.vaadin.shared.ui.grid.GridStaticCellType; import com.vaadin.shared.ui.grid.HeightMode; import com.vaadin.shared.ui.grid.ScrollDestination; import com.vaadin.shared.ui.grid.SectionState; import com.vaadin.ui.components.grid.ColumnReorderListener; import com.vaadin.ui.components.grid.ColumnResizeListener; import com.vaadin.ui.components.grid.ColumnVisibilityChangeListener; import com.vaadin.ui.components.grid.DetailsGenerator; import com.vaadin.ui.components.grid.Editor; import com.vaadin.ui.components.grid.EditorImpl; import com.vaadin.ui.components.grid.Footer; import com.vaadin.ui.components.grid.FooterRow; import com.vaadin.ui.components.grid.GridMultiSelect; import com.vaadin.ui.components.grid.GridSelectionModel; import com.vaadin.ui.components.grid.GridSingleSelect; import com.vaadin.ui.components.grid.Header; import com.vaadin.ui.components.grid.Header.Row; import com.vaadin.ui.components.grid.HeaderCell; import com.vaadin.ui.components.grid.HeaderRow; import com.vaadin.ui.components.grid.ItemClickListener; import com.vaadin.ui.components.grid.MultiSelectionModel; import com.vaadin.ui.components.grid.MultiSelectionModelImpl; import com.vaadin.ui.components.grid.NoSelectionModel; import com.vaadin.ui.components.grid.SingleSelectionModel; import com.vaadin.ui.components.grid.SingleSelectionModelImpl; import com.vaadin.ui.components.grid.SortOrderProvider; 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.ui.renderers.AbstractRenderer; import com.vaadin.ui.renderers.ComponentRenderer; import com.vaadin.ui.renderers.HtmlRenderer; import com.vaadin.ui.renderers.Renderer; import com.vaadin.ui.renderers.TextRenderer; import com.vaadin.util.ReflectTools; import elemental.json.Json; import elemental.json.JsonObject; import elemental.json.JsonValue; /** * A grid component for displaying tabular data. * * @author Vaadin Ltd * @since 8.0 * * @param * the grid bean type */ public class Grid extends AbstractListing implements HasComponents, HasDataProvider, SortNotifier> { private static final String DECLARATIVE_DATA_ITEM_TYPE = "data-item-type"; /** * A callback method for fetching items. The callback is provided with a * list of sort orders, offset index and limit. * * @param * the grid bean type */ @FunctionalInterface public interface FetchItemsCallback extends Serializable { /** * Returns a stream of items ordered by given sort orders, limiting the * results with given offset and limit. *

* This method is called after the size of the data set is asked from a * related size callback. The offset and limit are promised to be within * the size of the data set. * * @param sortOrder * a list of sort orders * @param offset * the first index to fetch * @param limit * the fetched item count * @return stream of items */ public Stream fetchItems(List sortOrder, int offset, int limit); } @Deprecated private static final Method COLUMN_REORDER_METHOD = ReflectTools.findMethod( ColumnReorderListener.class, "columnReorder", ColumnReorderEvent.class); private static final Method SORT_ORDER_CHANGE_METHOD = ReflectTools .findMethod(SortListener.class, "sort", SortEvent.class); @Deprecated private static final Method COLUMN_RESIZE_METHOD = ReflectTools.findMethod( ColumnResizeListener.class, "columnResize", ColumnResizeEvent.class); @Deprecated private static final Method ITEM_CLICK_METHOD = ReflectTools .findMethod(ItemClickListener.class, "itemClick", ItemClick.class); @Deprecated private static final Method COLUMN_VISIBILITY_METHOD = ReflectTools .findMethod(ColumnVisibilityChangeListener.class, "columnVisibilityChanged", ColumnVisibilityChangeEvent.class); /** * Selection mode representing the built-in selection models in grid. *

* These enums can be used in {@link Grid#setSelectionMode(SelectionMode)} * to easily switch between the build-in selection models. * * @see Grid#setSelectionMode(SelectionMode) * @see Grid#setSelectionModel(GridSelectionModel) */ public enum SelectionMode { /** * Single selection mode that maps to build-in * {@link SingleSelectionModel}. * * @see SingleSelectionModelImpl */ SINGLE { @Override protected GridSelectionModel createModel() { return new SingleSelectionModelImpl<>(); } }, /** * Multiselection mode that maps to build-in {@link MultiSelectionModel} * . * * @see MultiSelectionModelImpl */ MULTI { @Override protected GridSelectionModel createModel() { return new MultiSelectionModelImpl<>(); } }, /** * Selection model that doesn't allow selection. * * @see NoSelectionModel */ NONE { @Override protected GridSelectionModel createModel() { return new NoSelectionModel<>(); } }; /** * Creates the selection model to use with this enum. * * @param * the type of items in the grid * @return the selection model */ protected abstract GridSelectionModel createModel(); } /** * An event that is fired when the columns are reordered. */ public static class ColumnReorderEvent extends Component.Event implements HasUserOriginated { private final boolean userOriginated; /** * * @param source * the grid where the event originated from * @param userOriginated * true if event is a result of user * interaction, false if from API call */ public ColumnReorderEvent(Grid source, boolean userOriginated) { super(source); this.userOriginated = userOriginated; } /** * Returns true if the column reorder was done by the user, * false if not and it was triggered by server side code. * * @return true if event is a result of user interaction */ @Override public boolean isUserOriginated() { return userOriginated; } } /** * An event that is fired when a column is resized, either programmatically * or by the user. */ public static class ColumnResizeEvent extends Component.Event implements HasUserOriginated { private final Column column; private final boolean userOriginated; /** * * @param source * the grid where the event originated from * @param userOriginated * true if event is a result of user * interaction, false 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 true if the column resize was done by the user, * false if not and it was triggered by server side code. * * @return true if event is a result of user interaction */ @Override public boolean isUserOriginated() { return userOriginated; } } /** * An event fired when an item in the Grid has been clicked. * * @param * the grid bean type */ public static class ItemClick extends ConnectorEvent { private final T item; private final Column column; private final MouseEventDetails mouseEventDetails; private final int rowIndex; /** * Creates a new {@code ItemClick} event containing the given item and * Column originating from the given Grid. * */ public ItemClick(Grid source, Column column, T item, MouseEventDetails mouseEventDetails, int rowIndex) { super(source); this.column = column; this.item = item; this.mouseEventDetails = mouseEventDetails; this.rowIndex = rowIndex; } /** * Returns the clicked item. * * @return the clicked item */ public T getItem() { return item; } /** * Returns the clicked column. * * @return the clicked column */ public Column getColumn() { return column; } /** * Returns the source Grid. * * @return the grid */ @Override public Grid getSource() { return (Grid) super.getSource(); } /** * Returns the mouse event details. * * @return the mouse event details */ public MouseEventDetails getMouseEventDetails() { return mouseEventDetails; } /** * Returns the clicked rowIndex. * * @return the clicked rowIndex * @since 8.4 */ public int getRowIndex() { return rowIndex; } } /** * ContextClickEvent for the Grid Component. * *

* Usage: * *

     * grid.addContextClickListener(event -> Notification.show(
     *         ((GridContextClickEvent<Person>) event).getItem() + " Clicked"));
     * 
* * @param * the grid bean type */ public static class GridContextClickEvent extends ContextClickEvent { private final T item; private final int rowIndex; private final Column column; private final Section section; /** * Creates a new context click event. * * @param source * the grid where the context click occurred * @param mouseEventDetails * details about mouse position * @param section * the section of the grid which was clicked * @param rowIndex * the index of the row which was clicked * @param item * the item which was clicked * @param column * the column which was clicked */ public GridContextClickEvent(Grid source, MouseEventDetails mouseEventDetails, Section section, int rowIndex, T item, Column column) { super(source, mouseEventDetails); this.item = item; this.section = section; this.column = column; this.rowIndex = rowIndex; } /** * Returns the item of context clicked row. * * @return item of clicked row; null if header or footer */ public T getItem() { return item; } /** * Returns the clicked column. * * @return the clicked column */ public Column getColumn() { return column; } /** * Return the clicked section of Grid. * * @return section of grid */ public Section getSection() { return section; } /** * Returns the clicked row index. *

* 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 that is fired when a column's visibility changes. * * @since 7.5.0 */ public static class ColumnVisibilityChangeEvent extends Component.Event implements HasUserOriginated { 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 * true if the column was hidden, * false if it became visible * @param isUserOriginated * true if 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 true if the column was hidden false * if it was set visible */ public boolean isHidden() { return hidden; } @Override public boolean isUserOriginated() { return userOriginated; } } /** * A helper base class for creating extensions for the Grid component. * * @param */ public abstract static class AbstractGridExtension extends AbstractListingExtension { @Override public void extend(AbstractListing grid) { if (!(grid instanceof Grid)) { throw new IllegalArgumentException( getClass().getSimpleName() + " can only extend Grid"); } super.extend(grid); } /** * Adds given component to the connector hierarchy of Grid. * * @param c * the component to add */ protected void addComponentToGrid(Component c) { getParent().addExtensionComponent(c); } /** * Removes given component from the connector hierarchy of Grid. * * @param c * the component to remove */ protected void removeComponentFromGrid(Component c) { getParent().removeExtensionComponent(c); } @Override public Grid getParent() { return (Grid) super.getParent(); } @Override protected AbstractGridExtensionState getState() { return (AbstractGridExtensionState) super.getState(); } @Override protected AbstractGridExtensionState getState(boolean markAsDirty) { return (AbstractGridExtensionState) super.getState(markAsDirty); } protected String getInternalIdForColumn(Column column) { return getParent().getInternalIdForColumn(column); } } private final class GridServerRpcImpl implements GridServerRpc { @Override public void sort(String[] columnInternalIds, SortDirection[] directions, boolean isUserOriginated) { assert columnInternalIds.length == directions.length : "Column and sort direction counts don't match."; List> list = new ArrayList<>(directions.length); for (int i = 0; i < columnInternalIds.length; ++i) { Column column = columnKeys.get(columnInternalIds[i]); list.add(new GridSortOrder<>(column, directions[i])); } setSortOrder(list, isUserOriginated); } @Override public void itemClick(String rowKey, String columnInternalId, MouseEventDetails details, int rowIndex) { Column column = getColumnByInternalId(columnInternalId); T item = getDataCommunicator().getKeyMapper().get(rowKey); fireEvent(new ItemClick<>(Grid.this, column, item, details, rowIndex)); } @Override public void contextClick(int rowIndex, String rowKey, String columnInternalId, Section section, MouseEventDetails details) { T item = null; if (rowKey != null) { item = getDataCommunicator().getKeyMapper().get(rowKey); } fireEvent(new GridContextClickEvent<>(Grid.this, details, section, rowIndex, item, getColumnByInternalId(columnInternalId))); } @Override public void columnsReordered(List newColumnOrder, List 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().getField(diffStateKey) .getGenericType(); } catch (NoSuchFieldException | 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 internalId, boolean hidden) { Column column = getColumnByInternalId(internalId); column.checkColumnIsAttached(); if (column.isHidden() != hidden) { column.getState().hidden = hidden; fireColumnVisibilityChangeEvent(column, hidden, true); } } @Override public void columnResized(String internalId, double pixels) { final Column column = getColumnByInternalId(internalId); if (column != null && column.isResizable()) { column.getState().width = pixels; fireColumnResizeEvent(column, true); } } } /** * Class for managing visible details rows. * * @param * the grid bean type */ public static class DetailsManager extends AbstractGridExtension { private final Set visibleDetails = new HashSet<>(); private final Map components = new HashMap<>(); private DetailsGenerator generator; /** * Sets the details component generator. * * @param generator * the generator for details components */ public void setDetailsGenerator(DetailsGenerator generator) { if (this.generator != generator) { removeAllComponents(); } getState().hasDetailsGenerator = generator != null; this.generator = generator; visibleDetails.forEach(this::refresh); } @Override public void remove() { removeAllComponents(); super.remove(); } private void removeAllComponents() { // Clean up old components components.values().forEach(this::removeComponentFromGrid); components.clear(); } @Override public void generateData(T item, JsonObject jsonObject) { if (generator == null || !visibleDetails.contains(item)) { return; } if (!components.containsKey(item)) { Component detailsComponent = generator.apply(item); Objects.requireNonNull(detailsComponent, "Details generator can't create null components"); if (detailsComponent.getParent() != null) { throw new IllegalStateException( "Details component was already attached"); } addComponentToGrid(detailsComponent); components.put(item, detailsComponent); } jsonObject.put(GridState.JSONKEY_DETAILS_VISIBLE, components.get(item).getConnectorId()); } @Override public void destroyData(T item) { // No clean up needed. Components are removed when hiding details // and/or changing details generator } /** * Sets the visibility of details component for given item. * * @param item * the item to show details for * @param visible * {@code true} if details component should be visible; * {@code false} if it should be hidden */ public void setDetailsVisible(T item, boolean visible) { boolean refresh = false; if (!visible) { refresh = visibleDetails.remove(item); if (components.containsKey(item)) { removeComponentFromGrid(components.remove(item)); } } else { refresh = visibleDetails.add(item); } if (refresh) { refresh(item); } } /** * Returns the visibility of details component for given item. * * @param item * the item to show details for * * @return {@code true} if details component should be visible; * {@code false} if it should be hidden */ public boolean isDetailsVisible(T item) { return visibleDetails.contains(item); } @Override public Grid getParent() { return super.getParent(); } @Override protected DetailsManagerState getState() { return (DetailsManagerState) super.getState(); } @Override protected DetailsManagerState getState(boolean markAsDirty) { return (DetailsManagerState) super.getState(markAsDirty); } } /** * This extension manages the configuration and data communication for a * Column inside of a Grid component. * * @param * the grid bean type * @param * the column value type */ public static class Column extends AbstractExtension { /** * behavior when parsing nested properties which may contain * null values in the property chain */ public enum NestedNullBehavior { /** * throw a NullPointerException if there is a nested * null value */ THROW, /** * silently ignore any exceptions caused by nested null * values */ ALLOW_NULLS } private final ValueProvider valueProvider; private ValueProvider presentationProvider; private SortOrderProvider sortOrderProvider = direction -> { String id = getId(); if (id == null) { return Stream.empty(); } return Stream.of(new QuerySortOrder(id, direction)); }; private NestedNullBehavior nestedNullBehavior = NestedNullBehavior.THROW; private boolean sortable = true; private SerializableComparator comparator; private StyleGenerator styleGenerator = item -> null; private DescriptionGenerator descriptionGenerator; private DataGenerator dataGenerator = new DataGenerator() { @Override public void generateData(T item, JsonObject jsonObject) { ColumnState state = getState(false); String communicationId = getConnectorId(); assert communicationId != null : "No communication ID set for column " + state.caption; JsonObject obj = getDataObject(jsonObject, DataCommunicatorConstants.DATA); obj.put(communicationId, generateRendererValue(item, presentationProvider, state.renderer)); String style = styleGenerator.apply(item); if (style != null && !style.isEmpty()) { JsonObject styleObj = getDataObject(jsonObject, GridState.JSONKEY_CELLSTYLES); styleObj.put(communicationId, style); } if (descriptionGenerator != null) { String description = descriptionGenerator.apply(item); if (description != null && !description.isEmpty()) { JsonObject descriptionObj = getDataObject(jsonObject, GridState.JSONKEY_CELLDESCRIPTION); descriptionObj.put(communicationId, description); } } } @Override public void destroyData(T item) { removeComponent(getGrid().getDataProvider().getId(item)); } @Override public void destroyAllData() { // Make a defensive copy of keys, as the map gets cleared when // removing components. new HashSet<>(activeComponents.keySet()) .forEach(component -> removeComponent(component)); } }; private Binding editorBinding; private Map activeComponents = new HashMap<>(); private String userId; /** * Constructs a new Column configuration with given renderer and value * provider. * * @param valueProvider * the function to get values from items, not * null * @param renderer * the value renderer, not null */ protected Column(ValueProvider valueProvider, Renderer renderer) { this(valueProvider, ValueProvider.identity(), renderer); } /** * Constructs a new Column configuration with given renderer and value * provider. *

* For a more complete explanation on presentation provider, see * {@link #setRenderer(ValueProvider, Renderer)}. * * @param valueProvider * the function to get values from items, not * null * @param presentationProvider * the function to get presentations from the value of this * column, not null. For more details, see * {@link #setRenderer(ValueProvider, Renderer)} * @param renderer * the presentation renderer, not null * @param

* the presentation type * * @since 8.1 */ protected

Column(ValueProvider valueProvider, ValueProvider presentationProvider, Renderer renderer) { Objects.requireNonNull(valueProvider, "Value provider can't be null"); Objects.requireNonNull(presentationProvider, "Presentation provider can't be null"); Objects.requireNonNull(renderer, "Renderer can't be null"); ColumnState state = getState(); this.valueProvider = valueProvider; this.presentationProvider = presentationProvider; state.renderer = renderer; state.caption = ""; // Add the renderer as a child extension of this extension, thus // ensuring the renderer will be unregistered when this column is // removed addExtension(renderer); Class valueType = renderer.getPresentationType(); if (Comparable.class.isAssignableFrom(valueType)) { comparator = (a, b) -> compareComparables( valueProvider.apply(a), valueProvider.apply(b)); } else if (Number.class.isAssignableFrom(valueType)) { /* * Value type will be Number whenever using NumberRenderer. * Provide explicit comparison support in this case even though * Number itself isn't Comparable. */ comparator = (a, b) -> compareNumbers( (Number) valueProvider.apply(a), (Number) valueProvider.apply(b)); } else { comparator = (a, b) -> compareMaybeComparables( valueProvider.apply(a), valueProvider.apply(b)); } } /** * Constructs a new Column configuration with given renderer and value * provider. *

* For a more complete explanation on presentation provider, see * {@link #setRenderer(ValueProvider, Renderer)}. * * @param valueProvider * the function to get values from items, not * null * @param presentationProvider * the function to get presentations from the value of this * column, not null. For more details, see * {@link #setRenderer(ValueProvider, Renderer)} * @param nestedNullBehavior * behavior on encountering nested null values * when reading the value from the bean * @param renderer * the presentation renderer, not null * @param

* the presentation type * * @since 8.8 */ protected

Column(ValueProvider valueProvider, ValueProvider presentationProvider, Renderer renderer, NestedNullBehavior nestedNullBehavior) { this(valueProvider, presentationProvider, renderer); this.nestedNullBehavior = nestedNullBehavior; } private static int compareMaybeComparables(Object a, Object b) { if (hasCommonComparableBaseType(a, b)) { return compareComparables(a, b); } return compareComparables(Objects.toString(a, ""), Objects.toString(b, "")); } private static boolean hasCommonComparableBaseType(Object a, Object b) { if (a instanceof Comparable && b instanceof Comparable) { Class aClass = a.getClass(); Class bClass = b.getClass(); if (aClass == bClass) { return true; } Class baseType = ReflectTools.findCommonBaseType(aClass, bClass); if (Comparable.class.isAssignableFrom(baseType)) { return true; } } if ((a == null && b instanceof Comparable) || (b == null && a instanceof Comparable)) { return true; } return false; } @SuppressWarnings({ "unchecked", "rawtypes" }) private static int compareComparables(Object a, Object b) { return ((Comparator) Comparator .nullsLast(Comparator.naturalOrder())).compare(a, b); } @SuppressWarnings("unchecked") private static int compareNumbers(Number a, Number b) { Number valueA = a != null ? a : Double.POSITIVE_INFINITY; Number valueB = b != null ? b : Double.POSITIVE_INFINITY; // Most Number implementations are Comparable if (valueA instanceof Comparable && valueA.getClass().isInstance(valueB)) { return ((Comparable) valueA).compareTo(valueB); } if (valueA.equals(valueB)) { return 0; } // Fall back to comparing based on potentially truncated values int compare = Long.compare(valueA.longValue(), valueB.longValue()); if (compare == 0) { // This might still produce 0 even though the values are not // equals, but there's nothing more we can do about that compare = Double.compare(valueA.doubleValue(), valueB.doubleValue()); } return compare; } @SuppressWarnings("unchecked") private

JsonValue generateRendererValue(T item, ValueProvider presentationProvider, Connector renderer) { V value; try { value = valueProvider.apply(item); } catch (NullPointerException npe) { value = null; if (NestedNullBehavior.THROW == nestedNullBehavior) { throw npe; } } P presentationValue = presentationProvider.apply(value); // Make Grid track components. if (renderer instanceof ComponentRenderer && presentationValue instanceof Component) { addComponent(getGrid().getDataProvider().getId(item), (Component) presentationValue); } return ((Renderer

) renderer).encode(presentationValue); } private void addComponent(Object item, Component component) { if (activeComponents.containsKey(item)) { if (activeComponents.get(item).equals(component)) { // Reusing old component return; } removeComponent(item); } activeComponents.put(item, component); getGrid().addExtensionComponent(component); } private void removeComponent(Object item) { Component component = activeComponents.remove(item); if (component != null) { getGrid().removeExtensionComponent(component); } } /** * Gets a data object with the given key from the given JsonObject. If * there is no object with the key, this method creates a new * JsonObject. * * @param jsonObject * the json object * @param key * the key where the desired data object is stored * @return data object for the given key */ private JsonObject getDataObject(JsonObject jsonObject, String key) { if (!jsonObject.hasKey(key)) { jsonObject.put(key, Json.createObject()); } return jsonObject.getObject(key); } @Override protected ColumnState getState() { return getState(true); } @Override protected ColumnState getState(boolean markAsDirty) { return (ColumnState) super.getState(markAsDirty); } /** * This method extends the given Grid with this Column. * * @param grid * the grid to extend */ private void extend(Grid grid) { super.extend(grid); } /** * Returns the identifier used with this Column in communication. * * @return the identifier string */ private String getInternalId() { return getState(false).internalId; } /** * Sets the identifier to use with this Column in communication. * * @param id * the identifier string */ private void setInternalId(String id) { Objects.requireNonNull(id, "Communication ID can't be null"); getState().internalId = id; } /** * Returns the user-defined identifier for this column. * * @return the identifier string */ public String getId() { return userId; } /** * Sets the user-defined identifier to map this column. The identifier * can be used for example in {@link Grid#getColumn(String)}. *

* The id is also used as the {@link #setSortProperty(String...) backend * sort property} for this column if no sort property or sort order * provider has been set for this column. * * @see #setSortProperty(String...) * @see #setSortOrderProvider(SortOrderProvider) * * @param id * the identifier string * @return this column */ public Column setId(String id) { Objects.requireNonNull(id, "Column identifier cannot be null"); if (userId != null) { throw new IllegalStateException( "Column identifier cannot be changed"); } userId = id; getGrid().setColumnId(id, this); updateSortable(); return this; } private void updateSortable() { boolean inMemory = getGrid().getDataProvider().isInMemory(); boolean hasSortOrder = getSortOrder(SortDirection.ASCENDING) .count() != 0; getState().sortable = this.sortable && (inMemory || hasSortOrder); } /** * Gets the function used to produce the value for data in this column * based on the row item. * * @return the value provider function * * @since 8.0.3 */ public ValueProvider getValueProvider() { return valueProvider; } /** * Sets whether the user can sort this column or not. Whether the column * is actually sortable after {@code setSortable(true)} depends on the * {@link DataProvider} and the defined sort order for this column. When * using an {@link InMemoryDataProvider} sorting can be automatic. * * @param sortable * {@code true} to enable sorting for this column; * {@code false} to disable it * @return this column */ public Column setSortable(boolean sortable) { if (this.sortable != sortable) { this.sortable = sortable; updateSortable(); } return this; } /** * Gets whether sorting is enabled for this column. * * @return {@code true} if the sorting is enabled for this column; * {@code false} if not */ public boolean isSortable() { return sortable; } /** * Gets whether the user can actually sort this column. * * @return {@code true} if the column can be sorted by the user; * {@code false} if not * * @since 8.3.2 */ public boolean isSortableByUser() { return getState(false).sortable; } /** * Sets the header aria-label for this column. * * @param caption * the header aria-label, null removes the aria-label from * this column * * @return this column * * @since 8.2 */ public Column setAssistiveCaption(String caption) { if (Objects.equals(caption, getAssistiveCaption())) { return this; } getState().assistiveCaption = caption; return this; } /** * Gets the header caption for this column. * * @return header caption * * @since 8.2 */ public String getAssistiveCaption() { return getState(false).assistiveCaption; } /** * Sets the header caption for this column. * * @param caption * the header caption, not null * * @return this column */ public Column setCaption(String caption) { Objects.requireNonNull(caption, "Header caption can't be null"); caption = Jsoup.parse(caption).text(); if (caption.equals(getState(false).caption)) { return this; } getState().caption = caption; HeaderRow row = getGrid().getDefaultHeaderRow(); if (row != null) { row.getCell(this).setText(caption); } return this; } /** * Gets the header caption for this column. * * @return header caption */ public String getCaption() { return getState(false).caption; } /** * Sets a comparator to use with in-memory sorting with this column. * Sorting with a back-end is done using * {@link Column#setSortProperty(String...)}. * * @param comparator * the comparator to use when sorting data in this column * @return this column */ public Column setComparator( SerializableComparator comparator) { Objects.requireNonNull(comparator, "Comparator can't be null"); this.comparator = comparator; return this; } /** * Gets the comparator to use with in-memory sorting for this column * when sorting in the given direction. * * @param sortDirection * the direction this column is sorted by * @return comparator for this column */ public SerializableComparator getComparator( SortDirection sortDirection) { Objects.requireNonNull(comparator, "No comparator defined for sorted column."); boolean reverse = sortDirection != SortDirection.ASCENDING; return reverse ? (t1, t2) -> comparator.reversed().compare(t1, t2) : comparator; } /** * Sets strings describing back end properties to be used when sorting * this column. *

* By default, the {@link #setId(String) column id} will be used as the * sort property. * * @param properties * the array of strings describing backend properties * @return this column */ public Column setSortProperty(String... properties) { Objects.requireNonNull(properties, "Sort properties can't be null"); return setSortOrderProvider(dir -> Arrays.stream(properties) .map(s -> new QuerySortOrder(s, dir))); } /** * Sets the sort orders when sorting this column. The sort order * provider is a function which provides {@link QuerySortOrder} objects * to describe how to sort by this column. *

* By default, the {@link #setId(String) column id} will be used as the * sort property. * * @param provider * the function to use when generating sort orders with the * given direction * @return this column */ public Column setSortOrderProvider(SortOrderProvider provider) { Objects.requireNonNull(provider, "Sort order provider can't be null"); sortOrderProvider = provider; // Update state updateSortable(); return this; } /** * Gets the sort orders to use with back-end sorting for this column * when sorting in the given direction. * * @see #setSortProperty(String...) * @see #setId(String) * @see #setSortOrderProvider(SortOrderProvider) * * @param direction * the sorting direction * @return stream of sort orders */ public Stream getSortOrder(SortDirection direction) { return sortOrderProvider.apply(direction); } /** * Sets the style generator that is used for generating class names for * cells in this column. Returning null from the generator results in no * custom style name being set. * * Note: The style generator is applied only to the body cells, not to * the Editor. * * @param cellStyleGenerator * the cell style generator to set, not null * @return this column * @throws NullPointerException * if {@code cellStyleGenerator} is {@code null} */ public Column setStyleGenerator( StyleGenerator cellStyleGenerator) { Objects.requireNonNull(cellStyleGenerator, "Cell style generator must not be null"); this.styleGenerator = cellStyleGenerator; getGrid().getDataCommunicator().reset(); return this; } /** * Gets the style generator that is used for generating styles for * cells. * * @return the cell style generator */ public StyleGenerator getStyleGenerator() { return styleGenerator; } /** * Sets the description generator that is used for generating * descriptions for cells in this column. This method uses the * {@link ContentMode#PREFORMATTED} content mode. * * @see #setDescriptionGenerator(DescriptionGenerator, ContentMode) * * @param cellDescriptionGenerator * the cell description generator to set, or {@code null} to * remove a previously set generator * @return this column */ public Column setDescriptionGenerator( DescriptionGenerator cellDescriptionGenerator) { return setDescriptionGenerator(cellDescriptionGenerator, ContentMode.PREFORMATTED); } /** * Sets the description generator that is used for generating * descriptions for cells in this column. This method uses the given * content mode. * * @see #setDescriptionGenerator(DescriptionGenerator) * * @param cellDescriptionGenerator * the cell description generator to set, or {@code null} to * remove a previously set generator * @param tooltipContentMode * the content mode for tooltips * @return this column * * @since 8.2 */ public Column setDescriptionGenerator( DescriptionGenerator cellDescriptionGenerator, ContentMode tooltipContentMode) { this.descriptionGenerator = cellDescriptionGenerator; getState().tooltipContentMode = tooltipContentMode; getGrid().getDataCommunicator().reset(); return this; } /** * Gets the description generator that is used for generating * descriptions for cells. * * @return the cell description generator, or null if no * generator is set */ public DescriptionGenerator getDescriptionGenerator() { return descriptionGenerator; } /** * Sets the ratio with which the column expands. *

* 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. *

* If a column has a defined width ({@link #setWidth(double)}), it * overrides this method's effects. *

* Example: A grid with three columns, with expand ratios 0, 1 * and 2, respectively. The column with a ratio of 0 is exactly * as wide as its contents requires. The column with a ratio of * 1 is as wide as it needs, plus a third of any excess * space, 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, plus two thirds 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(); if (expandRatio != getExpandRatio()) { getState().expandRatio = expandRatio; getGrid().markAsDirty(); } return this; } /** * Returns the column's expand ratio. * * @return the column's expand ratio * @see #setExpandRatio(int) */ public int getExpandRatio() { return getState(false).expandRatio; } /** * Clears the expand ratio for this column. *

* 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); } /** * Returns the width (in pixels). By default a column width is * {@value com.vaadin.shared.ui.grid.GridConstants#DEFAULT_COLUMN_WIDTH_PX} * (undefined). * * @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 getState(false).width; } /** * Sets the width (in pixels). *

* 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 (pixelWidth != getWidth()) { getState().width = pixelWidth; getGrid().markAsDirty(); getGrid().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 getState(false).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()) { getState().width = -1; getGrid().markAsDirty(); getGrid().fireColumnResizeEvent(this, false); } return this; } /** * Sets the minimum width for this column. *

* This defines the minimum guaranteed pixel width of the column * when it is set to expand. * * Note: Value -1 is not accepted, use {@link #setWidthUndefined()} * instead. * * @param pixels * the minimum width for the column * @throws IllegalStateException * if the column is no longer attached to any grid * @see #setExpandRatio(int) * @return the column itself */ 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; getGrid().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(false).minWidth; } /** * Sets whether the width of the contents in the column should be * considered minimum width for this column. *

* If this is set to true (default for backwards * compatibility), then a column will not shrink to smaller than the * width required to show the contents available when calculating the * widths (only the widths of the initially rendered rows are * considered). *

* If this is set to false and the column has been set to * expand using #setExpandRatio(int), then the contents of the column * will be ignored when calculating the width, and the column will thus * shrink down to the minimum width defined by #setMinimumWidth(double) * if necessary. * * @param minimumWidthFromContent * true to reserve space for all contents, * false to allow the column to shrink smaller * than the contents * @return the column itself * @throws IllegalStateException * if the column is no longer attached to any grid * @see #setMinimumWidth(double) * @since 8.1 */ public Column setMinimumWidthFromContent( boolean minimumWidthFromContent) throws IllegalStateException { checkColumnIsAttached(); if (isMinimumWidthFromContent() != minimumWidthFromContent) { getState().minimumWidthFromContent = minimumWidthFromContent; getGrid().markAsDirty(); } return this; } /** * Gets whether the width of the contents in the column should be * considered minimum width for this column. * * @return true to reserve space for all contents, * false to allow the column to shrink smaller than * the contents * @see #setMinimumWidthFromContent(boolean) * @since 8.1 */ public boolean isMinimumWidthFromContent() { return getState(false).minimumWidthFromContent; } /** * Sets the maximum width for this column. *

* This defines the maximum allowed pixel width of the column when * it is set to expand. * * @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; getGrid().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(false).maxWidth; } /** * 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 * @throws IllegalStateException * if the column is no longer attached to any grid */ public Column setResizable(boolean resizable) { checkColumnIsAttached(); if (resizable != isResizable()) { getState().resizable = resizable; getGrid().markAsDirty(); } 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 */ public String getHidingToggleCaption() { return getState(false).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}. *

* The default value is null, and in that case the column's * {@link #getCaption() header caption} is used. *

* NOTE: 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 */ public Column setHidingToggleCaption(String hidingToggleCaption) { if (hidingToggleCaption != getHidingToggleCaption()) { getState().hidingToggleCaption = hidingToggleCaption; } return this; } /** * Hides or shows the column. By default columns are visible before * explicitly hiding them. * * @since 7.5.0 * @param hidden * true to hide the column, false * to show * @return this column * @throws IllegalStateException * if the column is no longer attached to any grid */ public Column setHidden(boolean hidden) { checkColumnIsAttached(); if (hidden != isHidden()) { getState().hidden = hidden; getGrid().fireColumnVisibilityChangeEvent(this, hidden, false); } return this; } /** * Returns whether this column is hidden. Default is {@code false}. * * @since 7.5.0 * @return true if the column is currently hidden, * false otherwise */ public boolean isHidden() { return getState(false).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 * true if the column may be hidable by the user * via UI interaction * @return this column */ public Column setHidable(boolean hidable) { if (hidable != isHidable()) { getState().hidable = hidable; } return this; } /** * Returns whether this column can be hidden by the user. Default is * {@code false}. *

* Note: the column can be programmatically hidden using * {@link #setHidden(boolean)} regardless of the returned value. * * @since 7.5.0 * @return true if the user can hide the column, * false if not */ public boolean isHidable() { return getState(false).hidable; } /** * Returns whether this column can be resized by the user. Default is * {@code true}. *

* Note: 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(false).resizable; } /** * Sets whether this Column has a component displayed in Editor or not. * A column can only be editable if an editor component or binding has * been set. * * @param editable * {@code true} if column is editable; {@code false} if not * @return this column * * @see #setEditorComponent(HasValue, Setter) * @see #setEditorBinding(Binding) */ public Column setEditable(boolean editable) { Objects.requireNonNull(editorBinding, "Column has no editor binding or component defined"); getState().editable = editable; return this; } /** * Gets whether this Column has a component displayed in Editor or not. * * @return {@code true} if the column displays an editor component; * {@code false} if not */ public boolean isEditable() { return getState(false).editable; } /** * Sets an editor binding for this column. The {@link Binding} is used * when a row is in editor mode to define how to populate an editor * component based on the edited row and how to update an item based on * the value in the editor component. *

* To create a binding to use with a column, define a binding for the * editor binder (grid.getEditor().getBinder()) using e.g. * {@link Binder#forField(HasValue)}. You can also use * {@link #setEditorComponent(HasValue, Setter)} if no validator or * converter is needed for the binding. *

* The {@link HasValue} that the binding is defined to use must be a * {@link Component}. * * @param binding * the binding to use for this column * @return this column * * @see #setEditorComponent(HasValue, Setter) * @see Binding * @see Grid#getEditor() * @see Editor#getBinder() */ public Column setEditorBinding(Binding binding) { Objects.requireNonNull(binding, "null is not a valid editor field"); if (!(binding.getField() instanceof Component)) { throw new IllegalArgumentException( "Binding target must be a component."); } this.editorBinding = binding; return setEditable(true); } /** * Gets the binder binding that is currently used for this column. * * @return the used binder binding, or null if no binding * is configured * * @see #setEditorBinding(Binding) */ public Binding getEditorBinding() { return editorBinding; } /** * Sets a component and setter to use for editing values of this column * in the editor row. This is a shorthand for use in simple cases where * no validator or converter is needed. Use * {@link #setEditorBinding(Binding)} to support more complex cases. *

* Note: The same component cannot be used for multiple * columns. * * @param editorComponent * the editor component * @param setter * a setter that stores the component value in the row item * @return this column * * @see #setEditorBinding(Binding) * @see Grid#getEditor() * @see Binder#bind(HasValue, ValueProvider, Setter) */ public & Component> Column setEditorComponent( C editorComponent, Setter setter) { Objects.requireNonNull(editorComponent, "Editor component cannot be null"); Objects.requireNonNull(setter, "Setter cannot be null"); Binding binding = getGrid().getEditor().getBinder() .bind(editorComponent, valueProvider::apply, setter); return setEditorBinding(binding); } /** * Sets a component to use for editing values of this columns in the * editor row. This method can only be used if the column has an * {@link #setId(String) id} and the {@link Grid} has been created using * {@link Grid#Grid(Class)} or some other way that allows finding * properties based on property names. *

* This is a shorthand for use in simple cases where no validator or * converter is needed. Use {@link #setEditorBinding(Binding)} to * support more complex cases. *

* Note: The same component cannot be used for multiple * columns. * * @param editorComponent * the editor component * @return this column * * @see #setEditorBinding(Binding) * @see Grid#getEditor() * @see Binder#bind(HasValue, String) * @see Grid#Grid(Class) */ public & Component> Column setEditorComponent( C editorComponent) { Objects.requireNonNull(editorComponent, "Editor component cannot be null"); String propertyName = getId(); if (propertyName == null) { throw new IllegalStateException( "setEditorComponent without a setter can only be used if the column has an id. " + "Use another setEditorComponent(Component, Setter) or setEditorBinding(Binding) instead."); } Binding binding = getGrid().getEditor().getBinder() .bind(editorComponent, propertyName); return setEditorBinding(binding); } /** * Sets the Renderer for this Column. Setting the renderer will cause * all currently available row data to be recreated and sent to the * client. * * Note: Setting a new renderer will reset presentation provider if * it exists. * * @param renderer * the new renderer * @return this column * * @since 8.0.3 */ public Column setRenderer(Renderer renderer) { return setRenderer(ValueProvider.identity(), renderer); } /** * Sets the Renderer for this Column. Setting the renderer will cause * all currently available row data to be recreated and sent to the * client. *

* The presentation provider is a method that takes the value of this * column on a single row, and maps that to a value that the renderer * accepts. This feature can be used for storing a complex value in a * column for editing, but providing a simplified presentation for the * user when not editing. * * @param presentationProvider * the function to get presentations from the value of this * column, not {@code null} * @param renderer * the new renderer, not {@code null} * * @param

* the presentation type * * @return this column * * @since 8.1 */ public

Column setRenderer( ValueProvider presentationProvider, Renderer renderer) { Objects.requireNonNull(renderer, "Renderer can not be null"); Objects.requireNonNull(presentationProvider, "Presentation provider can not be null"); // Remove old renderer Connector oldRenderer = getState().renderer; if (oldRenderer instanceof Extension) { removeExtension((Extension) oldRenderer); } // Set new renderer getState().renderer = renderer; addExtension(renderer); this.presentationProvider = presentationProvider; // Trigger redraw getGrid().getDataCommunicator().reset(); return this; } /** * Gets the Renderer for this Column. * * @return the renderer * @since 8.1 */ public Renderer getRenderer() { return (Renderer) getState().renderer; } /** * Sets whether Grid should handle events in this Column from Components * and Widgets rendered by certain Renderers. By default the events are * not handled. *

* Note: Enabling this feature will for example select * a row when a component is clicked. For example in the case of a * {@link ComboBox} or {@link TextField} it might be problematic as the * component gets re-rendered and might lose focus. * * @param handleWidgetEvents * {@code true} to handle events; {@code false} to not * @return this column * @since 8.3 */ public Column setHandleWidgetEvents(boolean handleWidgetEvents) { getState().handleWidgetEvents = handleWidgetEvents; return this; } /** * Gets whether Grid is handling the events in this Column from * Component and Widgets. * * @see #setHandleWidgetEvents(boolean) * * @return {@code true} if handling events; {@code false} if not * @since 8.3 */ public boolean isHandleWidgetEvents() { return getState(false).handleWidgetEvents; } /** * Gets the grid that this column belongs to. * * @return the grid that this column belongs to, or null if * this column has not yet been associated with any grid */ @SuppressWarnings("unchecked") protected Grid getGrid() { return (Grid) getParent(); } /** * 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 (getGrid() == null) { throw new IllegalStateException( "Column is no longer attached to a grid."); } } /** * Writes the design attributes for this column into given element. * * @since 7.5.0 * * @param element * Element to write attributes into * * @param designContext * the design context */ protected void writeDesign(Element element, DesignContext designContext) { Attributes attributes = element.attributes(); ColumnState defaultState = new ColumnState(); if (getId() == null) { setId("column" + getGrid().getColumns().indexOf(this)); } DesignAttributeHandler.writeAttribute("column-id", attributes, getId(), null, String.class, designContext); // Sortable is a special attribute that depends on the data // provider. DesignAttributeHandler.writeAttribute("sortable", attributes, isSortable(), null, boolean.class, designContext); DesignAttributeHandler.writeAttribute("editable", attributes, isEditable(), defaultState.editable, boolean.class, designContext); DesignAttributeHandler.writeAttribute("resizable", attributes, isResizable(), defaultState.resizable, boolean.class, designContext); DesignAttributeHandler.writeAttribute("hidable", attributes, isHidable(), defaultState.hidable, boolean.class, designContext); DesignAttributeHandler.writeAttribute("hidden", attributes, isHidden(), defaultState.hidden, boolean.class, designContext); DesignAttributeHandler.writeAttribute("hiding-toggle-caption", attributes, getHidingToggleCaption(), defaultState.hidingToggleCaption, String.class, designContext); DesignAttributeHandler.writeAttribute("width", attributes, getWidth(), defaultState.width, Double.class, designContext); DesignAttributeHandler.writeAttribute("min-width", attributes, getMinimumWidth(), defaultState.minWidth, Double.class, designContext); DesignAttributeHandler.writeAttribute("max-width", attributes, getMaximumWidth(), defaultState.maxWidth, Double.class, designContext); DesignAttributeHandler.writeAttribute("expand", attributes, getExpandRatio(), defaultState.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 */ @SuppressWarnings("unchecked") protected void readDesign(Element design, DesignContext designContext) { Attributes attributes = design.attributes(); if (design.hasAttr("sortable")) { setSortable(DesignAttributeHandler.readAttribute("sortable", attributes, boolean.class)); } else { setSortable(false); } if (design.hasAttr("editable")) { /** * This is a fake editor just to have something (otherwise * "setEditable" throws an exception. * * Let's use TextField here because we support only Strings as * inline data type. It will work incorrectly for other types * but we don't support them anyway. */ setEditorComponent((HasValue & Component) new TextField(), (item, value) -> { // Ignore user value since we don't know the setter }); 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)); } if (design.hasAttr("assistive-caption")) { setAssistiveCaption(DesignAttributeHandler.readAttribute( "assistive-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)); } } } /** * Gets the DataGenerator for this Column. * * @return data generator */ private DataGenerator getDataGenerator() { return dataGenerator; } } private class HeaderImpl extends Header { @Override protected Grid getGrid() { return Grid.this; } @Override protected SectionState getState(boolean markAsDirty) { return Grid.this.getState(markAsDirty).header; } @Override protected Column getColumnByInternalId(String internalId) { return getGrid().getColumnByInternalId(internalId); } @Override @SuppressWarnings("unchecked") protected String getInternalIdForColumn(Column column) { return getGrid().getInternalIdForColumn((Column) column); } }; private class FooterImpl extends Footer { @Override protected Grid getGrid() { return Grid.this; } @Override protected SectionState getState(boolean markAsDirty) { return Grid.this.getState(markAsDirty).footer; } @Override protected Column getColumnByInternalId(String internalId) { return getGrid().getColumnByInternalId(internalId); } @Override @SuppressWarnings("unchecked") protected String getInternalIdForColumn(Column column) { return getGrid().getInternalIdForColumn((Column) column); } }; private final Set> columnSet = new LinkedHashSet<>(); private final Map> columnKeys = new HashMap<>(); private final Map> columnIds = new HashMap<>(); private final List> sortOrder = new ArrayList<>(); private final DetailsManager detailsManager; private final Set extensionComponents = new HashSet<>(); private StyleGenerator styleGenerator = item -> null; private DescriptionGenerator descriptionGenerator; private final Header header = new HeaderImpl(); private final Footer footer = new FooterImpl(); private int counter = 0; private GridSelectionModel selectionModel; private Editor editor; private PropertySet propertySet; private Class beanType = null; /** * Creates a new grid without support for creating columns based on property * names. Use an alternative constructor, such as {@link Grid#Grid(Class)}, * to create a grid that automatically sets up columns based on the type of * presented data. * * @see #Grid(Class) * @see #withPropertySet(PropertySet) */ public Grid() { this(new DataCommunicator<>()); } /** * Creates a new grid that uses reflection based on the provided bean type * to automatically set up an initial set of columns. All columns will be * configured using the same {@link Object#toString()} renderer that is used * by {@link #addColumn(ValueProvider)}. * * @param beanType * the bean type to use, not null * @see #Grid() * @see #withPropertySet(PropertySet) */ public Grid(Class beanType) { this(beanType, new DataCommunicator<>()); } /** * Creates a new grid that uses custom data communicator and provided bean * type * * It uses reflection of the provided bean type to automatically set up an * initial set of columns. All columns will be configured using the same * {@link Object#toString()} renderer that is used by * {@link #addColumn(ValueProvider)}. * * @param beanType * the bean type to use, not null * @param dataCommunicator * the data communicator to use, notnull * @since 8.0.7 */ protected Grid(Class beanType, DataCommunicator dataCommunicator) { this(BeanPropertySet.get(beanType), dataCommunicator); this.beanType = beanType; } /** * Creates a new grid with the given data communicator and without support * for creating columns based on property names. * * @param dataCommunicator * the custom data communicator to set * @see #Grid() * @see #Grid(PropertySet, DataCommunicator) * @since 8.0.7 */ protected Grid(DataCommunicator dataCommunicator) { this(new PropertySet() { @Override public Stream> getProperties() { // No columns configured by default return Stream.empty(); } @Override public Optional> getProperty(String name) { throw new IllegalStateException( "A Grid created without a bean type class literal or a custom property set" + " doesn't support finding properties by name."); } }, dataCommunicator); } /** * Creates a grid using a custom {@link PropertySet} implementation for * configuring the initial columns and resolving property names for * {@link #addColumn(String)} and * {@link Column#setEditorComponent(HasValue)}. * * @see #withPropertySet(PropertySet) * * @param propertySet * the property set implementation to use, not null. */ protected Grid(PropertySet propertySet) { this(propertySet, new DataCommunicator<>()); } /** * Creates a grid using a custom {@link PropertySet} implementation and * custom data communicator. *

* Property set is used for configuring the initial columns and resolving * property names for {@link #addColumn(String)} and * {@link Column#setEditorComponent(HasValue)}. * * @see #withPropertySet(PropertySet) * * @param propertySet * the property set implementation to use, not null. * @param dataCommunicator * the data communicator to use, notnull * @since 8.0.7 */ protected Grid(PropertySet propertySet, DataCommunicator dataCommunicator) { super(dataCommunicator); registerRpc(new GridServerRpcImpl()); setDefaultHeaderRow(appendHeaderRow()); setSelectionModel(new SingleSelectionModelImpl<>()); detailsManager = new DetailsManager<>(); addExtension(detailsManager); addDataGenerator(detailsManager); addDataGenerator((item, json) -> { String styleName = styleGenerator.apply(item); if (styleName != null && !styleName.isEmpty()) { json.put(GridState.JSONKEY_ROWSTYLE, styleName); } if (descriptionGenerator != null) { String description = descriptionGenerator.apply(item); if (description != null && !description.isEmpty()) { json.put(GridState.JSONKEY_ROWDESCRIPTION, description); } } }); setPropertySet(propertySet); // Automatically add columns for all available properties propertySet.getProperties().map(PropertyDefinition::getName) .forEach(this::addColumn); } @Override public void beforeClientResponse(boolean initial) { super.beforeClientResponse(initial); if (initial && editor.isOpen()) { // Re-attaching grid. Any old editor should be closed. editor.cancel(); } } /** * Sets the property set to use for this grid. Does not create or update * columns in any way but will delete and re-create the editor. *

* This is only meant to be called from constructors and readDesign, at a * stage where it does not matter if you throw away the editor. * * @param propertySet * the property set to use * * @since 8.0.3 */ protected void setPropertySet(PropertySet propertySet) { Objects.requireNonNull(propertySet, "propertySet cannot be null"); this.propertySet = propertySet; if (editor instanceof Extension) { removeExtension((Extension) editor); } editor = createEditor(); if (editor instanceof Extension) { addExtension((Extension) editor); } } /** * Returns the property set used by this grid. * * @return propertySet the property set to return * @since 8.4 */ protected PropertySet getPropertySet() { return propertySet; } /** * Creates a grid using a custom {@link PropertySet} implementation for * creating a default set of columns and for resolving property names with * {@link #addColumn(String)} and * {@link Column#setEditorComponent(HasValue)}. *

* This functionality is provided as static method instead of as a public * constructor in order to make it possible to use a custom property set * without creating a subclass while still leaving the public constructors * focused on the common use cases. * * @see Grid#Grid() * @see Grid#Grid(Class) * * @param propertySet * the property set implementation to use, not null. * @return a new grid using the provided property set, not null */ public static Grid withPropertySet( PropertySet propertySet) { return new Grid<>(propertySet); } /** * Creates a new {@code Grid} using the given caption. * * @param caption * the caption of the grid */ public Grid(String caption) { this(); setCaption(caption); } /** * Creates a new {@code Grid} using the given caption and * {@code DataProvider}. * * @param caption * the caption of the grid * @param dataProvider * the data provider, not {@code null} */ public Grid(String caption, DataProvider dataProvider) { this(caption); setDataProvider(dataProvider); } /** * Creates a new {@code Grid} using the given {@code DataProvider}. * * @param dataProvider * the data provider, not {@code null} */ public Grid(DataProvider dataProvider) { this(); setDataProvider(dataProvider); } /** * Creates a new {@code Grid} using the given caption and collection of * items. * * @param caption * the caption of the grid * @param items * the data items to use, not {@çode null} */ public Grid(String caption, Collection items) { this(caption, DataProvider.ofCollection(items)); } /** * Gets the bean type used by this grid. *

* The bean type is used to automatically set up a column added using a * property name. * * @return the used bean type or null if no bean type has been * defined * * @since 8.0.3 */ public Class getBeanType() { return beanType; } public void fireColumnVisibilityChangeEvent(Column column, boolean hidden, boolean userOriginated) { fireEvent(new ColumnVisibilityChangeEvent(this, column, hidden, userOriginated)); } /** * Adds a new column with the given property name. The column will use a * {@link TextRenderer}. The value is converted to a String using * {@link Object#toString()}. The property name will be used as the * {@link Column#getId() column id} and the {@link Column#getCaption() * column caption} will be set based on the property definition. *

* This method can only be used for a Grid created using * {@link Grid#Grid(Class)} or {@link #withPropertySet(PropertySet)}. *

* You can add columns for nested properties with dot notation, eg. * "property.nestedProperty" * * @param propertyName * the property name of the new column, not null * @return the newly added column, not null */ public Column addColumn(String propertyName) { return addColumn(propertyName, new TextRenderer()); } /** * Adds a new column with the given property name and renderer. The property * name will be used as the {@link Column#getId() column id} and the * {@link Column#getCaption() column caption} will be set based on the * property definition. *

* This method can only be used for a Grid created using * {@link Grid#Grid(Class)} or {@link #withPropertySet(PropertySet)}. *

* You can add columns for nested properties with dot notation, eg. * "property.nestedProperty" * * * @param propertyName * the property name of the new column, not null * @param renderer * the renderer to use, not null * @return the newly added column, not null */ public Column addColumn(String propertyName, AbstractRenderer renderer) { Objects.requireNonNull(propertyName, "Property name cannot be null"); Objects.requireNonNull(renderer, "Renderer cannot be null"); if (getColumn(propertyName) != null) { throw new IllegalStateException( "There is already a column for " + propertyName); } PropertyDefinition definition = propertySet .getProperty(propertyName) .orElseThrow(() -> new IllegalArgumentException( "Could not resolve property name " + propertyName + " from " + propertySet)); if (!renderer.getPresentationType() .isAssignableFrom(definition.getType())) { throw new IllegalArgumentException( renderer + " cannot be used with a property of type " + definition.getType().getName()); } @SuppressWarnings({ "unchecked", "rawtypes" }) Column column = addColumn(definition.getGetter(), (AbstractRenderer) renderer).setId(definition.getName()) .setCaption(definition.getCaption()); return column; } /** * Adds a new column with the given property name and renderer. The property * name will be used as the {@link Column#getId() column id} and the * {@link Column#getCaption() column caption} will be set based on the * property definition. *

* This method can only be used for a Grid created using * {@link Grid#Grid(Class)} or {@link #withPropertySet(PropertySet)}. *

* You can add columns for nested properties with dot notation, eg. * "property.nestedProperty" * * @param propertyName * the property name of the new column, not null * @param renderer * the renderer to use, not null * @param nestedNullBehavior * the behavior when * @return the newly added column, not null * * @since 8.8 */ public Column addColumn(String propertyName, AbstractRenderer renderer, Column.NestedNullBehavior nestedNullBehavior) { Objects.requireNonNull(propertyName, "Property name cannot be null"); Objects.requireNonNull(renderer, "Renderer cannot be null"); if (getColumn(propertyName) != null) { throw new IllegalStateException( "There is already a column for " + propertyName); } PropertyDefinition definition = propertySet .getProperty(propertyName) .orElseThrow(() -> new IllegalArgumentException( "Could not resolve property name " + propertyName + " from " + propertySet)); if (!renderer.getPresentationType() .isAssignableFrom(definition.getType())) { throw new IllegalArgumentException( renderer + " cannot be used with a property of type " + definition.getType().getName()); } @SuppressWarnings({ "unchecked", "rawtypes" }) Column column = createColumn(definition.getGetter(), ValueProvider.identity(), (AbstractRenderer) renderer, nestedNullBehavior); String generatedIdentifier = getGeneratedIdentifier(); addColumn(generatedIdentifier, column); column.setId(definition.getName()).setCaption(definition.getCaption()); return column; } /** * Adds a new text column to this {@link Grid} with a value provider. The * column will use a {@link TextRenderer}. The value is converted to a * String using {@link Object#toString()}. In-memory sorting will use the * natural ordering of elements if they are mutually comparable and * otherwise fall back to comparing the string representations of the * values. * * @param valueProvider * the value provider * * @return the new column */ public Column addColumn(ValueProvider valueProvider) { return addColumn(valueProvider, new TextRenderer()); } /** * Adds a new column to this {@link Grid} with typed renderer and value * provider. * * @param valueProvider * the value provider * @param renderer * the column value renderer * @param * the column value type * * @return the new column * * @see AbstractRenderer */ public Column addColumn(ValueProvider valueProvider, AbstractRenderer renderer) { return addColumn(valueProvider, ValueProvider.identity(), renderer); } /** * Adds a new column to this {@link Grid} with value provider and * presentation provider. *

* Note: The presentation type for this method is set to be * String. To use any custom renderer with the presentation provider, use * {@link #addColumn(ValueProvider, ValueProvider, AbstractRenderer)}. * * @param valueProvider * the value provider * @param presentationProvider * the value presentation provider * @param * the column value type * * @see #addColumn(ValueProvider, ValueProvider, AbstractRenderer) * * @return the new column * @since 8.1 */ public Column addColumn(ValueProvider valueProvider, ValueProvider presentationProvider) { return addColumn(valueProvider, presentationProvider, new TextRenderer()); } /** * Adds a new column to this {@link Grid} with value provider, presentation * provider and typed renderer. * *

* The presentation provider is a method that takes the value from the value * provider, and maps that to a value that the renderer accepts. This * feature can be used for storing a complex value in a column for editing, * but providing a simplified presentation for the user when not editing. * * @param valueProvider * the value provider * @param presentationProvider * the value presentation provider * @param renderer * the column value renderer * @param * the column value type * @param

* the column presentation type * * @return the new column * * @see AbstractRenderer * @since 8.1 */ public Column addColumn(ValueProvider valueProvider, ValueProvider presentationProvider, AbstractRenderer renderer) { String generatedIdentifier = getGeneratedIdentifier(); Column column = createColumn(valueProvider, presentationProvider, renderer); addColumn(generatedIdentifier, column); return column; } /** * Adds a column that shows components. *

* This is a shorthand for {@link #addColumn()} with a * {@link ComponentRenderer}. * * @param componentProvider * a value provider that will return a component for the given * item * @return the new column * @param * the column value type, extends component * @since 8.1 */ public Column addComponentColumn( ValueProvider componentProvider) { return addColumn(componentProvider, new ComponentRenderer()); } /** * Creates a column instance from a value provider, presentation provider * and a renderer. * * @param valueProvider * the value provider * @param presentationProvider * the presentation provider * @param renderer * the renderer * @return a new column instance * @param * the column value type * @param

* the column presentation type * * @since 8.1 */ protected Column createColumn( ValueProvider valueProvider, ValueProvider presentationProvider, AbstractRenderer renderer) { return new Column<>(valueProvider, presentationProvider, renderer); } /** * Creates a column instance from a value provider, presentation provider * and a renderer. * * @param valueProvider * the value provider * @param presentationProvider * the presentation provider * @param renderer * the renderer * @param nestedNullBehavior * the behavior when facing nested null values * @return a new column instance * @param * the column value type * @param

* the column presentation type * * @since 8.8 */ private Column createColumn(ValueProvider valueProvider, ValueProvider presentationProvider, AbstractRenderer renderer, Column.NestedNullBehavior nestedNullBehavior) { return new Column<>(valueProvider, presentationProvider, renderer, nestedNullBehavior); } private void addColumn(String identifier, Column column) { if (getColumns().contains(column)) { return; } column.extend(this); columnSet.add(column); columnKeys.put(identifier, column); column.setInternalId(identifier); addDataGenerator(column.getDataGenerator()); getState().columnOrder.add(identifier); getHeader().addColumn(identifier); getFooter().addColumn(identifier); if (getDefaultHeaderRow() != null) { getDefaultHeaderRow().getCell(column).setText(column.getCaption()); } column.updateSortable(); } /** * Removes the given column from this {@link Grid}. * * Note: If you have Editor with binding in this Grid to this property, you need to remove that * using removeBinding method provided by Binder. * * @param column * the column to remove * * @throws IllegalArgumentException * if the column is not a valid one */ public void removeColumn(Column column) { if (columnSet.remove(column)) { String columnId = column.getInternalId(); int displayIndex = getState(false).columnOrder.indexOf(columnId); assert displayIndex != -1 : "Tried to remove a column which is not included in columnOrder. This should not be possible as all columns should be in columnOrder."; columnKeys.remove(columnId); columnIds.remove(column.getId()); column.remove(); removeDataGenerator(column.getDataGenerator()); getHeader().removeColumn(columnId); getFooter().removeColumn(columnId); getState(true).columnOrder.remove(columnId); // Remove column from sorted columns. List> filteredSortOrder = sortOrder.stream() .filter(order -> !order.getSorted().equals(column)) .collect(Collectors.toList()); if (filteredSortOrder.size() < sortOrder.size()) { setSortOrder(filteredSortOrder); } if (displayIndex < getFrozenColumnCount()) { setFrozenColumnCount(getFrozenColumnCount() - 1); } } else { throw new IllegalArgumentException("Column with id " + column.getId() + " cannot be removed from the grid"); } } /** * Removes the column with the given column id. * * @see #removeColumn(Column) * @see Column#setId(String) * * @param columnId * the id of the column to remove, not null */ public void removeColumn(String columnId) { removeColumn(getColumnOrThrow(columnId)); } /** * Removes all columns from this Grid. * * @since 8.0.2 */ public void removeAllColumns() { for (Column column : getColumns()) { removeColumn(column); } } /** * Requests that the column widths should be recalculated. *

* 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 8.1.1 */ public void recalculateColumnWidths() { getRpcProxy(GridClientRpc.class).recalculateColumnWidths(); } /** * Sets the details component generator. * * @param generator * the generator for details components */ public void setDetailsGenerator(DetailsGenerator generator) { this.detailsManager.setDetailsGenerator(generator); } /** * Sets the visibility of details component for given item. * * @param item * the item to show details for * @param visible * {@code true} if details component should be visible; * {@code false} if it should be hidden */ public void setDetailsVisible(T item, boolean visible) { detailsManager.setDetailsVisible(item, visible); } /** * Returns the visibility of details component for given item. * * @param item * the item to show details for * * @return {@code true} if details component should be visible; * {@code false} if it should be hidden */ public boolean isDetailsVisible(T item) { return detailsManager.isDetailsVisible(item); } /** * Gets an unmodifiable collection of all columns currently in this * {@link Grid}. * * @return unmodifiable collection of columns */ public List> getColumns() { return Collections.unmodifiableList(getState(false).columnOrder.stream() .map(columnKeys::get).collect(Collectors.toList())); } /** * Gets a {@link Column} of this grid by its identifying string. * * When you use the Grid constructor with bean class, the columns are * initialised with columnId being the property name. * * @see Column#setId(String) * * @param columnId * the identifier of the column to get * @return the column corresponding to the given column identifier, or * null if there is no such column */ public Column getColumn(String columnId) { return columnIds.get(columnId); } private Column getColumnOrThrow(String columnId) { Objects.requireNonNull(columnId, "Column id cannot be null"); Column column = getColumn(columnId); if (column == null) { throw new IllegalStateException( "There is no column with the id " + columnId); } return column; } /** * {@inheritDoc} *

* Note that the order of the returned components it not specified. */ @Override public Iterator iterator() { Set componentSet = new LinkedHashSet<>(extensionComponents); Header header = getHeader(); for (int i = 0; i < header.getRowCount(); ++i) { HeaderRow row = header.getRow(i); componentSet.addAll(row.getComponents()); } Footer footer = getFooter(); for (int i = 0; i < footer.getRowCount(); ++i) { FooterRow row = footer.getRow(i); componentSet.addAll(row.getComponents()); } return Collections.unmodifiableSet(componentSet).iterator(); } /** * 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. *

* NOTE: this count includes {@link Column#isHidden() hidden * columns} in the count. *

* The default value is 0. * * @param numberOfColumns * the number of columns that should be frozen * * @throws IllegalArgumentException * if the column count is less than -1 or greater than the * number of visible columns */ public void setFrozenColumnCount(int numberOfColumns) { if (numberOfColumns < -1 || numberOfColumns > columnSet.size()) { throw new IllegalArgumentException( "count must be between -1 and the current number of columns (" + columnSet.size() + "): " + numberOfColumns); } int currentFrozenColumnState = getState(false).frozenColumnCount; /* * we remove the current value from the state so that setting frozen * columns will always happen after this call. This is so that the value * will be set also in the widget even if it happens to seem to be the * same as this current value we're setting. */ if (currentFrozenColumnState != numberOfColumns) { final String diffStateKey = "frozenColumnCount"; UI ui = getUI(); if (ui != null) { JsonObject diffState = ui.getConnectorTracker() .getDiffState(Grid.this); // if diffState is not present, there's nothing for us to clean if (diffState != null) { diffState.remove(diffStateKey); } } } 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. *

* NOTE: 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; } /** * Sets the number of rows that should be visible in Grid's body. This * method will set the height mode to be {@link HeightMode#ROW}. * * @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. * @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."); } if (Double.isInfinite(rows)) { throw new IllegalArgumentException( "Grid doesn't support infinite heights"); } if (Double.isNaN(rows)) { throw new IllegalArgumentException("NaN is not a valid row count"); } getState().heightMode = HeightMode.ROW; getState().heightByRows = rows; } /** * Gets the amount of rows in Grid's body that are shown, while * {@link #getHeightMode()} is {@link HeightMode#ROW}. * * @return the amount of rows that are being shown in Grid's body * @see #setHeightByRows(double) */ public double getHeightByRows() { return getState(false).heightByRows; } /** * {@inheritDoc} *

* Note: This method will set the height mode to be * {@link HeightMode#CSS}. * * @see #setHeightMode(HeightMode) */ @Override public void setHeight(float height, Unit unit) { getState().heightMode = HeightMode.CSS; super.setHeight(height, unit); } /** * Defines the mode in which the Grid widget's height is calculated. *

* If {@link HeightMode#CSS} is given, Grid will respect the values given * via a {@code setHeight}-method, and behave as a traditional Component. *

* If {@link HeightMode#ROW} is given, Grid will make sure that the body * will display as many rows as {@link #getHeightByRows()} defines. * Note: If headers/footers are inserted or removed, the widget * will resize itself to still display the required amount of rows in its * body. It also takes the horizontal scrollbar into account. * * @param heightMode * the mode in to which Grid should be set */ public void setHeightMode(HeightMode heightMode) { /** * This method is a workaround for the fact that Vaadin re-applies * widget dimensions (height/width) on each state change event. The * original design was to have setHeight 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. *

* Defaults to {@link HeightMode#CSS}. * * @return the current HeightMode */ public HeightMode getHeightMode() { return getState(false).heightMode; } /** * Sets the height of body, header and footer rows. If -1 (default), the row * height is calculated based on the theme for an empty row before the Grid * is displayed. *

* Note that all header, body and footer rows get the same height if * explicitly set. In automatic mode, each section is calculated separately * based on an empty row of that type. * * @see #setBodyRowHeight(double) * @see #setHeaderRowHeight(double) * @see #setFooterRowHeight(double) * * @param rowHeight * The height of a row in pixels or -1 for automatic calculation */ public void setRowHeight(double rowHeight) { setBodyRowHeight(rowHeight); setHeaderRowHeight(rowHeight); setFooterRowHeight(rowHeight); } /** * Sets the height of a body row. If -1 (default), the row height is * calculated based on the theme for an empty row before the Grid is * displayed. * * @param rowHeight * The height of a row in pixels or -1 for automatic calculation * @since 8.2 */ public void setBodyRowHeight(double rowHeight) { getState().bodyRowHeight = rowHeight; } /** * Sets the height of a header row. If -1 (default), the row height is * calculated based on the theme for an empty row before the Grid is * displayed. * * @param rowHeight * The height of a row in pixels or -1 for automatic calculation * @since 8.2 */ public void setHeaderRowHeight(double rowHeight) { getState().headerRowHeight = rowHeight; } /** * Sets the height of a footer row. If -1 (default), the row height is * calculated based on the theme for an empty row before the Grid is * displayed. * * @param rowHeight * The height of a row in pixels or -1 for automatic calculation * @since 8.2 */ public void setFooterRowHeight(double rowHeight) { getState().footerRowHeight = rowHeight; } /** * Returns the current body row height.-1 if row height is in automatic * calculation mode. * * @see #getBodyRowHeight() * @see #getHeaderRowHeight() * @see #getFooterRowHeight() * * @return body row height * @deprecated replaced by three separate row height controls */ @Deprecated public double getRowHeight() { return getBodyRowHeight(); } /** * Returns the current body row height. -1 if row height is in automatic * calculation mode. * * @return body row height * @since 8.2 */ public double getBodyRowHeight() { return getState(false).bodyRowHeight; } /** * Returns the current header row height. -1 if row height is in automatic * calculation mode. * * @return header row height * @since 8.2 */ public double getHeaderRowHeight() { return getState(false).headerRowHeight; } /** * Returns the current footer row height. -1 if row height is in automatic * calculation mode. * * @return footer row height * @since 8.2 */ public double getFooterRowHeight() { return getState(false).footerRowHeight; } /** * Sets the style generator that is used for generating class names for rows * in this grid. Returning null from the generator results in no custom * style name being set. * * Note: The style generator is applied only to the body cells, not to the * Editor. * * @see StyleGenerator * * @param styleGenerator * the row style generator to set, not null * @throws NullPointerException * if {@code styleGenerator} is {@code null} */ public void setStyleGenerator(StyleGenerator styleGenerator) { Objects.requireNonNull(styleGenerator, "Style generator must not be null"); this.styleGenerator = styleGenerator; getDataCommunicator().reset(); } /** * Gets the style generator that is used for generating class names for * rows. * * @see StyleGenerator * * @return the row style generator */ public StyleGenerator getStyleGenerator() { return styleGenerator; } /** * Sets the description generator that is used for generating descriptions * for rows. This method uses the {@link ContentMode#PREFORMATTED} content * mode. * * @see #setDescriptionGenerator(DescriptionGenerator, ContentMode) * * @param descriptionGenerator * the row description generator to set, or null to * remove a previously set generator */ public void setDescriptionGenerator( DescriptionGenerator descriptionGenerator) { setDescriptionGenerator(descriptionGenerator, ContentMode.PREFORMATTED); } /** * Sets the description generator that is used for generating descriptions * for rows. This method uses the given content mode. * * @see #setDescriptionGenerator(DescriptionGenerator) * * @param descriptionGenerator * the row description generator to set, or {@code null} to * remove a previously set generator * @param contentMode * the content mode for row tooltips * * @since 8.2 */ public void setDescriptionGenerator( DescriptionGenerator descriptionGenerator, ContentMode contentMode) { Objects.requireNonNull(contentMode, "contentMode cannot be null"); this.descriptionGenerator = descriptionGenerator; getState().rowDescriptionContentMode = contentMode; getDataCommunicator().reset(); } /** * Gets the description generator that is used for generating descriptions * for rows. * * @return the row description generator, or null if no * generator is set */ public DescriptionGenerator getDescriptionGenerator() { return descriptionGenerator; } // // HEADER AND FOOTER // /** * Returns the header row at the given index. * * @param index * the index of the row, where the topmost row has index zero * @return the header row at the index * @throws IndexOutOfBoundsException * if {@code rowIndex < 0 || rowIndex >= getHeaderRowCount()} */ public HeaderRow getHeaderRow(int index) { return getHeader().getRow(index); } /** * Gets the number of rows in the header section. * * @return the number of header rows */ public int getHeaderRowCount() { return header.getRowCount(); } /** * 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). Inserting at {@link #getHeaderRowCount()} appends the row * at the bottom of the header. * * @param index * the index at which to insert the row, where the topmost row * has index zero * @return the inserted header row * * @throws IndexOutOfBoundsException * if {@code rowIndex < 0 || rowIndex > getHeaderRowCount()} * * @see #appendHeaderRow() * @see #prependHeaderRow() * @see #removeHeaderRow(HeaderRow) * @see #removeHeaderRow(int) */ public HeaderRow addHeaderRowAt(int index) { return getHeader().addRowAt(index); } /** * Adds a new row at the bottom of the header section. * * @return the appended header row * * @see #prependHeaderRow() * @see #addHeaderRowAt(int) * @see #removeHeaderRow(HeaderRow) * @see #removeHeaderRow(int) */ public HeaderRow appendHeaderRow() { return addHeaderRowAt(getHeaderRowCount()); } /** * Adds a new row at the top of the header section. * * @return the prepended header row * * @see #appendHeaderRow() * @see #addHeaderRowAt(int) * @see #removeHeaderRow(HeaderRow) * @see #removeHeaderRow(int) */ public HeaderRow prependHeaderRow() { return addHeaderRowAt(0); } /** * Removes the given row from the header section. Removing a default row * sets the Grid to have no default row. * * @param row * the header row to be removed, not null * * @throws IllegalArgumentException * if the header does not contain the row * * @see #removeHeaderRow(int) * @see #addHeaderRowAt(int) * @see #appendHeaderRow() * @see #prependHeaderRow() */ public void removeHeaderRow(HeaderRow row) { getHeader().removeRow(row); } /** * Removes the row at the given position from the header section. * * @param index * the index of the row to remove, where the topmost row has * index zero * * @throws IndexOutOfBoundsException * if {@code index < 0 || index >= getHeaderRowCount()} * * @see #removeHeaderRow(HeaderRow) * @see #addHeaderRowAt(int) * @see #appendHeaderRow() * @see #prependHeaderRow() */ public void removeHeaderRow(int index) { getHeader().removeRow(index); } /** * Sets the visibility of the Header in this Grid. * * @param headerVisible * {@code true} if visible; {@code false} if not * * @since 8.1.1 */ public void setHeaderVisible(boolean headerVisible) { getHeader().setVisible(headerVisible); } /** * Gets the visibility of the Header in this Grid. * * @return {@code true} if visible; {@code false} if not * * @since 8.1.1 */ public boolean isHeaderVisible() { return getHeader().isVisible(); } /** * Returns the current default row of the header. * * @return the default row or null if no default row set * * @see #setDefaultHeaderRow(HeaderRow) */ public HeaderRow getDefaultHeaderRow() { return header.getDefaultRow(); } /** * Sets the default row of the header. The default row is a special header * row that displays column captions and sort indicators. By default Grid * has a single row which is also the default row. When a header row is set * as the default row, any existing cell content is replaced by the column * captions. * * @param row * the new default row, or null for no default row * * @throws IllegalArgumentException * if the header does not contain the row */ public void setDefaultHeaderRow(HeaderRow row) { header.setDefaultRow((Row) row); } /** * Returns the header section of this grid. The default header contains a * single row, set as the {@linkplain #setDefaultHeaderRow(HeaderRow) * default row}. * * @return the header section */ protected Header getHeader() { return header; } /** * Returns the footer row at the given index. * * @param index * the index of the row, where the topmost row has index zero * @return the footer row at the index * @throws IndexOutOfBoundsException * if {@code rowIndex < 0 || rowIndex >= getFooterRowCount()} */ public FooterRow getFooterRow(int index) { return getFooter().getRow(index); } /** * Gets the number of rows in the footer section. * * @return the number of footer rows */ public int getFooterRowCount() { return getFooter().getRowCount(); } /** * 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). Inserting at {@link #getFooterRowCount()} appends the row * at the bottom of the footer. * * @param index * the index at which to insert the row, where the topmost row * has index zero * @return the inserted footer row * * @throws IndexOutOfBoundsException * if {@code rowIndex < 0 || rowIndex > getFooterRowCount()} * * @see #appendFooterRow() * @see #prependFooterRow() * @see #removeFooterRow(FooterRow) * @see #removeFooterRow(int) */ public FooterRow addFooterRowAt(int index) { return getFooter().addRowAt(index); } /** * Adds a new row at the bottom of the footer section. * * @return the appended footer row * * @see #prependFooterRow() * @see #addFooterRowAt(int) * @see #removeFooterRow(FooterRow) * @see #removeFooterRow(int) */ public FooterRow appendFooterRow() { return addFooterRowAt(getFooterRowCount()); } /** * Adds a new row at the top of the footer section. * * @return the prepended footer row * * @see #appendFooterRow() * @see #addFooterRowAt(int) * @see #removeFooterRow(FooterRow) * @see #removeFooterRow(int) */ public FooterRow prependFooterRow() { return addFooterRowAt(0); } /** * Removes the given row from the footer section. Removing a default row * sets the Grid to have no default row. * * @param row * the footer row to be removed, not null * * @throws IllegalArgumentException * if the footer does not contain the row * * @see #removeFooterRow(int) * @see #addFooterRowAt(int) * @see #appendFooterRow() * @see #prependFooterRow() */ public void removeFooterRow(FooterRow row) { getFooter().removeRow(row); } /** * Removes the row at the given position from the footer section. * * @param index * the index of the row to remove, where the topmost row has * index zero * * @throws IndexOutOfBoundsException * if {@code index < 0 || index >= getFooterRowCount()} * * @see #removeFooterRow(FooterRow) * @see #addFooterRowAt(int) * @see #appendFooterRow() * @see #prependFooterRow() */ public void removeFooterRow(int index) { getFooter().removeRow(index); } /** * Sets the visibility of the Footer in this Grid. * * @param footerVisible * {@code true} if visible; {@code false} if not * * @since 8.1.1 */ public void setFooterVisible(boolean footerVisible) { getFooter().setVisible(footerVisible); } /** * Gets the visibility of the Footer in this Grid. * * @return {@code true} if visible; {@code false} if not * * @since 8.1.1 */ public boolean isFooterVisible() { return getFooter().isVisible(); } /** * Returns the footer section of this grid. * * @return the footer section */ protected Footer getFooter() { return footer; } /** * Registers a new column reorder listener. * * @param listener * the listener to register, not null * @return a registration for the listener */ public Registration addColumnReorderListener( ColumnReorderListener listener) { return addListener(ColumnReorderEvent.class, listener, COLUMN_REORDER_METHOD); } /** * Registers a new column resize listener. * * @param listener * the listener to register, not null * @return a registration for the listener */ public Registration addColumnResizeListener(ColumnResizeListener listener) { return addListener(ColumnResizeEvent.class, listener, COLUMN_RESIZE_METHOD); } /** * Adds an item click listener. The listener is called when an item of this * {@code Grid} is clicked. * * @param listener * the item click listener, not null * @return a registration for the listener * @see #addContextClickListener */ public Registration addItemClickListener( ItemClickListener listener) { return addListener(GridConstants.ITEM_CLICK_EVENT_ID, ItemClick.class, listener, ITEM_CLICK_METHOD); } /** * Adds a context click listener that gets notified when a context click * happens. * * @param listener * the context click listener to add, not null actual event * provided to the listener is {@link GridContextClickEvent} * @return a registration object for removing the listener * * @since 8.1 * @see #addItemClickListener * @see Registration */ @Override public Registration addContextClickListener( ContextClickEvent.ContextClickListener listener) { return super.addContextClickListener(listener); } /** * Registers a new column visibility change listener. * * @param listener * the listener to register, not null * @return a registration for the listener */ public Registration addColumnVisibilityChangeListener( ColumnVisibilityChangeListener listener) { return addListener(ColumnVisibilityChangeEvent.class, listener, COLUMN_VISIBILITY_METHOD); } /** * Returns whether column reordering is allowed. Default value is * false. * * @return true if reordering is allowed */ public boolean isColumnReorderingAllowed() { return getState(false).columnReorderingAllowed; } /** * Sets whether or not column reordering is allowed. Default value is * false. * * @param columnReorderingAllowed * specifies whether column reordering is allowed */ public void setColumnReorderingAllowed(boolean columnReorderingAllowed) { if (isColumnReorderingAllowed() != columnReorderingAllowed) { getState().columnReorderingAllowed = columnReorderingAllowed; } } /** * Sets the columns and their order based on their column ids. Columns * currently in this grid that are not present in the list of column ids are * removed. This includes any column that has no id. Similarly, any new * column in columns will be added to this grid. New columns can only be * added for a Grid created using {@link Grid#Grid(Class)} or * {@link #withPropertySet(PropertySet)}. * * * @param columnIds * the column ids to set * * @see Column#setId(String) */ public void setColumns(String... columnIds) { // Must extract to an explicitly typed variable because otherwise javac // cannot determine which overload of setColumnOrder to use Column[] newColumnOrder = Stream.of(columnIds) .map((Function>) id -> { Column column = getColumn(id); if (column == null) { column = addColumn(id); } return column; }).toArray(Column[]::new); setColumnOrder(newColumnOrder); // The columns to remove are now at the end of the column list getColumns().stream().skip(columnIds.length) .forEach(this::removeColumn); } private String getGeneratedIdentifier() { String columnId = "" + counter; counter++; return columnId; } /** * 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 columns * the columns in the order they should be */ public void setColumnOrder(Column... columns) { setColumnOrder(Stream.of(columns)); } private void setColumnOrder(Stream> columns) { List columnOrder = new ArrayList<>(); columns.forEach(column -> { if (columnSet.contains(column)) { columnOrder.add(column.getInternalId()); } else { throw new IllegalStateException( "setColumnOrder should not be called " + "with columns that are not in the grid."); } }); List stateColumnOrder = getState().columnOrder; if (stateColumnOrder.size() != columnOrder.size()) { stateColumnOrder.removeAll(columnOrder); columnOrder.addAll(stateColumnOrder); } getState().columnOrder = columnOrder; fireColumnReorderEvent(false); } /** * Sets a new column order for the grid based on their column ids. All * columns which are not ordered here will remain in the order they were * before as the last columns of grid. * * @param columnIds * the column ids in the order they should be * * @see Column#setId(String) */ public void setColumnOrder(String... columnIds) { setColumnOrder(Stream.of(columnIds).map(this::getColumnOrThrow)); } /** * Returns the selection model for this grid. * * @return the selection model, not null */ public GridSelectionModel getSelectionModel() { assert selectionModel != null : "No selection model set by " + getClass().getName() + " constructor"; return selectionModel; } /** * Use this grid as a single select in {@link Binder}. *

* Throws {@link IllegalStateException} if the grid is not using a * {@link SingleSelectionModel}. * * @return the single select wrapper that can be used in binder * @throws IllegalStateException * if not using a single selection model */ public GridSingleSelect asSingleSelect() { return new GridSingleSelect<>(this); } public Editor getEditor() { return editor; } /** * User this grid as a multiselect in {@link Binder}. *

* Throws {@link IllegalStateException} if the grid is not using a * {@link MultiSelectionModel}. * * @return the multiselect wrapper that can be used in binder * @throws IllegalStateException * if not using a multiselection model */ public GridMultiSelect asMultiSelect() { return new GridMultiSelect<>(this); } /** * Sets the selection model for the grid. *

* This method is for setting a custom selection model, and is * {@code protected} because {@link #setSelectionMode(SelectionMode)} should * be used for easy switching between built-in selection models. *

* The default selection model is {@link SingleSelectionModelImpl}. *

* To use a custom selection model, you can e.g. extend the grid call this * method with your custom selection model. * * @param model * the selection model to use, not {@code null} * * @see #setSelectionMode(SelectionMode) */ @SuppressWarnings("unchecked") protected void setSelectionModel(GridSelectionModel model) { Objects.requireNonNull(model, "selection model cannot be null"); if (selectionModel != null) { // null when called from constructor selectionModel.remove(); } selectionModel = model; if (selectionModel instanceof AbstractListingExtension) { ((AbstractListingExtension) selectionModel).extend(this); } else { addExtension(selectionModel); } } /** * Sets the grid's selection mode. *

* The built-in selection models are: *

    *
  • {@link SelectionMode#SINGLE} -> {@link SingleSelectionModelImpl}, * the default model
  • *
  • {@link SelectionMode#MULTI} -> {@link MultiSelectionModelImpl}, with * checkboxes in the first column for selection
  • *
  • {@link SelectionMode#NONE} -> {@link NoSelectionModel}, preventing * selection
  • *
*

* To use your custom selection model, you can use * {@link #setSelectionModel(GridSelectionModel)}, see existing selection * model implementations for example. * * @param selectionMode * the selection mode to switch to, not {@code null} * @return the used selection model * * @see SelectionMode * @see GridSelectionModel * @see #setSelectionModel(GridSelectionModel) */ public GridSelectionModel setSelectionMode(SelectionMode selectionMode) { Objects.requireNonNull(selectionMode, "Selection mode cannot be null."); GridSelectionModel model = selectionMode.createModel(); setSelectionModel(model); return model; } /** * This method is a shorthand that delegates to the currently set selection * model. * * @see #getSelectionModel() * @see GridSelectionModel */ public Set getSelectedItems() { return getSelectionModel().getSelectedItems(); } /** * This method is a shorthand that delegates to the currently set selection * model. * * @see #getSelectionModel() * @see GridSelectionModel */ public void select(T item) { getSelectionModel().select(item); } /** * This method is a shorthand that delegates to the currently set selection * model. * * @see #getSelectionModel() * @see GridSelectionModel */ public void deselect(T item) { getSelectionModel().deselect(item); } /** * This method is a shorthand that delegates to the currently set selection * model. * * @see #getSelectionModel() * @see GridSelectionModel */ public void deselectAll() { getSelectionModel().deselectAll(); } /** * Adds a selection listener to the current selection model. *

* NOTE: If selection mode is switched with * {@link #setSelectionMode(SelectionMode)}, then this listener is not * triggered anymore when selection changes! *

* This is a shorthand for * {@code grid.getSelectionModel().addSelectionListener()}. To get more * detailed selection events, use {@link #getSelectionModel()} and either * {@link SingleSelectionModel#addSingleSelectionListener(SingleSelectionListener)} * or * {@link MultiSelectionModel#addMultiSelectionListener(MultiSelectionListener)} * depending on the used selection mode. * * @param listener * the listener to add * @return a registration handle to remove the listener * @throws UnsupportedOperationException * if selection has been disabled with * {@link SelectionMode#NONE} */ public Registration addSelectionListener(SelectionListener listener) throws UnsupportedOperationException { return getSelectionModel().addSelectionListener(listener); } /** * Sort this Grid in ascending order by a specified column. * * @param column * a column to sort against * */ public void sort(Column column) { sort(column, SortDirection.ASCENDING); } /** * Sort this Grid in user-specified direction by a column. * * @param column * a column to sort against * @param direction * a sort order value (ascending/descending) * */ public void sort(Column column, SortDirection direction) { setSortOrder(Collections .singletonList(new GridSortOrder<>(column, direction))); } /** * Sort this Grid in ascending order by a specified column defined by id. * * @param columnId * the id of the column to sort against * * @see Column#setId(String) */ public void sort(String columnId) { sort(columnId, SortDirection.ASCENDING); } /** * Sort this Grid in a user-specified direction by a column defined by id. * * @param columnId * the id of the column to sort against * @param direction * a sort order value (ascending/descending) * * @see Column#setId(String) */ public void sort(String columnId, SortDirection direction) { sort(getColumnOrThrow(columnId), direction); } /** * Clear the current sort order, and re-sort the grid. */ public void clearSortOrder() { setSortOrder(Collections.emptyList()); } /** * Sets the sort order to use. * * @param order * a sort order list. * * @throws IllegalArgumentException * if order is null */ public void setSortOrder(List> order) { setSortOrder(order, false); } /** * Sets the sort order to use, given a {@link GridSortOrderBuilder}. * Shorthand for {@code setSortOrder(builder.build())}. * * @see GridSortOrderBuilder * * @param builder * the sort builder to retrieve the sort order from * @throws NullPointerException * if builder is null */ public void setSortOrder(GridSortOrderBuilder builder) { Objects.requireNonNull(builder, "Sort builder cannot be null"); setSortOrder(builder.build()); } /** * 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) { return addListener(SortEvent.class, listener, SORT_ORDER_CHANGE_METHOD); } /** * Get the current sort order list. * * @return a sort order list */ public List> getSortOrder() { return Collections.unmodifiableList(sortOrder); } /** * Scrolls to a certain item, using {@link ScrollDestination#ANY}. *

* If the item has an open details row, its size will also be taken into * account. * * @param row * zero based index of the item to scroll to in the current view. * @throws IllegalArgumentException * if the provided row is outside the item range */ public void scrollTo(int row) throws IllegalArgumentException { scrollTo(row, ScrollDestination.ANY); } /** * Scrolls to a certain item, using user-specified scroll destination. *

* If the item has an open details row, its size will also be taken into * account. * * @param row * zero based index of the item to scroll to in the current view. * @param destination * value specifying desired position of scrolled-to row, not * {@code null} * @throws IllegalArgumentException * if the provided row is outside the item range */ public void scrollTo(int row, ScrollDestination destination) { Objects.requireNonNull(destination, "ScrollDestination can not be null"); if (row >= getDataCommunicator().getDataProviderSize()) { throw new IllegalArgumentException("Row outside dataProvider size"); } getRpcProxy(GridClientRpc.class).scrollToRow(row, destination); } /** * Scrolls to the beginning of the first data row. */ public void scrollToStart() { getRpcProxy(GridClientRpc.class).scrollToStart(); } /** * Scrolls to the end of the last data row. */ public void scrollToEnd() { getRpcProxy(GridClientRpc.class).scrollToEnd(); } @Override protected GridState getState() { return getState(true); } @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 Editor instance. Can be overridden to create a custom * Editor. If the Editor is a {@link AbstractGridExtension}, it will be * automatically added to {@link DataCommunicator}. * * @return editor */ protected Editor createEditor() { return new EditorImpl<>(propertySet); } private void addExtensionComponent(Component c) { if (extensionComponents.add(c)) { c.setParent(this); markAsDirty(); } } private void removeExtensionComponent(Component c) { if (extensionComponents.remove(c)) { c.setParent(null); markAsDirty(); } } private void fireColumnReorderEvent(boolean userOriginated) { fireEvent(new ColumnReorderEvent(this, userOriginated)); } private void fireColumnResizeEvent(Column column, boolean userOriginated) { fireEvent(new ColumnResizeEvent(this, column, userOriginated)); } @Override protected void readItems(Element design, DesignContext context) { // Grid handles reading of items in Grid#readData } @Override public DataProvider getDataProvider() { return internalGetDataProvider(); } @Override public void setDataProvider(DataProvider dataProvider) { internalSetDataProvider(dataProvider); } /** * Sets a CallbackDataProvider using the given fetch items callback and a * size callback. *

* This method is a shorthand for making a {@link CallbackDataProvider} that * handles a partial {@link Query} object. * * @param fetchItems * a callback for fetching items * @param sizeCallback * a callback for getting the count of items * * @see CallbackDataProvider * @see #setDataProvider(DataProvider) */ public void setDataProvider(FetchItemsCallback fetchItems, SerializableSupplier sizeCallback) { internalSetDataProvider( new CallbackDataProvider<>( q -> fetchItems.fetchItems(q.getSortOrders(), q.getOffset(), q.getLimit()), q -> sizeCallback.get())); } @Override protected void doReadDesign(Element design, DesignContext context) { Attributes attrs = design.attributes(); if (design.hasAttr(DECLARATIVE_DATA_ITEM_TYPE)) { String itemType = design.attr(DECLARATIVE_DATA_ITEM_TYPE); setBeanType(itemType); } if (attrs.hasKey("selection-mode")) { setSelectionMode(DesignAttributeHandler.readAttribute( "selection-mode", attrs, SelectionMode.class)); } Attributes attr = design.attributes(); if (attr.hasKey("selection-allowed")) { setReadOnly(DesignAttributeHandler .readAttribute("selection-allowed", attr, Boolean.class)); } if (attrs.hasKey("rows")) { setHeightByRows(DesignAttributeHandler.readAttribute("rows", attrs, double.class)); } readStructure(design, context); // Read frozen columns after columns are read. if (attrs.hasKey("frozen-columns")) { setFrozenColumnCount(DesignAttributeHandler .readAttribute("frozen-columns", attrs, int.class)); } } /** * Sets the bean type to use for property mapping. *

* This method is responsible also for setting or updating the property set * so that it matches the given bean type. *

* Protected mostly for Designer needs, typically should not be overridden * or even called. * * @param beanTypeClassName * the fully qualified class name of the bean type * * @since 8.0.3 */ @SuppressWarnings("unchecked") protected void setBeanType(String beanTypeClassName) { setBeanType((Class) resolveClass(beanTypeClassName)); } /** * Sets the bean type to use for property mapping. *

* This method is responsible also for setting or updating the property set * so that it matches the given bean type. *

* Protected mostly for Designer needs, typically should not be overridden * or even called. * * @param beanType * the bean type class * * @since 8.0.3 */ protected void setBeanType(Class beanType) { this.beanType = beanType; setPropertySet(BeanPropertySet.get(beanType)); } private Class resolveClass(String qualifiedClassName) { try { Class resolvedClass = Class.forName(qualifiedClassName, true, VaadinServiceClassLoaderUtil.findDefaultClassLoader()); return resolvedClass; } catch (ClassNotFoundException | SecurityException e) { throw new IllegalArgumentException( "Unable to find class " + qualifiedClassName, e); } } @Override protected void doWriteDesign(Element design, DesignContext designContext) { Attributes attr = design.attributes(); if (this.beanType != null) { design.attr(DECLARATIVE_DATA_ITEM_TYPE, this.beanType.getCanonicalName()); } DesignAttributeHandler.writeAttribute("selection-allowed", attr, isReadOnly(), false, Boolean.class, designContext); Attributes attrs = design.attributes(); Grid defaultInstance = designContext.getDefaultInstance(this); DesignAttributeHandler.writeAttribute("frozen-columns", attrs, getFrozenColumnCount(), defaultInstance.getFrozenColumnCount(), int.class, designContext); if (HeightMode.ROW.equals(getHeightMode())) { DesignAttributeHandler.writeAttribute("rows", attrs, getHeightByRows(), defaultInstance.getHeightByRows(), double.class, designContext); } SelectionMode mode = getSelectionMode(); if (mode != null) { DesignAttributeHandler.writeAttribute("selection-mode", attrs, mode, SelectionMode.SINGLE, SelectionMode.class, designContext); } writeStructure(design, designContext); } @Override protected T deserializeDeclarativeRepresentation(String item) { if (item == null) { return super.deserializeDeclarativeRepresentation( UUID.randomUUID().toString()); } return super.deserializeDeclarativeRepresentation(new String(item)); } @Override protected boolean isReadOnly() { SelectionMode selectionMode = getSelectionMode(); if (SelectionMode.SINGLE.equals(selectionMode)) { return asSingleSelect().isReadOnly(); } if (SelectionMode.MULTI.equals(selectionMode)) { return asMultiSelect().isReadOnly(); } return false; } @Override protected void setReadOnly(boolean readOnly) { SelectionMode selectionMode = getSelectionMode(); if (SelectionMode.SINGLE.equals(selectionMode)) { asSingleSelect().setReadOnly(readOnly); } else if (SelectionMode.MULTI.equals(selectionMode)) { asMultiSelect().setReadOnly(readOnly); } } private void readStructure(Element design, DesignContext context) { if (design.children().isEmpty()) { return; } 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"); } List> providers = new ArrayList<>(); for (Element col : colgroups.get(0).getElementsByTag("col")) { String id = DesignAttributeHandler.readAttribute("column-id", col.attributes(), null, String.class); // If there is a property with a matching name available, // map to that Optional> property = propertySet .getProperties().filter(p -> p.getName().equals(id)) .findFirst(); Column column; if (property.isPresent()) { column = addColumn(id); } else { DeclarativeValueProvider provider = new DeclarativeValueProvider<>(); column = createColumn(provider, ValueProvider.identity(), new HtmlRenderer()); addColumn(getGeneratedIdentifier(), column); if (id != null) { column.setId(id); } providers.add(provider); } column.readDesign(col, context); } for (Element child : table.children()) { if (child.tagName().equals("thead")) { getHeader().readDesign(child, context); } else if (child.tagName().equals("tbody")) { readData(child, providers); } else if (child.tagName().equals("tfoot")) { getFooter().readDesign(child, context); } } // Sync default header captions to column captions if (getDefaultHeaderRow() != null) { for (Column c : getColumns()) { HeaderCell headerCell = getDefaultHeaderRow().getCell(c); if (headerCell.getCellType() == GridStaticCellType.TEXT) { c.setCaption(headerCell.getText()); } } } } /** * Reads the declarative representation of a grid's data from the given * element and stores it in the given {@link DeclarativeValueProvider}s. * Each member in the list of value providers corresponds to a column in the * grid. * * @since 8.1 * * @param body * the element to read data from * @param providers * list of {@link DeclarativeValueProvider}s to store the data of * each column to */ protected void readData(Element body, List> providers) { getSelectionModel().deselectAll(); List items = new ArrayList<>(); List selectedItems = new ArrayList<>(); for (Element row : body.children()) { T item = deserializeDeclarativeRepresentation(row.attr("item")); items.add(item); if (row.hasAttr("selected")) { selectedItems.add(item); } Elements cells = row.children(); int i = 0; for (Element cell : cells) { providers.get(i).addValue(item, cell.html()); i++; } } setItems(items); selectedItems.forEach(getSelectionModel()::select); } private void writeStructure(Element design, DesignContext designContext) { if (getColumns().isEmpty()) { return; } Element tableElement = design.appendElement("table"); Element colGroup = tableElement.appendElement("colgroup"); getColumns().forEach(column -> column .writeDesign(colGroup.appendElement("col"), designContext)); // Always write thead. Reads correctly when there no header rows getHeader().writeDesign(tableElement.appendElement("thead"), designContext); if (designContext.shouldWriteData(this)) { Element bodyElement = tableElement.appendElement("tbody"); writeData(bodyElement, designContext); } if (getFooter().getRowCount() > 0) { getFooter().writeDesign(tableElement.appendElement("tfoot"), designContext); } } /** * Writes the data contained in this grid. Used when serializing a grid to * its declarative representation, if * {@link DesignContext#shouldWriteData(Component)} returns {@code true} for * the grid that is being written. * * @since 8.1 * * @param body * the body element to write the declarative representation of * data to * @param designContext * the design context * * @since 8.1 */ protected void writeData(Element body, DesignContext designContext) { getDataProvider().fetch(new Query<>()) .forEach(item -> writeRow(body, item, designContext)); } private void writeRow(Element container, T item, DesignContext context) { Element tableRow = container.appendElement("tr"); tableRow.attr("item", serializeDeclarativeRepresentation(item)); if (getSelectionModel().isSelected(item)) { tableRow.attr("selected", true); } for (Column column : getColumns()) { Object value = column.valueProvider.apply(item); tableRow.appendElement("td") .append(Optional.ofNullable(value).map(Object::toString) .map(DesignFormatter::encodeForTextNode) .orElse("")); } } private SelectionMode getSelectionMode() { GridSelectionModel selectionModel = getSelectionModel(); SelectionMode mode = null; if (selectionModel.getClass().equals(SingleSelectionModelImpl.class)) { mode = SelectionMode.SINGLE; } else if (selectionModel.getClass() .equals(MultiSelectionModelImpl.class)) { mode = SelectionMode.MULTI; } else if (selectionModel.getClass().equals(NoSelectionModel.class)) { mode = SelectionMode.NONE; } return mode; } /** * Sets a user-defined identifier for given column. * * @see Column#setId(String) * * @param column * the column * @param id * the user-defined identifier */ protected void setColumnId(String id, Column column) { if (columnIds.containsKey(id)) { throw new IllegalArgumentException("Duplicate ID for columns"); } columnIds.put(id, column); } @Override protected Collection getCustomAttributes() { Collection result = super.getCustomAttributes(); // "rename" for frozen column count result.add("frozen-column-count"); result.add("frozen-columns"); // "rename" for height-mode result.add("height-by-rows"); result.add("rows"); // add a selection-mode attribute result.add("selection-mode"); return result; } /** * Returns a column identified by its internal id. This id should not be * confused with the user-defined identifier. * * @param columnId * the internal id of column * @return column identified by internal id */ protected Column getColumnByInternalId(String columnId) { return columnKeys.get(columnId); } /** * Returns the internal id for given column. This id should not be confused * with the user-defined identifier. * * @param column * the column * @return internal id of given column */ protected String getInternalIdForColumn(Column column) { return column.getInternalId(); } private void setSortOrder(List> order, boolean userOriginated) { Objects.requireNonNull(order, "Sort order list cannot be null"); // Update client state to display sort order. List sortColumns = new ArrayList<>(); List directions = new ArrayList<>(); order.stream().forEach(sortOrder -> { sortColumns.add(sortOrder.getSorted().getInternalId()); directions.add(sortOrder.getDirection()); }); getState().sortColumns = sortColumns.toArray(new String[0]); getState().sortDirs = directions.toArray(new SortDirection[0]); sortOrder.clear(); sortOrder.addAll(order); sort(userOriginated); } private void sort(boolean userOriginated) { // Set sort orders // In-memory comparator getDataCommunicator().setInMemorySorting(createSortingComparator(), false); // Back-end sort properties List sortProperties = new ArrayList<>(); sortOrder.stream().map( order -> order.getSorted().getSortOrder(order.getDirection())) .forEach(s -> s.forEach(sortProperties::add)); getDataCommunicator().setBackEndSorting(sortProperties, true); // Close grid editor if it's open. if (getEditor().isOpen()) { getEditor().cancel(); } fireEvent(new SortEvent<>(this, new ArrayList<>(sortOrder), userOriginated)); } /** * Creates a comparator for grid to sort rows. * * @return the comparator based on column sorting information. */ protected SerializableComparator createSortingComparator() { /* * thenComparing is defined to return a serializable comparator as long * as both original comparators are also serializable */ BinaryOperator> operator = (comparator1, comparator2) -> comparator1.thenComparing(comparator2)::compare; return sortOrder.stream().map( order -> order.getSorted().getComparator(order.getDirection())) .reduce((x, y) -> 0, operator); } @Override protected void internalSetDataProvider(DataProvider dataProvider) { super.internalSetDataProvider(dataProvider); for (Column column : getColumns()) { column.updateSortable(); } } }