From 1c1506ef0447b1d979a6adb4d812ae9858f00b67 Mon Sep 17 00:00:00 2001 From: John Ahlroos Date: Tue, 29 Oct 2013 13:44:49 +0200 Subject: [PATCH] Base grid component and column API (#12829, #12830) Change-Id: I6c4eae8a4369e9452dd56e764633cecfe9bf553a --- .../src/com/vaadin/client/ui/grid/Grid.java | 456 ++++++++++++++++++ .../com/vaadin/client/ui/grid/GridColumn.java | 34 ++ .../vaadin/client/ui/grid/GridConnector.java | 189 ++++++++ .../com/vaadin/ui/components/grid/Grid.java | 260 ++++++++++ .../vaadin/ui/components/grid/GridColumn.java | 186 +++++++ .../server/component/grid/GridColumns.java | 207 ++++++++ .../shared/ui/grid/GridColumnState.java | 61 +++ .../com/vaadin/shared/ui/grid/GridState.java | 52 ++ .../components/grid/GridBasicFeatures.java | 144 ++++++ 9 files changed, 1589 insertions(+) create mode 100644 client/src/com/vaadin/client/ui/grid/Grid.java create mode 100644 client/src/com/vaadin/client/ui/grid/GridColumn.java create mode 100644 client/src/com/vaadin/client/ui/grid/GridConnector.java create mode 100644 server/src/com/vaadin/ui/components/grid/Grid.java create mode 100644 server/src/com/vaadin/ui/components/grid/GridColumn.java create mode 100644 server/tests/src/com/vaadin/tests/server/component/grid/GridColumns.java create mode 100644 shared/src/com/vaadin/shared/ui/grid/GridColumnState.java create mode 100644 shared/src/com/vaadin/shared/ui/grid/GridState.java create mode 100644 uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java diff --git a/client/src/com/vaadin/client/ui/grid/Grid.java b/client/src/com/vaadin/client/ui/grid/Grid.java new file mode 100644 index 0000000000..8921aa3008 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/Grid.java @@ -0,0 +1,456 @@ +/* + * Copyright 2000-2013 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.client.ui.grid; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import com.google.gwt.core.shared.GWT; +import com.google.gwt.user.client.ui.Composite; +import com.vaadin.shared.util.SharedUtil; + +/** + * A data grid view that supports columns and lazy loading of data rows from a + * data source. + * + *

Columns

+ *

+ * The {@link GridColumn} class defines the renderer used to render a cell in + * the grid. Implement {@link GridColumn#getValue(Object)} to retrieve the cell + * value from the row object and return the cell renderer to render that cell. + *

+ *

+ * {@link GridColumn}s contain other properties like the width of the column and + * the visiblity of the column. If you want to change a column's properties + * after it has been added to the grid you can get a column object for a + * specific column index using {@link Grid#getColumn(int)}. + *

+ *

+ * + * TODO Explain about headers/footers once the multiple header/footer api has + * been implemented + * + *

Data sources

+ *

+ * TODO Explain about what a data source is and how it should be implemented. + *

+ * + * @since 7.2 + * @author Vaadin Ltd + */ +public class Grid extends Composite { + + /** + * Escalator used internally by the grid to render the rows + */ + private Escalator escalator = GWT.create(Escalator.class); + + /** + * List of columns in the grid. Order defines the visible order. + */ + private final List> columns = new ArrayList>(); + + /** + * Base class for grid columns internally used by the Grid. You should use + * {@link GridColumn} when creating new columns. + * + * @param + * the row type + */ + public static abstract class AbstractGridColumn { + + /** + * Grid associated with the column + */ + private Grid grid; + + /** + * Text displayed in the column header + */ + private String header; + + /** + * Text displayed in the column footer + */ + private String footer; + + /** + * Is the column visible + */ + private boolean visible; + + /** + * Internally used by the grid to set itself + * + * @param grid + */ + private void setGrid(Grid grid) { + if (this.grid != null && grid != null) { + // Trying to replace grid + throw new IllegalStateException( + "Column already is attached to grid. Remove the column first from the grid and then add it."); + } + + this.grid = grid; + } + + /** + * Gets text in the header of the column. By default the header caption + * is empty. + * + * @return the text displayed in the column caption + */ + public String getHeaderCaption() { + return header; + } + + /** + * Sets the text in the header of the column. + * + * @param caption + * the text displayed in the column header + */ + public void setHeaderCaption(String caption) { + if (SharedUtil.equals(caption, this.header)) { + return; + } + + this.header = caption; + + if (grid != null) { + grid.refreshHeader(); + } + } + + /** + * Gets text in the footer of the column. By default the footer caption + * is empty. + * + * @return The text displayed in the footer of the column + */ + public String getFooterCaption() { + return footer; + } + + /** + * Sets text in the footer of the column. + * + * @param caption + * the text displayed in the footer of the column + */ + public void setFooterCaption(String caption) { + if (SharedUtil.equals(caption, this.footer)) { + return; + } + + this.footer = caption; + + if (grid != null) { + grid.refreshFooter(); + } + } + + /** + * Is the column visible. By default all columns are visible. + * + * @return true if the column is visible + */ + public boolean isVisible() { + return visible; + } + + /** + * Sets a column as visible in the grid. + * + * @param visible + * Set to true to show the column in the grid + */ + public void setVisible(boolean visible) { + if (this.visible == visible) { + return; + } + + // Remove column + if (grid != null) { + int index = findIndexOfColumn(); + ColumnConfiguration conf = grid.escalator + .getColumnConfiguration(); + + if (visible) { + conf.insertColumns(index, 1); + } else { + conf.removeColumns(index, 1); + } + + // TODO should update body as well + } + + this.visible = visible; + } + + /** + * Returns the text that should be displayed in the cell. + * + * @param row + * the row object that provides the cell content + * @return The cell content of the row + */ + public abstract String getValue(T row); + + /** + * Finds the index of this column instance + * + */ + private int findIndexOfColumn() { + return grid.columns.indexOf(this); + } + } + + /** + * Creates a new instance. + */ + public Grid() { + initWidget(escalator); + + escalator.getHeader().setEscalatorUpdater(createHeaderUpdater()); + escalator.getBody().setEscalatorUpdater(createBodyUpdater()); + escalator.getFooter().setEscalatorUpdater(createFooterUpdater()); + } + + /** + * Creates the header updater that updates the escalator header rows from + * the column and column group rows. + * + * @return the updater that updates the data in the escalator. + */ + private EscalatorUpdater createHeaderUpdater() { + return new EscalatorUpdater() { + + @Override + public void updateCells(Row row, List cellsToUpdate) { + if (isHeaderVisible()) { + for (Cell cell : cellsToUpdate) { + AbstractGridColumn column = columns.get(cell + .getColumn()); + cell.getElement().setInnerText( + column.getHeaderCaption()); + } + } + } + }; + } + + // TODO Should be implemented bu the data sources + private EscalatorUpdater createBodyUpdater() { + return new EscalatorUpdater() { + + @Override + public void updateCells(Row row, List cellsToUpdate) { + for (Cell cell : cellsToUpdate) { + cell.getElement().setInnerHTML("-"); + } + } + }; + } + + /** + * Creates the footer updater that updates the escalator footer rows from + * the column and column group rows. + * + * @return the updater that updates the data in the escalator. + */ + private EscalatorUpdater createFooterUpdater() { + return new EscalatorUpdater() { + + @Override + public void updateCells(Row row, List cellsToUpdate) { + if (isFooterVisible()) { + for (Cell cell : cellsToUpdate) { + AbstractGridColumn column = columns.get(cell + .getColumn()); + cell.getElement().setInnerText( + column.getFooterCaption()); + } + } + } + }; + } + + /** + * Refreshes all header rows. + */ + private void refreshHeader() { + RowContainer header = escalator.getHeader(); + if (isHeaderVisible() && header.getRowCount() > 0) { + header.refreshRows(0, header.getRowCount()); + } + } + + /** + * Refreshes all footer rows. + */ + private void refreshFooter() { + RowContainer footer = escalator.getFooter(); + if (isFooterVisible() && footer.getRowCount() > 0) { + footer.refreshRows(0, footer.getRowCount()); + } + } + + /** + * Adds a column as the last column in the grid. + * + * @param column + * the column to add + */ + public void addColumn(GridColumn column) { + ColumnConfiguration conf = escalator.getColumnConfiguration(); + addColumn(column, conf.getColumnCount()); + } + + /** + * Inserts a column into a specific position in the grid. + * + * @param index + * the index where the column should be inserted into + * @param column + * the column to add + */ + public void addColumn(GridColumn column, int index) { + + // Register this grid instance with the column + ((AbstractGridColumn) column).setGrid(this); + + columns.add(index, column); + + ColumnConfiguration conf = escalator.getColumnConfiguration(); + conf.insertColumns(index, 1); + } + + /** + * Removes a column from the grid. + * + * @param column + * the column to remove + */ + public void removeColumn(GridColumn column) { + + int columnIndex = columns.indexOf(column); + columns.remove(columnIndex); + + // de-register column with grid + ((AbstractGridColumn) column).setGrid(null); + + ColumnConfiguration conf = escalator.getColumnConfiguration(); + conf.removeColumns(columnIndex, 1); + } + + /** + * Returns the amount of columns in the grid. + * + * @return The number of columns in the grid + */ + public int getColumnCount() { + return columns.size(); + } + + /** + * Returns a list of columns in the grid. + * + * @return A unmodifiable list of the columns in the grid + */ + public List> getColumns() { + return Collections.unmodifiableList(new ArrayList>( + columns)); + } + + /** + * Returns a column by its index in the grid. + * + * @param index + * the index of the column + * @return The column in the given index + * @throws IllegalArgumentException + * if the column index does not exist in the grid + */ + public GridColumn getColumn(int index) throws IllegalArgumentException { + try { + return columns.get(index); + } catch (ArrayIndexOutOfBoundsException aioobe) { + throw new IllegalStateException("Column not found.", aioobe); + } + } + + /** + * Sets the header row visible. + * + * @param visible + * true if header rows should be visible + */ + public void setHeaderVisible(boolean visible) { + if (visible == isHeaderVisible()) { + return; + } + + RowContainer header = escalator.getHeader(); + + // TODO Should support multiple headers + if (visible) { + header.insertRows(0, 1); + } else { + header.removeRows(0, 1); + } + } + + /** + * Are the header row(s) visible? + * + * @return true if the header is visible + */ + public boolean isHeaderVisible() { + return escalator.getHeader().getRowCount() > 0; + } + + /** + * Sets the footer row(s) visible. + * + * @param visible + * true if header rows should be visible + */ + public void setFooterVisible(boolean visible) { + if (visible == isFooterVisible()) { + return; + } + + RowContainer footer = escalator.getFooter(); + + // TODO Should support multiple footers + if (visible) { + footer.insertRows(0, 1); + } else { + footer.removeRows(0, 1); + } + } + + /** + * Are the footer row(s) visible? + * + * @return true if the footer is visible + */ + public boolean isFooterVisible() { + return escalator.getFooter().getRowCount() > 0; + } +} diff --git a/client/src/com/vaadin/client/ui/grid/GridColumn.java b/client/src/com/vaadin/client/ui/grid/GridColumn.java new file mode 100644 index 0000000000..992bfae014 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/GridColumn.java @@ -0,0 +1,34 @@ +/* + * Copyright 2000-2013 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.client.ui.grid; + +/** + * Represents a column in the {@link Grid}. + * + * @param + * The row type + * + * @since 7.2 + * @author Vaadin Ltd + */ +public abstract class GridColumn extends Grid.AbstractGridColumn { + + /* + * This class is a convenience class so you do not have to reference + * Grid.AbstractGridColumn in your production code. The real implementation + * should be in the abstract class. + */ +} diff --git a/client/src/com/vaadin/client/ui/grid/GridConnector.java b/client/src/com/vaadin/client/ui/grid/GridConnector.java new file mode 100644 index 0000000000..e35e3f1f78 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/GridConnector.java @@ -0,0 +1,189 @@ +/* + * Copyright 2000-2013 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.client.ui.grid; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import com.vaadin.client.communication.StateChangeEvent; +import com.vaadin.client.ui.AbstractComponentConnector; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.grid.GridColumnState; +import com.vaadin.shared.ui.grid.GridState; + +/** + * Connects the client side {@link Grid} widget with the server side + * {@link com.vaadin.ui.components.grid.Grid} component. + * + * @since 7.2 + * @author Vaadin Ltd + */ +@Connect(com.vaadin.ui.components.grid.Grid.class) +public class GridConnector extends AbstractComponentConnector { + + private class CustomGridColumn extends GridColumn { + + @Override + public String getValue(String[] obj) { + // FIXME Should return something from the data source. + return null; + } + } + + // Maps a generated column id -> A grid column instance + private Map columnIdToColumn = new HashMap(); + + @Override + protected void init() { + + // FIXME Escalator bug requires to do this when running compiled. Not + // required when in devmode. Most likely Escalator.setWidth() is called + // before attach and measuring from DOM does not work then. + getWidget().setWidth(getState().width); + getWidget().setHeight(getState().height); + + } + + @Override + protected Grid createWidget() { + // FIXME Shouldn't be needed after #12873 has been fixed. + return new Grid(); + } + + @Override + public Grid getWidget() { + return (Grid) super.getWidget(); + } + + @Override + public GridState getState() { + return (GridState) super.getState(); + } + + @Override + public void onStateChanged(StateChangeEvent stateChangeEvent) { + super.onStateChanged(stateChangeEvent); + + // Header + if (stateChangeEvent.hasPropertyChanged("headerVisible")) { + getWidget().setHeaderVisible(getState().headerVisible); + } + + // Footer + if (stateChangeEvent.hasPropertyChanged("footerVisible")) { + getWidget().setFooterVisible(getState().footerVisible); + } + + // Column updates + if (stateChangeEvent.hasPropertyChanged("columns")) { + + int totalColumns = getState().columns.size(); + int currentColumns = getWidget().getColumnCount(); + + // Remove old columns + purgeRemovedColumns(); + + // Add new columns + for (int columnIndex = currentColumns; columnIndex < totalColumns; columnIndex++) { + addColumnFromStateChangeEvent(columnIndex, stateChangeEvent); + } + + // Update old columns + for (int columnIndex = 0; columnIndex < currentColumns; columnIndex++) { + // FIXME Currently updating all column header / footers when a + // change in made in one column. When the framework supports + // quering a specific item in a list then it should do so here. + updateColumnFromStateChangeEvent(columnIndex, stateChangeEvent); + } + } + } + + /** + * Updates a column from a state change event. + * + * @param columnIndex + * The index of the column to update + * @param stateChangeEvent + * The state change event that contains the changes for the + * column + */ + private void updateColumnFromStateChangeEvent(int columnIndex, + StateChangeEvent stateChangeEvent) { + GridColumn column = getWidget().getColumn(columnIndex); + GridColumnState columnState = getState().columns.get(columnIndex); + updateColumnFromState(column, columnState); + } + + /** + * Adds a new column to the grid widget from a state change event + * + * @param columnIndex + * The index of the column, according to how it + * @param stateChangeEvent + */ + private void addColumnFromStateChangeEvent(int columnIndex, + StateChangeEvent stateChangeEvent) { + GridColumnState state = getState().columns.get(columnIndex); + CustomGridColumn column = new CustomGridColumn(); + updateColumnFromState(column, state); + columnIdToColumn.put(state.id, column); + getWidget().addColumn(column, columnIndex); + } + + /** + * Updates fields in column from a {@link GridColumnState} DTO + * + * @param column + * The column to update + * @param state + * The state to update from + */ + private void updateColumnFromState(GridColumn column, + GridColumnState state) { + column.setHeaderCaption(state.header); + column.setFooterCaption(state.footer); + column.setVisible(state.visible); + } + + /** + * Removes any orphan columns that has been removed from the state from the + * grid + */ + private void purgeRemovedColumns() { + + // Get columns still registered in the state + Set columnsInState = new HashSet(); + for (GridColumnState columnState : getState().columns) { + columnsInState.add(columnState.id); + } + + // Remove column no longer in state + Iterator columnIdIterator = columnIdToColumn.keySet() + .iterator(); + while (columnIdIterator.hasNext()) { + String id = columnIdIterator.next(); + if (!columnsInState.contains(id)) { + CustomGridColumn column = columnIdToColumn.get(id); + columnIdIterator.remove(); + getWidget().removeColumn(column); + } + } + } +} diff --git a/server/src/com/vaadin/ui/components/grid/Grid.java b/server/src/com/vaadin/ui/components/grid/Grid.java new file mode 100644 index 0000000000..25ac796d47 --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/Grid.java @@ -0,0 +1,260 @@ +/* + * Copyright 2000-2013 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.components.grid; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import com.vaadin.data.Container; +import com.vaadin.data.Container.PropertySetChangeEvent; +import com.vaadin.data.Container.PropertySetChangeListener; +import com.vaadin.data.Container.PropertySetChangeNotifier; +import com.vaadin.server.KeyMapper; +import com.vaadin.shared.ui.grid.GridColumnState; +import com.vaadin.shared.ui.grid.GridState; +import com.vaadin.ui.AbstractComponent; + +/** + * Data grid component + * + *

Lazy loading

TODO To be revised when the data data source + * implementation has been don. + * + *

Columns

The grid columns are based on the property ids of the + * underlying data source. Each property id represents one column in the grid. + * To retrive a column in the grid you can use {@link Grid#getColumn(Object)} + * with the property id of the column. A grid column contains properties like + * the width, the footer and header captions of the column. + * + *

Auxiliary headers and footers

TODO To be revised when column + * grouping is implemented. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class Grid extends AbstractComponent { + + private Container.Indexed datasource; + + /** + * Property id -> Column instance mapping + */ + private final Map columns = new HashMap(); + + /** + * Key generator for column server->client communication + */ + private final KeyMapper columnKeys = new KeyMapper(); + + /** + * Property listener for listening to changes in data source properties. + */ + private final PropertySetChangeListener propertyListener = new PropertySetChangeListener() { + + @Override + public void containerPropertySetChange(PropertySetChangeEvent event) { + Collection properties = new HashSet(event.getContainer() + .getContainerPropertyIds()); + + // Cleanup columns that are no longer in grid + List removedColumns = new LinkedList(); + for (Object columnId : columns.keySet()) { + if (!properties.contains(columnId)) { + removedColumns.add(columnId); + } + } + for (Object columnId : removedColumns) { + GridColumn column = columns.remove(columnId); + columnKeys.remove(columnId); + getState().columns.remove(column.getState()); + } + + // Add new columns + for (Object propertyId : properties) { + if (!columns.containsKey(propertyId)) { + appendColumn(propertyId); + } + } + } + }; + + /** + * Creates a new Grid using the given datasource. + * + * @param datasource + * the data source for the grid + */ + public Grid(Container.Indexed datasource) { + setContainerDatasource(datasource); + } + + /** + * Sets the grid data source. + * + * @param container + * The container data source. Cannot be null. + * @throws IllegalArgumentException + * if the data source is null + */ + public void setContainerDatasource(Container.Indexed container) { + if (container == null) { + throw new IllegalArgumentException( + "Cannot set the datasource to null"); + } + if (datasource == container) { + return; + } + + // Remove old listener + if (datasource instanceof PropertySetChangeNotifier) { + ((PropertySetChangeNotifier) datasource) + .removePropertySetChangeListener(propertyListener); + } + + datasource = container; + + // Listen to changes in properties and remove columns if needed + if (datasource instanceof PropertySetChangeNotifier) { + ((PropertySetChangeNotifier) datasource) + .addPropertySetChangeListener(propertyListener); + } + + getState().columns.clear(); + + // Add columns + for (Object propertyId : datasource.getContainerPropertyIds()) { + if (!columns.containsKey(propertyId)) { + GridColumn column = appendColumn(propertyId); + + // By default use property id as column caption + column.setHeaderCaption(String.valueOf(propertyId)); + + } + } + } + + /** + * Returns the grid data source. + * + * @return the container data source of the grid + */ + public Container.Indexed getContainerDatasource() { + return datasource; + } + + /** + * Returns a column based on the property id + * + * @param propertyId + * the property id of the column + * @return the column or null if not found + */ + public GridColumn getColumn(Object propertyId) { + return columns.get(propertyId); + } + + /** + * Sets the header rows visible. + * + * @param visible + * true if the header rows should be visible + */ + public void setHeaderVisible(boolean visible) { + getState().headerVisible = visible; + } + + /** + * Are the header rows visible? + * + * @return true if the header is visible + */ + public boolean isHeaderVisible() { + return getState(false).headerVisible; + } + + /** + * Sets the footer rows visible. + * + * @param visible + * true if the header rows should be visible + */ + public void setFooterVisible(boolean visible) { + getState().footerVisible = visible; + } + + /** + * Are the footer rows visible. + * + * @return true if the footer rows should be visible + */ + public boolean isFooterVisible() { + return getState(false).footerVisible; + } + + /** + * Used internally by the {@link Grid} to get a {@link GridColumn} by + * referencing its generated state id. Also used by {@link GridColumn} to + * verify if it has been detached from the {@link Grid} + * + * @param columnId + * The client id generated for the column when the column is + * added to the grid + * @return The column with the id or null if not found + */ + GridColumn getColumnByColumnId(String columnId) { + Object propertyId = columnKeys.get(columnId); + return getColumn(propertyId); + } + + @Override + protected GridState getState() { + return (GridState) super.getState(); + } + + @Override + protected GridState getState(boolean markAsDirty) { + return (GridState) super.getState(markAsDirty); + } + + /** + * Creates a new column based on a property id and appends it as the last + * column. + * + * @param datasourcePropertyId + * The property id of a property in the datasource + */ + protected GridColumn appendColumn(Object datasourcePropertyId) { + if (datasourcePropertyId == null) { + throw new IllegalArgumentException("Property id cannot be null"); + } + assert datasource.getContainerPropertyIds().contains( + datasourcePropertyId) : "Datasource should contain the property id"; + + GridColumnState columnState = new GridColumnState(); + columnState.id = columnKeys.key(datasourcePropertyId); + getState().columns.add(columnState); + + GridColumn column = new GridColumn(this, columnState); + columns.put(datasourcePropertyId, column); + + return column; + } +} diff --git a/server/src/com/vaadin/ui/components/grid/GridColumn.java b/server/src/com/vaadin/ui/components/grid/GridColumn.java new file mode 100644 index 0000000000..505919b3cf --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/GridColumn.java @@ -0,0 +1,186 @@ +/* + * Copyright 2000-2013 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.components.grid; + +import com.vaadin.shared.ui.grid.GridColumnState; + +/** + * A column in the grid. Can be obtained by calling + * {@link Grid#getColumn(Object propertyId)}. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class GridColumn { + + /** + * The shared state of the column + */ + private final GridColumnState state; + + /** + * The grid this column is associated with + */ + private final Grid grid; + + /** + * Internally used constructor. + * + * @param grid + * The grid this column belongs to. Should not be null. + * @param state + * the shared state of this column + */ + GridColumn(Grid grid, GridColumnState state) { + this.grid = grid; + this.state = state; + } + + /** + * Returns the serializable state of this column that is sent to the client + * side connector. + * + * @return the internal state of the column + */ + GridColumnState getState() { + return state; + } + + /** + * Returns the caption of the header. By default the header caption is the + * property id of the column. + * + * @return the text in the header + * + * @throws IllegalStateException + * if the column no longer is attached to the grid + */ + public String getHeaderCaption() throws IllegalStateException { + checkColumnIsAttached(); + return state.header; + } + + /** + * Sets the caption of the header. + * + * @param caption + * the text to show in the caption + * + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + public void setHeaderCaption(String caption) throws IllegalStateException { + checkColumnIsAttached(); + state.header = caption; + grid.markAsDirty(); + } + + /** + * Returns the caption of the footer. By default the captions are + * null. + * + * @return the text in the footer + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + public String getFooterCaption() throws IllegalStateException { + checkColumnIsAttached(); + return state.footer; + } + + /** + * Sets the caption of the footer. + * + * @param caption + * the text to show in the caption + * + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + public void setFooterCaption(String caption) throws IllegalStateException { + checkColumnIsAttached(); + state.footer = caption; + grid.markAsDirty(); + } + + /** + * 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 int getWidth() throws IllegalStateException { + checkColumnIsAttached(); + return state.width; + } + + /** + * Sets the width (in pixels). + * + * FIXME Currently not implemented. + * + * @param pixelWidth + * the new pixel width of the column + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + public void setWidth(int pixelWidth) throws IllegalStateException { + checkColumnIsAttached(); + state.width = pixelWidth; + grid.markAsDirty(); + } + + /** + * Is this column visible in the grid. By default all columns are visible. + * + * @return true if the column is visible + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + public boolean isVisible() throws IllegalStateException { + checkColumnIsAttached(); + return state.visible; + } + + /** + * Set the visibility of this column + * + * @param visible + * is the column visible + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + public void setVisible(boolean visible) throws IllegalStateException { + checkColumnIsAttached(); + state.visible = visible; + grid.markAsDirty(); + } + + /** + * Checks if column is attached and throws an {@link IllegalStateException} + * if it is not + * + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + protected void checkColumnIsAttached() throws IllegalStateException { + if (grid.getColumnByColumnId(state.id) == null) { + throw new IllegalStateException("Column no longer exists."); + } + } +} diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/GridColumns.java b/server/tests/src/com/vaadin/tests/server/component/grid/GridColumns.java new file mode 100644 index 0000000000..5989d537b4 --- /dev/null +++ b/server/tests/src/com/vaadin/tests/server/component/grid/GridColumns.java @@ -0,0 +1,207 @@ +/* + * Copyright 2000-2013 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.tests.server.component.grid; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.server.KeyMapper; +import com.vaadin.shared.ui.grid.GridColumnState; +import com.vaadin.shared.ui.grid.GridState; +import com.vaadin.ui.components.grid.Grid; +import com.vaadin.ui.components.grid.GridColumn; + +public class GridColumns { + + private Grid grid; + + private GridState state; + + private Method getStateMethod; + + private Field columnIdGeneratorField; + + private KeyMapper columnIdMapper; + + @Before + public void setup() throws Exception { + IndexedContainer ds = new IndexedContainer(); + for (int c = 0; c < 10; c++) { + ds.addContainerProperty("column" + c, String.class, ""); + } + grid = new Grid(ds); + + getStateMethod = Grid.class.getDeclaredMethod("getState"); + getStateMethod.setAccessible(true); + + state = (GridState) getStateMethod.invoke(grid); + + columnIdGeneratorField = Grid.class.getDeclaredField("columnKeys"); + columnIdGeneratorField.setAccessible(true); + + columnIdMapper = (KeyMapper) columnIdGeneratorField.get(grid); + } + + @Test + public void testColumnGeneration() throws Exception { + + for (Object propertyId : grid.getContainerDatasource() + .getContainerPropertyIds()) { + + // All property ids should get a column + GridColumn column = grid.getColumn(propertyId); + assertNotNull(column); + + // Property id should be the column header by default + assertEquals(propertyId.toString(), column.getHeaderCaption()); + } + } + + @Test + public void testModifyingColumnProperties() throws Exception { + + // Modify first column + GridColumn column = grid.getColumn("column1"); + assertNotNull(column); + + column.setFooterCaption("CustomFooter"); + assertEquals("CustomFooter", column.getFooterCaption()); + assertEquals(column.getFooterCaption(), + getColumnState("column1").footer); + + column.setHeaderCaption("CustomHeader"); + assertEquals("CustomHeader", column.getHeaderCaption()); + assertEquals(column.getHeaderCaption(), + getColumnState("column1").header); + + column.setVisible(false); + assertFalse(column.isVisible()); + assertFalse(getColumnState("column1").visible); + + column.setVisible(true); + assertTrue(column.isVisible()); + assertTrue(getColumnState("column1").visible); + + column.setWidth(100); + assertEquals(100, column.getWidth()); + assertEquals(column.getWidth(), getColumnState("column1").width); + + column.setWidth(-1); + assertEquals(-1, column.getWidth()); + assertEquals(-1, getColumnState("column1").width); + } + + @Test + public void testRemovingColumn() throws Exception { + + GridColumn column = grid.getColumn("column1"); + assertNotNull(column); + + // Remove column + grid.getContainerDatasource().removeContainerProperty("column1"); + + try { + column.setHeaderCaption("asd"); + fail("Succeeded in modifying a detached column"); + } catch (IllegalStateException ise) { + // Detached state should throw exception + } + + try { + column.setFooterCaption("asd"); + fail("Succeeded in modifying a detached column"); + } catch (IllegalStateException ise) { + // Detached state should throw exception + } + + try { + column.setVisible(false); + fail("Succeeded in modifying a detached column"); + } catch (IllegalStateException ise) { + // Detached state should throw exception + } + + try { + column.setWidth(123); + fail("Succeeded in modifying a detached column"); + } catch (IllegalStateException ise) { + // Detached state should throw exception + } + + assertNull(grid.getColumn("column1")); + assertNull(getColumnState("column1")); + } + + @Test + public void testAddingColumn() { + grid.getContainerDatasource().addContainerProperty("columnX", + String.class, ""); + GridColumn column = grid.getColumn("columnX"); + assertNotNull(column); + } + + @Test + public void testHeaderVisiblility() { + + assertTrue(grid.isHeaderVisible()); + assertTrue(state.headerVisible); + + grid.setHeaderVisible(false); + assertFalse(grid.isHeaderVisible()); + assertFalse(state.headerVisible); + + grid.setHeaderVisible(true); + assertTrue(grid.isHeaderVisible()); + assertTrue(state.headerVisible); + } + + @Test + public void testFooterVisibility() { + + assertTrue(grid.isFooterVisible()); + assertTrue(state.footerVisible); + + grid.setFooterVisible(false); + assertFalse(grid.isFooterVisible()); + assertFalse(state.footerVisible); + + grid.setFooterVisible(true); + assertTrue(grid.isFooterVisible()); + assertTrue(state.footerVisible); + } + + private GridColumnState getColumnState(Object propertyId) { + String columnId = columnIdMapper.key(propertyId); + for (GridColumnState columnState : state.columns) { + if (columnState.id.equals(columnId)) { + return columnState; + } + } + return null; + } + +} diff --git a/shared/src/com/vaadin/shared/ui/grid/GridColumnState.java b/shared/src/com/vaadin/shared/ui/grid/GridColumnState.java new file mode 100644 index 0000000000..391eb2a65c --- /dev/null +++ b/shared/src/com/vaadin/shared/ui/grid/GridColumnState.java @@ -0,0 +1,61 @@ +/* + * Copyright 2000-2013 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.shared.ui.grid; + +import java.io.Serializable; + +/** + * Column state DTO for transferring column properties from the server to the + * client + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class GridColumnState implements Serializable { + + /** + * Id used by grid connector to map server side column with client side + * column + */ + public String id; + + /** + * Header caption for the column + * + * FIXME Only single header currently supported. Should support many + * headers. + */ + public String header; + + /** + * Footer caption for the column + * + * FIXME Only single footer currently supported. Should support many + * footers. + */ + public String footer; + + /** + * Has the column been hidden. By default the column is visible. + */ + public boolean visible = true; + + /** + * Column width in pixels. Default column width is 100px. + */ + public int width = 100; + +} diff --git a/shared/src/com/vaadin/shared/ui/grid/GridState.java b/shared/src/com/vaadin/shared/ui/grid/GridState.java new file mode 100644 index 0000000000..e1e0fff354 --- /dev/null +++ b/shared/src/com/vaadin/shared/ui/grid/GridState.java @@ -0,0 +1,52 @@ +/* + * Copyright 2000-2013 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.shared.ui.grid; + +import java.util.ArrayList; +import java.util.List; + +import com.vaadin.shared.AbstractComponentState; + +/** + * The shared state for the {@link com.vaadin.ui.components.grid.Grid} component + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class GridState extends AbstractComponentState { + { + // FIXME Grid currently does not support undefined size + width = "400px"; + height = "400px"; + } + + /** + * Columns in grid. Column order implicitly deferred from list order. + */ + public List columns = new ArrayList(); + + /** + * Are the header row(s) visible. By default they are visible. + */ + public boolean headerVisible = true; + + /** + * Are the footer row(s) visible. By default they are visible. + */ + public boolean footerVisible = true; + +} diff --git a/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java b/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java new file mode 100644 index 0000000000..5b3d742f19 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java @@ -0,0 +1,144 @@ +/* + * Copyright 2000-2013 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.tests.components.grid; + +import java.util.ArrayList; + +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.tests.components.AbstractComponentTest; +import com.vaadin.ui.components.grid.Grid; +import com.vaadin.ui.components.grid.GridColumn; + +/** + * Tests the basic features like columns, footers and headers + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class GridBasicFeatures extends AbstractComponentTest { + + private final int COLUMNS = 10; + + @Override + protected Grid constructComponent() { + + // Build data source + IndexedContainer ds = new IndexedContainer(); + + for (int col = 0; col < COLUMNS; col++) { + ds.addContainerProperty("Column" + col, String.class, ""); + } + + Grid grid = new Grid(ds); + + // Headers and footers + for (int col = 0; col < COLUMNS; col++) { + GridColumn column = grid.getColumn("Column" + col); + column.setHeaderCaption("Column " + col); + column.setFooterCaption("Footer " + col); + } + + createColumnActions(); + + return grid; + } + + protected void createColumnActions() { + createCategory("Columns", null); + + for (int c = 0; c < COLUMNS; c++) { + createCategory("Column" + c, "Columns"); + + createBooleanAction("Visible", "Column" + c, true, + new Command() { + + @Override + public void execute(Grid grid, Boolean value, + Object columnIndex) { + Object propertyId = (new ArrayList(grid + .getContainerDatasource() + .getContainerPropertyIds()) + .get((Integer) columnIndex)); + GridColumn column = grid.getColumn(propertyId); + column.setVisible(!column.isVisible()); + } + }, c); + + createBooleanAction("Footer", "Column" + c, true, + new Command() { + + @Override + public void execute(Grid grid, Boolean value, + Object columnIndex) { + Object propertyId = (new ArrayList(grid + .getContainerDatasource() + .getContainerPropertyIds()) + .get((Integer) columnIndex)); + GridColumn column = grid.getColumn(propertyId); + String footer = column.getFooterCaption(); + if (footer == null) { + column.setFooterCaption("Footer " + columnIndex); + } else { + column.setFooterCaption(null); + } + } + }, c); + + createBooleanAction("Header", "Column" + c, true, + new Command() { + + @Override + public void execute(Grid grid, Boolean value, + Object columnIndex) { + Object propertyId = (new ArrayList(grid + .getContainerDatasource() + .getContainerPropertyIds()) + .get((Integer) columnIndex)); + GridColumn column = grid.getColumn(propertyId); + String header = column.getHeaderCaption(); + if (header == null) { + column.setHeaderCaption("Column " + columnIndex); + } else { + column.setHeaderCaption(null); + } + } + }, c); + + createClickAction("Remove", "Column" + c, + new Command() { + + @Override + public void execute(Grid grid, String value, Object data) { + grid.getContainerDatasource() + .removeContainerProperty("Column" + data); + } + }, null, c); + + } + + } + + @Override + protected Integer getTicketNumber() { + return 12829; + } + + @Override + protected Class getTestClass() { + return Grid.class; + } + +} -- 2.39.5