/* * Copyright 2000-2016 Vaadin Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.vaadin.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.Set; import java.util.function.BinaryOperator; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; import org.jsoup.nodes.Element; import com.vaadin.data.Binder; import com.vaadin.data.SelectionModel; import com.vaadin.event.ConnectorEvent; import com.vaadin.event.ContextClickEvent; import com.vaadin.event.EventListener; import com.vaadin.server.EncodeResult; import com.vaadin.server.Extension; import com.vaadin.server.JsonCodec; import com.vaadin.server.SerializableComparator; import com.vaadin.server.SerializableFunction; import com.vaadin.server.data.SortOrder; 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.grid.ColumnState; 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.SectionState; import com.vaadin.shared.util.SharedUtil; import com.vaadin.ui.Grid.FooterRow; import com.vaadin.ui.components.grid.Footer; import com.vaadin.ui.components.grid.Header; import com.vaadin.ui.components.grid.Header.Row; import com.vaadin.ui.components.grid.SingleSelectionModel; import com.vaadin.ui.declarative.DesignContext; import com.vaadin.ui.renderers.AbstractRenderer; 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 { @Deprecated private static final Method COLUMN_REORDER_METHOD = ReflectTools.findMethod( ColumnReorderListener.class, "columnReorder", ColumnReorderEvent.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, "accept", ItemClick.class); @Deprecated private static final Method COLUMN_VISIBILITY_METHOD = ReflectTools .findMethod(ColumnVisibilityChangeListener.class, "columnVisibilityChanged", ColumnVisibilityChangeEvent.class); /** * An event listener for column reorder events in the Grid. */ @FunctionalInterface public interface ColumnReorderListener extends Serializable { /** * Called when the columns of the grid have been reordered. * * @param event * An event providing more information */ void columnReorder(ColumnReorderEvent event); } /** * The server-side interface that controls Grid's selection state. * SelectionModel should extend {@link AbstractGridExtension}. * * @param * the grid bean type */ public interface GridSelectionModel extends SelectionModel, Extension { /** * Removes this selection model from the grid. *

* Must call super {@link Extension#remove()} to detach the extension, * and fire an selection change event for the selection model (with an * empty selection). */ @Override void remove(); } /** * An event listener for column resize events in the Grid. */ @FunctionalInterface public interface ColumnResizeListener extends Serializable { /** * Called when the columns of the grid have been resized. * * @param event * An event providing more information */ void columnResize(ColumnResizeEvent event); } /** * An event that is fired when the columns are reordered. */ public static class ColumnReorderEvent extends Component.Event { 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 */ 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 { 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 */ 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; /** * 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) { super(source); this.column = column; this.item = item; this.mouseEventDetails = mouseEventDetails; } /** * 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; } } /** * A listener for item click events. * * @param * the grid bean type * * @see ItemClick * @see Registration */ @FunctionalInterface public interface ItemClickListener extends EventListener> { /** * Invoked when this listener receives a item click event from a Grid to * which it has been added. * * @param event * the received event, not null */ @Override public void accept(ItemClick event); } /** * ContextClickEvent for the Grid Component. * * @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 listener for column visibility change events in the Grid. * * @since 7.5.0 */ @FunctionalInterface public interface ColumnVisibilityChangeListener extends Serializable { /** * Called when a column has become hidden or unhidden. * * @param event */ void columnVisibilityChanged(ColumnVisibilityChangeEvent event); } /** * An event that is fired when a column's visibility changes. * * @since 7.5.0 */ public static class ColumnVisibilityChangeEvent extends Component.Event { private final Column column; private final boolean userOriginated; private final boolean hidden; /** * Constructor for a column visibility change event. * * @param source * the grid from which this event originates * @param column * the column that changed its visibility * @param hidden * true if the column was hidden, * false if it became visible * @param isUserOriginated * true iff the event was triggered by an UI * interaction */ public ColumnVisibilityChangeEvent(Grid source, Column column, boolean hidden, boolean isUserOriginated) { super(source); this.column = column; this.hidden = hidden; userOriginated = isUserOriginated; } /** * Gets the column that became hidden or visible. * * @return the column that became hidden or visible. * @see Column#isHidden() */ public Column getColumn() { return column; } /** * Was the column set hidden or visible. * * @return true if the column was hidden false * if it was set visible */ public boolean isHidden() { return hidden; } /** * 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 */ public boolean isUserOriginated() { return userOriginated; } } /** * A callback interface for generating description texts for an item. * * @param * the grid bean type */ @FunctionalInterface public interface DescriptionGenerator extends SerializableFunction { } /** * A callback interface for generating details for a particular row in Grid. * * @param * the grid bean type */ @FunctionalInterface public interface DetailsGenerator extends Function, Serializable { } /** * 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(); } } private final class GridServerRpcImpl implements GridServerRpc { @Override public void sort(String[] columnIds, SortDirection[] directions, boolean isUserOriginated) { assert columnIds.length == directions.length : "Column and sort direction counts don't match."; sortOrder.clear(); if (columnIds.length == 0) { // Grid is not sorted anymore. getDataCommunicator() .setBackEndSorting(Collections.emptyList()); getDataCommunicator().setInMemorySorting(null); return; } for (int i = 0; i < columnIds.length; ++i) { Column column = columnKeys.get(columnIds[i]); sortOrder.add(new SortOrder<>(column, directions[i])); } // Set sort orders // In-memory comparator BinaryOperator> operator = (comparator1, comparator2) -> SerializableComparator.asInstance( (Comparator & Serializable) comparator1 .thenComparing(comparator2)); SerializableComparator comparator = sortOrder.stream() .map(order -> order.getSorted() .getComparator(order.getDirection())) .reduce((x, y) -> 0, operator); getDataCommunicator().setInMemorySorting(comparator); // 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); } @Override public void itemClick(String rowKey, String columnId, MouseEventDetails details) { Column column = columnKeys.containsKey(columnId) ? columnKeys.get(columnId) : null; T item = getDataCommunicator().getKeyMapper().get(rowKey); fireEvent(new ItemClick<>(Grid.this, column, item, details)); } @Override public void contextClick(int rowIndex, String rowKey, String columnId, Section section, MouseEventDetails details) { T item = null; if (rowKey != null) { item = getDataCommunicator().getKeyMapper().get(rowKey); } fireEvent(new GridContextClickEvent<>(Grid.this, details, section, rowIndex, item, getColumn(columnId))); } @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() .getDeclaredField(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 id, boolean hidden) { Column column = getColumn(id); ColumnState columnState = column.getState(false); if (columnState.hidden != hidden) { columnState.hidden = hidden; fireColumnVisibilityChangeEvent(column, hidden, true); } } @Override public void columnResized(String id, double pixels) { final Column column = getColumn(id); if (column != null && column.isResizable()) { column.getState().width = pixels; fireColumnResizeEvent(column, true); markAsDirty(); } } } /** * 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(); } 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 data, JsonObject jsonObject) { if (generator == null || !visibleDetails.contains(data)) { return; } if (!components.containsKey(data)) { Component detailsComponent = generator.apply(data); 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(data, detailsComponent); } jsonObject.put(GridState.JSONKEY_DETAILS_VISIBLE, components.get(data).getConnectorId()); } @Override public void destroyData(T data) { // 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 data * 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 data, boolean visible) { boolean refresh = false; if (!visible) { refresh = visibleDetails.remove(data); if (components.containsKey(data)) { removeComponentFromGrid(components.remove(data)); } } else { refresh = visibleDetails.add(data); } if (refresh) { refresh(data); } } /** * Returns the visibility of details component for given item. * * @param data * 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 data) { return visibleDetails.contains(data); } @Override public Grid getParent() { return super.getParent(); } } /** * 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 AbstractGridExtension { private final SerializableFunction valueProvider; private SerializableFunction>> sortOrderProvider; private SerializableComparator comparator; private StyleGenerator styleGenerator = item -> null; private DescriptionGenerator descriptionGenerator; /** * Constructs a new Column configuration with given header caption, * renderer and value provider. * * @param caption * the header caption * @param valueProvider * the function to get values from items * @param renderer * the type of value */ protected Column(String caption, SerializableFunction valueProvider, Renderer renderer) { Objects.requireNonNull(caption, "Header caption can't be null"); Objects.requireNonNull(valueProvider, "Value provider can't be null"); Objects.requireNonNull(renderer, "Renderer can't be null"); ColumnState state = getState(); this.valueProvider = valueProvider; state.renderer = renderer; state.caption = caption; sortOrderProvider = d -> Stream.of(); // 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) -> { @SuppressWarnings("unchecked") Comparable comp = (Comparable) valueProvider.apply(a); return comp.compareTo(valueProvider.apply(b)); }; state.sortable = true; } 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) -> { return compareNumbers((Number) valueProvider.apply(a), (Number) valueProvider.apply(b)); }; state.sortable = true; } else { state.sortable = false; } } @SuppressWarnings("unchecked") private static int compareNumbers(Number a, Number b) { assert a.getClass() == b.getClass(); // Most Number implementations are Comparable if (a instanceof Comparable && a.getClass().isInstance(b)) { return ((Comparable) a).compareTo(b); } else if (a.equals(b)) { return 0; } else { // Fall back to comparing based on potentially truncated values int compare = Long.compare(a.longValue(), b.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(a.doubleValue(), b.doubleValue()); } return compare; } } @Override public void generateData(T data, JsonObject jsonObject) { ColumnState state = getState(false); String communicationId = getConnectorId(); assert communicationId != null : "No communication ID set for column " + state.caption; @SuppressWarnings("unchecked") Renderer renderer = (Renderer) state.renderer; JsonObject obj = getDataObject(jsonObject, DataCommunicatorConstants.DATA); V providerValue = valueProvider.apply(data); JsonValue rendererValue = renderer.encode(providerValue); obj.put(communicationId, rendererValue); String style = styleGenerator.apply(data); if (style != null && !style.isEmpty()) { JsonObject styleObj = getDataObject(jsonObject, GridState.JSONKEY_CELLSTYLES); styleObj.put(communicationId, style); } if (descriptionGenerator != null) { String description = descriptionGenerator.apply(data); if (description != null && !description.isEmpty()) { JsonObject descriptionObj = getDataObject(jsonObject, GridState.JSONKEY_CELLDESCRIPTION); descriptionObj.put(communicationId, description); } } } /** * 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 */ public String getId() { return getState(false).id; } /** * Sets the identifier to use with this Column in communication. * * @param id * the identifier string */ private void setId(String id) { Objects.requireNonNull(id, "Communication ID can't be null"); getState().id = id; } /** * Sets whether the user can sort this column or not. * * @param sortable * {@code true} if the column can be sorted by the user; * {@code false} if not * @return this column */ public Column setSortable(boolean sortable) { getState().sortable = sortable; return this; } /** * Gets whether the user can sort this column or not. * * @return {@code true} if the column can be sorted by the user; * {@code false} if not */ public boolean isSortable() { return getState(false).sortable; } /** * 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"); getState().caption = caption; HeaderRow row = getParent().getDefaultHeaderRow(); if (row != null) { row.getCell(getId()).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. This method is a short hand for * {@link #setSortBuilder(Function)} that takes an array of strings and * uses the same sorting direction for all of them. * * @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"); sortOrderProvider = dir -> Arrays.stream(properties) .map(s -> new SortOrder<>(s, dir)); return this; } /** * Sets the sort orders when sorting this column. The sort order * provider is a function which provides {@link SortOrder} objects to * describe how to sort by this column. * * @param provider * the function to use when generating sort orders with the * given direction * @return this column */ public Column setSortOrderProvider( SerializableFunction>> provider) { Objects.requireNonNull(provider, "Sort order provider can't be null"); sortOrderProvider = provider; return this; } /** * Gets the sort orders to use with back-end sorting for this column * when sorting in the given direction. * * @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. * * @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; getParent().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. * * @param cellDescriptionGenerator * the cell description generator to set, or * null to remove a previously set generator * @return this column */ public Column setDescriptionGenerator( DescriptionGenerator cellDescriptionGenerator) { this.descriptionGenerator = cellDescriptionGenerator; getParent().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; getParent().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 is 100px wide. * * @return the width in pixels of the column * @throws IllegalStateException * if the column is no longer attached to any grid */ public double getWidth() throws IllegalStateException { checkColumnIsAttached(); return 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; getParent().markAsDirty(); getParent().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; getParent().markAsDirty(); getParent().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. * * @throws IllegalStateException * if the column is no longer attached to any grid * @see #setExpandRatio(int) */ public Column setMinimumWidth(double pixels) throws IllegalStateException { checkColumnIsAttached(); final double maxwidth = getMaximumWidth(); if (pixels >= 0 && pixels > maxwidth && maxwidth >= 0) { throw new IllegalArgumentException("New minimum width (" + pixels + ") was greater than maximum width (" + maxwidth + ")"); } getState().minWidth = pixels; getParent().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 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; getParent().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; getParent().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 #getHeaderCaption() 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; getParent().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 iff 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; } /** * 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 (getParent() == null) { throw new IllegalStateException( "Column is no longer attached to a grid."); } } } /** * A header row in a Grid. */ public interface HeaderRow extends Serializable { /** * Returns the cell on this row corresponding to the given column id. * * @param columnId * the id of the column whose header cell to get, not null * @return the header cell * @throws IllegalArgumentException * if there is no such column in the grid */ public HeaderCell getCell(String columnId); /** * Returns the cell on this row corresponding to the given column. * * @param column * the column whose header cell to get, not null * @return the header cell * @throws IllegalArgumentException * if there is no such column in the grid */ public default HeaderCell getCell(Column column) { return getCell(column.getId()); } } /** * An individual cell on a Grid header row. */ public interface HeaderCell extends Serializable { /** * Returns the textual caption of this cell. * * @return the header caption */ public String getText(); /** * Sets the textual caption of this cell. * * @param text * the header caption to set, not null */ public void setText(String text); /** * Returns the HTML content displayed in this cell. * * @return the html * */ public String getHtml(); /** * Sets the HTML content displayed in this cell. * * @param html * the html to set */ public void setHtml(String html); /** * Returns the component displayed in this cell. * * @return the component */ public Component getComponent(); /** * Sets the component displayed in this cell. * * @param component * the component to set */ public void setComponent(Component component); /** * Returns the type of content stored in this cell. * * @return cell content type */ public GridStaticCellType getCellType(); } /** * A footer row in a Grid. */ public interface FooterRow extends Serializable { /** * Returns the cell on this row corresponding to the given column id. * * @param columnId * the id of the column whose footer cell to get, not null * @return the footer cell * @throws IllegalArgumentException * if there is no such column in the grid */ public FooterCell getCell(String columnId); /** * Returns the cell on this row corresponding to the given column. * * @param column * the column whose footer cell to get, not null * @return the footer cell * @throws IllegalArgumentException * if there is no such column in the grid */ public default FooterCell getCell(Column column) { return getCell(column.getId()); } } /** * An individual cell on a Grid footer row. */ public interface FooterCell extends Serializable { /** * Returns the textual caption of this cell. * * @return the footer caption */ public String getText(); /** * Sets the textual caption of this cell. * * @param text * the footer caption to set, not null */ public void setText(String text); /** * Returns the HTML content displayed in this cell. * * @return the html * */ public String getHtml(); /** * Sets the HTML content displayed in this cell. * * @param html * the html to set */ public void setHtml(String html); /** * Returns the component displayed in this cell. * * @return the component */ public Component getComponent(); /** * Sets the component displayed in this cell. * * @param component * the component to set */ public void setComponent(Component component); /** * Returns the type of content stored in this cell. * * @return cell content type */ public GridStaticCellType getCellType(); } 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 Collection> getColumns() { return Grid.this.getColumns(); } }; 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 Collection> getColumns() { return Grid.this.getColumns(); } }; private final Set> columnSet = new LinkedHashSet<>(); private final Map> columnKeys = 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; /** * Constructor for the {@link Grid} component. */ public Grid() { registerRpc(new GridServerRpcImpl()); setDefaultHeaderRow(appendHeaderRow()); selectionModel = new SingleSelectionModel<>(this); 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); } } }); } public void fireColumnVisibilityChangeEvent(Column column, boolean hidden, boolean userOriginated) { fireEvent(new ColumnVisibilityChangeEvent(this, column, hidden, userOriginated)); } /** * Adds a new column to this {@link Grid} with given identifier, typed * renderer and value provider. * * @param identifier * the identifier in camel case for the new column * @param valueProvider * the value provider * @param renderer * the column value class * @param * the column value type * * @return the new column * @throws IllegalArgumentException * if the same identifier is used for multiple columns * * @see {@link AbstractRenderer} */ public Column addColumn(String identifier, SerializableFunction valueProvider, AbstractRenderer renderer) throws IllegalArgumentException { if (columnKeys.containsKey(identifier)) { throw new IllegalArgumentException( "Multiple columns with the same identifier: " + identifier); } final Column column = new Column<>( SharedUtil.camelCaseToHumanFriendly(identifier), valueProvider, renderer); addColumn(identifier, column); return column; } /** * Adds a new text column to this {@link Grid} with given identifier and * string value provider. The column will use a {@link TextRenderer}. * * @param identifier * the header caption * @param valueProvider * the value provider * * @return the new column * @throws IllegalArgumentException * if the same identifier is used for multiple columns */ public Column addColumn(String identifier, SerializableFunction valueProvider) { return addColumn(identifier, valueProvider, new TextRenderer()); } /** * Adds a new text column to this {@link Grid} with string value provider. * The column will use a {@link TextRenderer}. Identifier for the column is * generated automatically. * * @param valueProvider * the value provider * * @return the new column */ public Column addColumn( SerializableFunction valueProvider) { return addColumn(getGeneratedIdentifier(), valueProvider, new TextRenderer()); } /** * Adds a new column to this {@link Grid} with typed renderer and value * provider. Identifier for the column is generated automatically. * * @param valueProvider * the value provider * @param renderer * the column value class * @param * the column value type * * @return the new column * * @see {@link AbstractRenderer} */ public Column addColumn( SerializableFunction valueProvider, AbstractRenderer renderer) { return addColumn(getGeneratedIdentifier(), valueProvider, renderer); } private void addColumn(String identifier, Column column) { if (getColumns().contains(column)) { return; } column.extend(this); columnSet.add(column); columnKeys.put(identifier, column); column.setId(identifier); addDataGenerator(column); getState().columnOrder.add(identifier); getHeader().addColumn(identifier); if (getDefaultHeaderRow() != null) { getDefaultHeaderRow().getCell(identifier) .setText(column.getCaption()); } } /** * Removes the given column from this {@link Grid}. * * @param column * the column to remove */ public void removeColumn(Column column) { if (columnSet.remove(column)) { columnKeys.remove(column.getId()); removeDataGenerator(column); getHeader().removeColumn(column.getId()); column.remove(); } } /** * 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 data * 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 data, boolean visible) { detailsManager.setDetailsVisible(data, visible); } /** * Returns the visibility of details component for given item. * * @param data * 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 data) { return detailsManager.isDetailsVisible(data); } /** * 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(this::getColumn).collect(Collectors.toList())); } /** * Gets a {@link Column} of this grid by its identifying string. * * @param columnId * the identifier of the column to get * @return the column corresponding to the given column id */ public Column getColumn(String columnId) { return columnKeys.get(columnId); } @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); getColumns().forEach(column -> { HeaderCell cell = row.getCell(column); if (cell.getCellType() == GridStaticCellType.WIDGET) { componentSet.add(cell.getComponent()); } }); } Footer footer = getFooter(); for (int i = 0; i < footer.getRowCount(); ++i) { FooterRow row = footer.getRow(i); getColumns().forEach(column -> { FooterCell cell = row.getCell(column); if (cell.getCellType() == GridStaticCellType.WIDGET) { componentSet.add(cell.getComponent()); } }); } 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. *

* 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); } 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. If null is given, then Grid's * height is undefined * @throws IllegalArgumentException * if {@code rows} is zero or less * @throws IllegalArgumentException * if {@code rows} is {@link Double#isInfinite(double) infinite} * @throws IllegalArgumentException * if {@code rows} is {@link Double#isNaN(double) NaN} */ public void setHeightByRows(double rows) { if (rows <= 0.0d) { throw new IllegalArgumentException( "More than zero rows must be shown."); } else if (Double.isInfinite(rows)) { throw new IllegalArgumentException( "Grid doesn't support infinite heights"); } else if (Double.isNaN(rows)) { throw new IllegalArgumentException("NaN is not a valid row count"); } getState().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 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. * * @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. * * @param descriptionGenerator * the row description generator to set, or null to * remove a previously set generator */ public void setDescriptionGenerator( DescriptionGenerator descriptionGenerator) { this.descriptionGenerator = descriptionGenerator; 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); } /** * 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); } /** * Returns the footer section of this grid. The default footer contains a * single row, set as the {@linkplain #setDefaultFooterRow(FooterRow) * default row}. * * @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 */ public Registration addItemClickListener( ItemClickListener listener) { return addListener(GridConstants.ITEM_CLICK_EVENT_ID, ItemClick.class, listener, ITEM_CLICK_METHOD); } /** * 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 for the grid. Columns currently in this * grid that are not present in columns are removed. Similarly, any new * column in columns will be added to this grid. * * @param columns * the columns to set */ public void setColumns(Column... columns) { List> currentColumns = getColumns(); Set> removeColumns = new HashSet<>(currentColumns); Set> addColumns = Arrays.stream(columns) .collect(Collectors.toSet()); removeColumns.removeAll(addColumns); removeColumns.stream().forEach(this::removeColumn); addColumns.removeAll(currentColumns); addColumns.stream().forEach(c -> addColumn(getIdentifier(c), c)); setColumnOrder(columns); } private String getIdentifier(Column column) { return columnKeys.entrySet().stream() .filter(entry -> entry.getValue().equals(column)) .map(entry -> entry.getKey()).findFirst() .orElse(getGeneratedIdentifier()); } private String getGeneratedIdentifier() { String columnId = "generatedColumn" + 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) { List columnOrder = new ArrayList<>(); for (Column column : columns) { if (columnSet.contains(column)) { columnOrder.add(column.getId()); } else { throw new IllegalArgumentException( "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); } /** * Returns the selection model for this listing. * * @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}. *

* Sets the grid to single select mode, if not yet so. * * @return the single select wrapper that can be used in binder */ public SingleSelect asSingleSelect() { GridSelectionModel model = getSelectionModel(); if (!(model instanceof SingleSelectionModel)) { model = new SingleSelectionModel<>(this); setSelectionModel(model); } return ((SingleSelectionModel) model).asSingleSelect(); } /** * Sets the selection model for this listing. *

* The default selection model is {@link SingleSelectionModel}. * * @param model * the selection model to use, not {@code null} */ protected void setSelectionModel(GridSelectionModel model) { Objects.requireNonNull(model, "selection model cannot be null"); selectionModel.remove(); selectionModel = model; } @Override protected GridState getState() { return getState(true); } @Override protected GridState getState(boolean markAsDirty) { return (GridState) super.getState(markAsDirty); } 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 Element writeItem(Element design, T item, DesignContext context) { // TODO see vaadin/framework8-issues#390 return null; } @Override protected void readItems(Element design, DesignContext context) { // TODO see vaadin/framework8-issues#390 } }