package au.com.vaadinutils.crud; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import javax.persistence.metamodel.SingularAttribute; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import com.ejt.vaadin.sizereporter.ComponentResizeEvent; import com.ejt.vaadin.sizereporter.ComponentResizeListener; import com.ejt.vaadin.sizereporter.SizeReporter; import com.google.common.base.Preconditions; import com.vaadin.data.Container.Indexed; import com.vaadin.data.util.GeneratedPropertyContainer; import com.vaadin.data.util.PropertyValueGenerator; import com.vaadin.data.util.converter.Converter; import com.vaadin.ui.Component; import com.vaadin.ui.Grid; import com.vaadin.ui.Grid.AbstractRenderer; import com.vaadin.ui.Grid.Column; import com.vaadin.ui.Grid.ColumnReorderEvent; import com.vaadin.ui.Grid.ColumnReorderListener; import com.vaadin.ui.Grid.ColumnResizeEvent; import com.vaadin.ui.Grid.ColumnResizeListener; import com.vaadin.ui.Grid.ColumnVisibilityChangeEvent; import com.vaadin.ui.Grid.ColumnVisibilityChangeListener; import com.vaadin.ui.renderers.HtmlRenderer; import com.vaadin.ui.renderers.TextRenderer; import au.com.vaadinutils.dao.Path; import au.com.vaadinutils.user.UserSettingsStorageFactory; import de.datenhahn.vaadin.componentrenderer.ComponentRenderer; public class GridHeadingPropertySet { private Logger logger = LogManager.getLogger(); private List<GridHeadingToPropertyId> cols = new LinkedList<>(); private boolean eraseSavedConfig = false; private Grid grid; private String uniqueId; private boolean dynamicColumnWidth = false; // Set to true if you would like to defer loading settings until // applySettingsToColumns is called private boolean deferLoadSettings = false; public GridHeadingPropertySet(final List<GridHeadingToPropertyId> cols) { this.cols = cols; } public static <E> Builder<E> getBuilder(Class<E> Class) { return new Builder<>(); } interface Start<E> { public AddingColumn<E> createColumn(String heading, String propertyId); public <T> AddingColumn<E> createColumn(String heading, SingularAttribute<E, T> headingPropertyId); public GridHeadingPropertySet build(); } public interface AddingColumn<E> { public AddingColumn<E> setLockedState(boolean lockedState); public AddingColumn<E> setDefaultVisibleState(boolean defaultVisibleState); public AddingColumn<E> setWidth(Integer width); public AddingColumn<E> setColumnGenerator(PropertyValueGenerator<?> columnGenerator); public AddingColumn<E> setRenderer(AbstractRenderer<?> renderer); public GridHeadingPropertySet build(); public AddingColumn<E> setConverter(Converter<String, ?> converter); public Builder<E> addColumn(); } public static class Builder<E> implements AddingColumn<E>, Start<E> { private List<GridHeadingToPropertyId> cols = new LinkedList<>(); private boolean eraseSavedConfig = false; private boolean dynamicColumnWidth = false; @Override public GridHeadingPropertySet build() { addColumn(); final GridHeadingPropertySet propertySet = new GridHeadingPropertySet(this.cols); propertySet.eraseSavedConfig = eraseSavedConfig; propertySet.dynamicColumnWidth = dynamicColumnWidth; return propertySet; } @Override public AddingColumn<E> setRenderer(AbstractRenderer<?> renderer) { columnBuilder.setRenderer(renderer); return this; } @Override public AddingColumn<E> setConverter(Converter<String, ?> converter) { columnBuilder.setConverter(converter); return this; } public void setEraseSavedConfig() { eraseSavedConfig = true; } /** * Setting dynamic column width ensures that the total width of columns * is always equal to the width of the component. This prevents * horizontal scrolling as well as ensures that the complete width of * the component is always utilised. If a column is resized, then all * columns to the right of it are also resized based on how much space * there is remaining to the edge of the component. */ public void setDynamicColumnWidth() { dynamicColumnWidth = true; } /* Add column methods */ /** * Add a new table column * * @param heading * the column heading that will be displayed * @param headingPropertyId * the heading property id * @param defaultVisibleState * whether the column is visible by default * @param lockedState * whether the visibility of a column can be modified * @param width * the width of the column * @return the builder */ public Builder<E> addColumn(final String heading, final String headingPropertyId, final boolean defaultVisibleState, final boolean lockedState, final int width) { createColumn(heading, headingPropertyId).setDefaultVisibleState(defaultVisibleState) .setLockedState(lockedState).setWidth(width); this.addColumn(); return this; } GridHeadingToPropertyId.Builder columnBuilder = null; @Override public AddingColumn<E> createColumn(String heading, String propertyId) { addColumn(); columnBuilder = new GridHeadingToPropertyId.Builder(heading, propertyId); return this; } @Override public <T> AddingColumn<E> createColumn(String heading, SingularAttribute<E, T> headingPropertyId) { addColumn(); columnBuilder = new GridHeadingToPropertyId.Builder(heading, headingPropertyId.getName()); return this; } @Override public Builder<E> addColumn() { if (columnBuilder != null) { cols.add(columnBuilder.build()); } // fail fast rather than have weird behaviour columnBuilder = null; return this; } @Override public AddingColumn<E> setLockedState(boolean lockedState) { columnBuilder.setLockedState(lockedState); return this; } @Override public AddingColumn<E> setDefaultVisibleState(boolean defaultVisibleState) { columnBuilder.setDefaultVisibleState(defaultVisibleState); return this; } @Override public AddingColumn<E> setWidth(Integer width) { columnBuilder.setWidth(width); return this; } @Override public AddingColumn<E> setColumnGenerator(PropertyValueGenerator<?> columnGenerator) { columnBuilder.setColumnGenerator(columnGenerator); return this; } public <T extends Object> Builder<E> addColumn(final String heading, final SingularAttribute<E, T> headingPropertyId, final boolean defaultVisibleState, final boolean lockedState, int width) { createColumn(heading, headingPropertyId.getName()).setDefaultVisibleState(defaultVisibleState) .setLockedState(lockedState).setWidth(width); this.addColumn(); return this; } public <T extends Object> Builder<E> addColumn(final String heading, final String headingPropertyId, final boolean defaultVisibleState, final boolean lockedState) { cols.add(new GridHeadingToPropertyId(heading, headingPropertyId, null, defaultVisibleState, lockedState, null)); return this; } public <T extends Object> Builder<E> addColumn(final String heading, final SingularAttribute<E, T> headingPropertyId, final boolean defaultVisibleState, final boolean lockedState) { return addColumn(heading, headingPropertyId.getName(), defaultVisibleState, lockedState); } /* Add generated column methods */ /** * Add a new generated table column * * @param heading * the column heading that will be displayed * @param headingPropertyId * the heading property id * @param columnGenerator * the column generator * @param defaultVisibleState * whether the column is visible by default * @param lockedState * whether the visibility of a column can be modified * @param width * the width of the column * @return the builder */ public Builder<E> addGeneratedColumn(final String heading, final String headingPropertyId, final PropertyValueGenerator<?> columnGenerator, final boolean defaultVisibleState, final boolean lockedState, final int width) { cols.add(new GridHeadingToPropertyId(heading, headingPropertyId, columnGenerator, defaultVisibleState, lockedState, width)); return this; } public <T extends Object> Builder<E> addGeneratedColumn(final String heading, final SingularAttribute<E, T> headingPropertyId, final PropertyValueGenerator<?> columnGenerator, final boolean defaultVisibleState, final boolean lockedState, int width) { return addGeneratedColumn(heading, headingPropertyId.getName(), columnGenerator, defaultVisibleState, lockedState, width); } public Builder<E> addGeneratedColumn(final String heading, final String headingPropertyId, final PropertyValueGenerator<?> columnGenerator, final boolean defaultVisibleState, final boolean lockedState) { cols.add(new GridHeadingToPropertyId(heading, headingPropertyId, columnGenerator, defaultVisibleState, lockedState, null)); return this; } public <T extends Object> Builder<E> addGeneratedColumn(final String heading, final SingularAttribute<E, T> headingPropertyId, final PropertyValueGenerator<?> columnGenerator, final boolean defaultVisibleState, final boolean lockedState) { return addGeneratedColumn(heading, headingPropertyId.getName(), columnGenerator, defaultVisibleState, lockedState); } public Builder<E> addGeneratedColumn(final String heading, final PropertyValueGenerator<?> columnGenerator, final boolean defaultVisibleState, final boolean lockedState, int width) { return addGeneratedColumn(heading, heading + "-generated", columnGenerator, defaultVisibleState, lockedState, width); } public Builder<E> addGeneratedColumn(final String heading, final PropertyValueGenerator<?> columnGenerator, final boolean defaultVisibleState, final boolean lockedState) { return addGeneratedColumn(heading, heading + "-generated", columnGenerator, defaultVisibleState, lockedState); } /* Add column convenience methods */ public <T extends Object> Builder<E> addColumn(final String heading, final String headingPropertyId, final int width) { return addColumn(heading, headingPropertyId, true, false, width); } public <T extends Object> Builder<E> addColumn(final String heading, final SingularAttribute<E, T> headingPropertyId, final int width) { return addColumn(heading, headingPropertyId, true, false, width); } public Builder<E> addColumn(final String heading, final String headingPropertyId) { return addColumn(heading, headingPropertyId, true, false); } public <T extends Object> Builder<E> addColumn(final String heading, final SingularAttribute<E, T> headingPropertyId) { return addColumn(heading, headingPropertyId, true, false); } public <T extends Object> Builder<E> addColumn(String heading, Path pathToHeadingPropertyId) { return addColumn(heading, pathToHeadingPropertyId.getName()); } /* Add hidden column convenience methods */ public Builder<E> addHiddenColumn(final String heading, final String headingPropertyId, final int width) { return addColumn(heading, headingPropertyId, false, false, width); } public <T extends Object> Builder<E> addHiddenColumn(final String heading, final SingularAttribute<E, T> headingPropertyId, final int width) { return addColumn(heading, headingPropertyId, false, false, width); } public Builder<E> addHiddenColumn(final String heading, final String headingPropertyId) { return addColumn(heading, headingPropertyId, false, false); } public <T extends Object> Builder<E> addHiddenColumn(final String heading, final SingularAttribute<E, T> headingPropertyId) { return addColumn(heading, headingPropertyId, false, false); } /* Add generated column convenience methods */ public Builder<E> addGeneratedColumn(final String heading, final String headingPropertyId, final PropertyValueGenerator<?> columnGenerator, final int width) { return addGeneratedColumn(heading, headingPropertyId, columnGenerator, true, false, width); } public <T extends Object> Builder<E> addGeneratedColumn(final String heading, final SingularAttribute<E, T> headingPropertyId, final PropertyValueGenerator<?> columnGenerator, final int width) { return addGeneratedColumn(heading, headingPropertyId, columnGenerator, true, false, width); } public Builder<E> addGeneratedColumn(final String heading, final String headingPropertyId, final PropertyValueGenerator<?> columnGenerator) { return addGeneratedColumn(heading, headingPropertyId, columnGenerator, true, false); } public <T extends Object> Builder<E> addGeneratedColumn(final String heading, final SingularAttribute<E, T> headingPropertyId, final PropertyValueGenerator<?> columnGenerator) { return addGeneratedColumn(heading, headingPropertyId, columnGenerator, true, false); } public Builder<E> addGeneratedColumn(final String heading, final PropertyValueGenerator<?> columnGenerator, final int width) { return addGeneratedColumn(heading, heading + "-generated", columnGenerator, width); } public Builder<E> addGeneratedColumn(final String heading, final PropertyValueGenerator<?> columnGenerator) { return addGeneratedColumn(heading, heading + "-generated", columnGenerator); } /* Add hidden generated column convenience methods */ public Builder<E> addHiddenGeneratedColumn(final String heading, final String headingPropertyId, final PropertyValueGenerator<?> columnGenerator, final int width) { return addGeneratedColumn(heading, headingPropertyId, columnGenerator, false, false, width); } public <T extends Object> Builder<E> addHiddenGeneratedColumn(final String heading, final SingularAttribute<E, T> headingPropertyId, final PropertyValueGenerator<?> columnGenerator, final int width) { return addGeneratedColumn(heading, headingPropertyId, columnGenerator, false, false, width); } public Builder<E> addHiddenGeneratedColumn(final String heading, final String headingPropertyId, final PropertyValueGenerator<?> columnGenerator) { return addGeneratedColumn(heading, headingPropertyId, columnGenerator, false, false); } public <T extends Object> Builder<E> addHiddenGeneratedColumn(final String heading, final SingularAttribute<E, T> headingPropertyId, final PropertyValueGenerator<?> columnGenerator) { return addGeneratedColumn(heading, headingPropertyId, columnGenerator, false, false); } public Builder<E> addHiddenGeneratedColumn(final String heading, final PropertyValueGenerator<?> columnGenerator, final int width) { return addGeneratedColumn(heading, columnGenerator, false, false, width); } public Builder<E> addHiddenGeneratedColumn(final String heading, final PropertyValueGenerator<?> columnGenerator) { return addGeneratedColumn(heading, columnGenerator, false, false); } } public List<GridHeadingToPropertyId> getColumns() { return cols; } public void applyToGrid(Grid grid) { final StackTraceElement[] trace = new Exception().getStackTrace(); for (StackTraceElement call : trace) { if (!call.getClassName().contains("au.com.vaadinutils")) { if (call.getClassName().contains(".")) { applyToGrid(grid, call.getClassName().substring(call.getClassName().lastIndexOf("."))); } else { applyToGrid(grid, call.getClassName()); } return; } } throw new RuntimeException("Unable to determine calling class name, " + " use applyToTable(Table table, String uniqueTableId) " + " instead of applyToTable(Table table)"); } /** * * @param grid * @param uniqueId * - an id for this layout/grid combination, it is used to * identify stored column widths in a key value map */ public void applyToGrid(final Grid grid, final String uniqueId) { this.grid = grid; this.uniqueId = uniqueId; try { // Changing the bound container at this point can cause issues // elsewhere, so we avoid it if possible Indexed gridContainer = grid.getContainerDataSource(); for (GridHeadingToPropertyId column : getColumns()) { if (column.isGenerated()) { gridContainer = wrapGridContainer(grid); break; } } final List<String> colsToShow = new LinkedList<>(); for (GridHeadingToPropertyId column : getColumns()) { final String propertyId = column.getPropertyId(); if (column.isGenerated()) { final PropertyValueGenerator<?> columnGenerator = column.getColumnGenerator(); ((GeneratedPropertyContainer) gridContainer).addGeneratedProperty(propertyId, columnGenerator); final Column gridColumn = grid.getColumn(propertyId); if (columnGenerator.getType() == String.class && gridColumn.getRenderer() instanceof TextRenderer) { gridColumn.setRenderer(new HtmlRenderer(), null); } else if (columnGenerator.getType() == Component.class) { gridColumn.setRenderer(new ComponentRenderer()); } } else { Preconditions.checkArgument( grid.getContainerDataSource().getContainerPropertyIds().contains(propertyId), propertyId + " is not a valid property id, valid property ids are " + grid.getContainerDataSource().getContainerPropertyIds().toString()); } colsToShow.add(propertyId); final Column gridColumn = grid.getColumn(propertyId); if (column.getRenderer() != null) { gridColumn.setRenderer(column.getRenderer()); } if (column.getConverter() != null) { gridColumn.setConverter(column.getConverter()); } gridColumn.setHeaderCaption(column.getHeader()); if (column.getWidth() != null) { gridColumn.setWidth(column.getWidth()); } else { gridColumn.setExpandRatio(1); gridColumn.setMinimumWidth(1); } if (column.isLocked()) { gridColumn.setHidable(false); } else { gridColumn.setHidable(true); if (!column.isVisibleByDefault()) { gridColumn.setHidden(true); } } } grid.setColumns(colsToShow.toArray()); if (eraseSavedConfig) { eraseSavedConfig(uniqueId); } if (!deferLoadSettings) { configureSaveColumnWidths(grid, uniqueId); configureSaveColumnOrder(grid, uniqueId); configureSaveColumnVisible(grid, uniqueId); } } catch (Exception e) { logger.error(e, e); } } void eraseSavedConfig(final String uniqueTableId) { UserSettingsStorageFactory.getUserSettingsStorage().erase(uniqueTableId); } private GeneratedPropertyContainer wrapGridContainer(final Grid grid) { Indexed gridContainer = grid.getContainerDataSource(); if (!(gridContainer instanceof GeneratedPropertyContainer)) { final GeneratedPropertyContainer gpc = new GeneratedPropertyContainer(gridContainer); grid.setContainerDataSource(gpc); gridContainer = gpc; } return (GeneratedPropertyContainer) gridContainer; } public void setDeferLoadSettings(final boolean deferLoadSettings) { this.deferLoadSettings = deferLoadSettings; } public void applySettingsToColumns() { Preconditions.checkState(grid != null, "You must call applytoGrid first"); configureSaveColumnWidths(grid, uniqueId); configureSaveColumnOrder(grid, uniqueId); configureSaveColumnVisible(grid, uniqueId); } private void configureSaveColumnWidths(final Grid grid, final String uniqueId) { final String keyStub = uniqueId + "-width"; for (GridHeadingToPropertyId column : getColumns()) { final String columnId = column.getPropertyId(); final String setting = keyStub + "-" + columnId; final String columnWidth = UserSettingsStorageFactory.getUserSettingsStorage().get(setting); if (columnWidth != null && columnWidth.length() > 0) { try { final Double width = Double.parseDouble(columnWidth); if (width > 0) { grid.getColumn(columnId).setWidth(Double.parseDouble(columnWidth)); } } catch (NumberFormatException e) { logger.error("Invalid width setting for " + setting); } } } grid.addColumnResizeListener(new ColumnResizeListener() { private static final long serialVersionUID = 4034036880290943146L; @Override public void columnResize(ColumnResizeEvent event) { final String propertyId = (String) event.getColumn().getPropertyId(); final double width = event.getColumn().getWidth(); UserSettingsStorageFactory.getUserSettingsStorage().store(keyStub + "-" + propertyId, "" + width); } }); if (dynamicColumnWidth) { configureDynamicColumnWidth(); } } private void configureDynamicColumnWidth() { final AtomicBoolean resizing = new AtomicBoolean(false); final AtomicInteger gridWidth = new AtomicInteger(); final SizeReporter sizeReporter = new SizeReporter(grid); sizeReporter.addResizeListener(new ComponentResizeListener() { private static final long serialVersionUID = 1L; @Override public void sizeChanged(ComponentResizeEvent event) { final int newGridWidth = event.getWidth(); gridWidth.set(newGridWidth); if (newGridWidth > 1) { final List<Column> gridColumns = grid.getColumns(); double columnsTotalWidth = 0; for (Column column : gridColumns) { columnsTotalWidth += column.getWidth(); } final double widthDiscrepancy = gridWidth.get() - columnsTotalWidth; if (widthDiscrepancy != 0) { final double perColumnChange = widthDiscrepancy / gridColumns.size(); for (Column column : gridColumns) { resizing.set(true); column.setWidth(column.getWidth() + perColumnChange); resizing.set(false); } } } } }); grid.addColumnResizeListener(new ColumnResizeListener() { private static final long serialVersionUID = 1L; @Override public void columnResize(ColumnResizeEvent event) { if (gridWidth.get() > 0 && !resizing.get()) { resizing.set(true); final List<Column> gridColumns = grid.getColumns(); final int totalColumns = gridColumns.size(); final Column resizedColumn = event.getColumn(); final double resizedColumnWidth = resizedColumn.getWidth(); final int resizedColumnIndex = gridColumns.indexOf(resizedColumn); // availableWidth = grid width - width of column being // resized - widths of columns to the left double availableWidth = gridWidth.get() - resizedColumnWidth; for (int i = 0; i < resizedColumnIndex; i++) { availableWidth -= gridColumns.get(i).getWidth(); } // columnsToResize = total columns - column being resized - // number of columns to the right final int columnsToResize = totalColumns - resizedColumnIndex - 1; final double perColumnWidth = availableWidth / columnsToResize; for (int i = (resizedColumnIndex + 1); i < totalColumns; i++) { gridColumns.get(i).setWidth(perColumnWidth); } resizing.set(false); } } }); } private void configureSaveColumnOrder(final Grid grid, final String uniqueId) { final String keyStub = uniqueId + "-order"; final List<Column> availableColumns = grid.getColumns(); final String columns = UserSettingsStorageFactory.getUserSettingsStorage().get(keyStub); if (availableColumns.size() > 0 && columns != null && !columns.isEmpty()) { final Object[] parsedColumns = columns.split(", ?"); if (parsedColumns.length > 0) { grid.setColumns(calculateColumnOrder(availableColumns, parsedColumns)); } } grid.addColumnReorderListener(new ColumnReorderListener() { private static final long serialVersionUID = -2810298692555333890L; @Override public void columnReorder(ColumnReorderEvent event) { final List<Column> columns = ((Grid) event.getSource()).getColumns(); if (columns.size() > 0) { String parsedColumns = ""; for (Column column : columns) { parsedColumns += column.getPropertyId() + ", "; } parsedColumns = parsedColumns.substring(0, parsedColumns.length() - 2); UserSettingsStorageFactory.getUserSettingsStorage().store(keyStub, "" + parsedColumns); } } }); } /** * If a column order has already been saved for a user, but the columns for * a grid have been modified, then we need to remove any columns that no * longer exist and add any new columns to the list of visible columns. * * @param availableColumns * the columns that are available in the table * @param parsedColumns * the column order that has been restored from preferences * @return the calculated order of columns with old removed and new added */ private Object[] calculateColumnOrder(final List<Column> availableColumns, final Object[] parsedColumns) { final List<Object> availableList = new ArrayList<>(availableColumns.size()); for (Column column : availableColumns) { availableList.add(column.getPropertyId()); } final List<Object> parsedList = new ArrayList<>(Arrays.asList(parsedColumns)); // Remove old columns parsedList.retainAll(availableList); // Add new columns in the same index position as they were added to the // table in final List<Object> newList = new ArrayList<>(availableList); newList.removeAll(parsedList); for (Object column : newList) { parsedList.add(availableList.indexOf(column), column); } return parsedList.toArray(); } private void configureSaveColumnVisible(final Grid grid, final String uniqueId) { final String keyStub = uniqueId + "-visible"; for (GridHeadingToPropertyId id : getColumns()) { final String setVisible = UserSettingsStorageFactory.getUserSettingsStorage() .get(keyStub + "-" + id.getPropertyId()); if (setVisible != null && !setVisible.isEmpty()) { grid.getColumn(id.getPropertyId()).setHidden(!Boolean.parseBoolean(setVisible)); } } grid.addColumnVisibilityChangeListener(new ColumnVisibilityChangeListener() { private static final long serialVersionUID = -9082974567948595049L; @Override public void columnVisibilityChanged(ColumnVisibilityChangeEvent event) { final Column column = event.getColumn(); final boolean isVisible = !column.isHidden(); UserSettingsStorageFactory.getUserSettingsStorage().store(keyStub + "-" + column.getPropertyId(), "" + isVisible); } }); } @Override public String toString() { return Arrays.toString(cols.toArray()); } }