diff options
author | John Ahlroos <john@vaadin.com> | 2013-10-29 13:44:49 +0200 |
---|---|---|
committer | Vaadin Code Review <review@vaadin.com> | 2013-11-19 11:07:24 +0000 |
commit | 1c1506ef0447b1d979a6adb4d812ae9858f00b67 (patch) | |
tree | 801cd0118eb73c18527739fee512bac3339f2457 /server | |
parent | a752fcb10935c73f0a9d9d9c7948bfb29fcbb2e2 (diff) | |
download | vaadin-framework-1c1506ef0447b1d979a6adb4d812ae9858f00b67.tar.gz vaadin-framework-1c1506ef0447b1d979a6adb4d812ae9858f00b67.zip |
Base grid component and column API (#12829, #12830)
Change-Id: I6c4eae8a4369e9452dd56e764633cecfe9bf553a
Diffstat (limited to 'server')
3 files changed, 653 insertions, 0 deletions
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 + * + * <h3>Lazy loading</h3> TODO To be revised when the data data source + * implementation has been don. + * + * <h3>Columns</h3> 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. + * + * <h3>Auxiliary headers and footers</h3> 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<Object, GridColumn> columns = new HashMap<Object, GridColumn>(); + + /** + * Key generator for column server->client communication + */ + private final KeyMapper<Object> columnKeys = new KeyMapper<Object>(); + + /** + * Property listener for listening to changes in data source properties. + */ + private final PropertySetChangeListener propertyListener = new PropertySetChangeListener() { + + @Override + public void containerPropertySetChange(PropertySetChangeEvent event) { + Collection<?> properties = new HashSet<Object>(event.getContainer() + .getContainerPropertyIds()); + + // Cleanup columns that are no longer in grid + List<Object> removedColumns = new LinkedList<Object>(); + 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 <code>null</code> if not found + */ + public GridColumn getColumn(Object propertyId) { + return columns.get(propertyId); + } + + /** + * Sets the header rows visible. + * + * @param visible + * <code>true</code> if the header rows should be visible + */ + public void setHeaderVisible(boolean visible) { + getState().headerVisible = visible; + } + + /** + * Are the header rows visible? + * + * @return <code>true</code> if the header is visible + */ + public boolean isHeaderVisible() { + return getState(false).headerVisible; + } + + /** + * Sets the footer rows visible. + * + * @param visible + * <code>true</code> if the header rows should be visible + */ + public void setFooterVisible(boolean visible) { + getState().footerVisible = visible; + } + + /** + * Are the footer rows visible. + * + * @return <code>true</code> 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 <code>null</code> 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 + * <code>null</code>. + * + * @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 <code>true</code> 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<Object> 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<Object>) 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; + } + +} |