diff options
Diffstat (limited to 'server')
4 files changed, 725 insertions, 5 deletions
diff --git a/server/src/main/java/com/vaadin/ui/Grid.java b/server/src/main/java/com/vaadin/ui/Grid.java index 94f8c9aee7..3777decbe9 100644 --- a/server/src/main/java/com/vaadin/ui/Grid.java +++ b/server/src/main/java/com/vaadin/ui/Grid.java @@ -49,6 +49,8 @@ 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.HeightMode; +import com.vaadin.shared.ui.grid.SectionState; +import com.vaadin.ui.components.grid.Header; import com.vaadin.ui.renderers.AbstractRenderer; import com.vaadin.ui.renderers.Renderer; import com.vaadin.ui.renderers.TextRenderer; @@ -512,7 +514,7 @@ public class Grid<T> extends AbstractSingleSelect<T> implements HasComponents { if (rowKey != null) { item = getDataCommunicator().getKeyMapper().get(rowKey); } - fireEvent(new GridContextClickEvent<T>(Grid.this, details, section, + fireEvent(new GridContextClickEvent<>(Grid.this, details, section, rowIndex, item, getColumn(columnId))); } @@ -825,6 +827,15 @@ public class Grid<T> extends AbstractSingleSelect<T> implements HasComponents { } /** + * 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 @@ -1408,6 +1419,53 @@ public class Grid<T> extends AbstractSingleSelect<T> implements HasComponents { } } + /** + * 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 + * @return the header cell + */ + 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 + * @return the header cell + */ + 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 + */ + public void setText(String text); + } + private KeyMapper<Column<T, ?>> columnKeys = new KeyMapper<>(); private Set<Column<T, ?>> columnSet = new LinkedHashSet<>(); private List<SortOrder<Column<T, ?>>> sortOrder = new ArrayList<>(); @@ -1416,15 +1474,26 @@ public class Grid<T> extends AbstractSingleSelect<T> implements HasComponents { private StyleGenerator<T> styleGenerator = item -> null; private DescriptionGenerator<T> descriptionGenerator; + private Header header = new Header() { + @Override + protected SectionState getState(boolean markAsDirty) { + return Grid.this.getState(markAsDirty).header; + } + }; + /** * Constructor for the {@link Grid} component. */ public Grid() { setSelectionModel(new SingleSelection()); registerRpc(new GridServerRpcImpl()); + + appendHeaderRow(); + detailsManager = new DetailsManager<>(); addExtension(detailsManager); addDataGenerator(detailsManager); + addDataGenerator((item, json) -> { String styleName = styleGenerator.apply(item); if (styleName != null && !styleName.isEmpty()) { @@ -1455,8 +1524,6 @@ public class Grid<T> extends AbstractSingleSelect<T> implements HasComponents { * the value provider * @param renderer * the column value class - * @param <T> - * the type of this grid * @param <V> * the column value type * @@ -1467,13 +1534,23 @@ public class Grid<T> extends AbstractSingleSelect<T> implements HasComponents { public <V> Column<T, V> addColumn(String caption, Function<T, ? extends V> valueProvider, AbstractRenderer<? super T, V> renderer) { - Column<T, V> column = new Column<>(caption, valueProvider, renderer); + final Column<T, V> column = new Column<>(caption, valueProvider, + renderer); + final String columnId = columnKeys.key(column); column.extend(this); - column.setId(columnKeys.key(column)); + column.setId(columnId); columnSet.add(column); addDataGenerator(column); + getHeader().addColumn(columnId); + + if (getHeaderRowCount() > 0) { + // TODO Default header API to be added in a later patch + HeaderRow defaultHeader = getHeaderRow(0); + defaultHeader.getCell(columnId).setText(caption); + } + return column; } @@ -1762,6 +1839,130 @@ public class Grid<T> extends AbstractSingleSelect<T> implements HasComponents { return descriptionGenerator; } + // + // HEADER AND FOOTER + // + + /** + * Returns the header row at the given index. + * + * @param rowIndex + * 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 rowIndex) { + return getHeader().getRow(rowIndex); + } + + /** + * 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. + * + * @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 rowIndex + * the index of the row to remove, where the topmost row has + * index zero + * + * @throws IndexOutOfBoundsException + * if {@code rowIndex < 0 || rowIndex >= getHeaderRowCount()} + * + * @see #removeHeaderRow(HeaderRow) + * @see #addHeaderRowAt(int) + * @see #appendHeaderRow() + * @see #prependHeaderRow() + */ + public void removeHeaderRow(int rowIndex) { + getHeader().removeRow(rowIndex); + } + + /** + * Returns the header section of this grid. The default header contains a + * single row displaying the column captions. + * + * @return the header section + */ + protected Header getHeader() { + return header; + } + /** * Registers a new column resize listener. * diff --git a/server/src/main/java/com/vaadin/ui/components/grid/Header.java b/server/src/main/java/com/vaadin/ui/components/grid/Header.java new file mode 100644 index 0000000000..8348795c10 --- /dev/null +++ b/server/src/main/java/com/vaadin/ui/components/grid/Header.java @@ -0,0 +1,57 @@ +/* + * 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.components.grid; + +import com.vaadin.ui.Grid; + +/** + * Represents the header section of a Grid. + */ +public abstract class Header extends StaticSection<Header.Row> { + + public class Row extends StaticSection.StaticRow<Row.Cell> + implements Grid.HeaderRow { + + public class Cell extends StaticSection.StaticCell implements + Grid.HeaderCell { + protected Cell() { + super(Row.this); + } + } + + /** + * @param section + */ + protected Row() { + super(Header.this); + } + + @Override + protected Cell createCell() { + return new Cell(); + } + + @Override + protected String getCellTagName() { + return "th"; + } + } + + @Override + public Row createRow() { + return new Row(); + } +} diff --git a/server/src/main/java/com/vaadin/ui/components/grid/StaticSection.java b/server/src/main/java/com/vaadin/ui/components/grid/StaticSection.java new file mode 100644 index 0000000000..dbd5749bd0 --- /dev/null +++ b/server/src/main/java/com/vaadin/ui/components/grid/StaticSection.java @@ -0,0 +1,307 @@ +/* + * 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.components.grid; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import com.vaadin.shared.ui.grid.SectionState; +import com.vaadin.shared.ui.grid.SectionState.CellState; +import com.vaadin.shared.ui.grid.SectionState.RowState; + +/** + * Represents the header or footer section of a Grid. + * + * @author Vaadin Ltd. + * + * @param <ROW> + * the type of the rows in the section + * + * @since 8.0 + */ +public abstract class StaticSection<ROW extends StaticSection.StaticRow<?>> + implements Serializable { + + /** + * Abstract base class for Grid header and footer rows. + * + * @param <CELL> + * the type of the cells in the row + */ + public abstract static class StaticRow<CELL extends StaticCell> + implements Serializable { + + private RowState rowState = new RowState(); + private StaticSection<?> section; + private Map<Object, CELL> cells = new LinkedHashMap<>(); + + /** + * Creates a new row belonging to the given section. + * + * @param section + * the section of the row + */ + protected StaticRow(StaticSection<?> section) { + this.section = section; + } + + /** + * Creates and returns a new instance of the cell type. + * + * @return the created cell + */ + protected abstract CELL createCell(); + + /** + * Returns the declarative tag name used for the cells in this row. + * + * @return the cell tag name + */ + protected abstract String getCellTagName(); + + /** + * Adds a cell to this section, corresponding to the given column id. + * + * @param columnId + * the id of the column for which to add a cell + */ + protected void addCell(String columnId) { + CELL cell = createCell(); + cell.setColumnId(columnId); + cells.put(columnId, cell); + rowState.cells.put(columnId, cell.getCellState()); + } + + /** + * Removes the cell from this section that corresponds to the given + * column id. If there is no such cell, does nothing. + * + * @param columnId + * the id of the column from which to remove the cell + */ + protected void removeCell(Object columnId) { + CELL cell = cells.remove(columnId); + if (cell != null) { + rowState.cells.remove(cell.getCellState()); + } + } + + /** + * Returns the shared state of this row. + * + * @return the row state + */ + protected RowState getRowState() { + return rowState; + } + + /** + * Returns the cell in this section that corresponds to the given column + * id. + * + * @param columnId + * the id of the column + * @return the cell for the given column or null if not found + */ + public CELL getCell(String columnId) { + CELL cell = cells.get(columnId); + return cell; + } + } + + /** + * A header or footer cell. Has a simple textual caption. + */ + abstract static class StaticCell implements Serializable { + + private CellState cellState = new CellState(); + private StaticRow<?> row; + + protected StaticCell(StaticRow<?> row) { + this.row = row; + } + + void setColumnId(String id) { + cellState.columnId = id; + } + + String getColumnId() { + return cellState.columnId; + } + + /** + * Gets the row where this cell is. + * + * @return row for this cell + */ + public StaticRow<?> getRow() { + return row; + } + + /** + * Returns the shared state of this cell. + * + * @return the cell state + */ + protected CellState getCellState() { + return cellState; + } + + /** + * Sets the textual caption of this cell. + * + * @param text + * a plain text caption, not null + */ + public void setText(String text) { + Objects.requireNonNull(text, "text cannot be null"); + cellState.text = text; + row.section.markAsDirty(); + } + + /** + * Returns the textual caption of this cell. + * + * @return the plain text caption + */ + public String getText() { + return cellState.text; + } + } + + private List<ROW> rows = new ArrayList<>(); + + /** + * Creates a new row instance. + * + * @return the new row + */ + protected abstract ROW createRow(); + + /** + * Returns the shared state of this section. + * + * @param markAsDirty + * {@code true} to mark the state as modified, {@code false} + * otherwise + * @return the section state + */ + protected abstract SectionState getState(boolean markAsDirty); + + /** + * Marks the state of this section as modified. + */ + protected void markAsDirty() { + getState(true); + } + + /** + * Adds a new row at the given index. + * + * @param index + * the index of the new row + * @return the added row + * @throws IndexOutOfBoundsException + * if {@code index < 0 || index > getRowCount()} + */ + public ROW addRowAt(int index) { + ROW row = createRow(); + rows.add(index, row); + getState(true).rows.add(index, row.getRowState()); + return row; + } + + /** + * Removes the row at the given index. + * + * @param index + * the index of the row to remove + * @throws IndexOutOfBoundsException + * if {@code index < 0 || index >= getRowCount()} + */ + public void removeRow(int index) { + rows.remove(index); + getState(true).rows.remove(index); + } + + /** + * Removes the given row from this section. + * + * @param row + * the row to remove, not null + * @throws IllegalArgumentException + * if this section does not contain the row + */ + public void removeRow(Object row) { + Objects.requireNonNull(row, "row cannot be null"); + int index = rows.indexOf(row); + if (index < 0) { + throw new IllegalArgumentException( + "Section does not contain the given row"); + } + removeRow(index); + } + + /** + * Returns the row at the given index. + * + * @param index + * the index of the row + * @return the row at the index + * @throws IndexOutOfBoundsException + * if {@code index < 0 || index >= getRowCount()} + */ + public ROW getRow(int index) { + return rows.get(index); + } + + /** + * Returns the number of rows in this section. + * + * @return the number of rows + */ + public int getRowCount() { + return rows.size(); + } + + /** + * Adds a cell corresponding to the given column id to this section. + * + * @param columnId + * the id of the column for which to add a cell + */ + public void addColumn(String columnId) { + for (ROW row : rows) { + row.addCell(columnId); + } + } + + /** + * Removes the cell corresponding to the given column id. + * + * @param columnId + * the id of the column whose cell to remove + */ + public void removeColumn(String columnId) { + for (ROW row : rows) { + row.removeCell(columnId); + } + } +} diff --git a/server/src/test/java/com/vaadin/tests/server/component/grid/GridHeaderFooterTest.java b/server/src/test/java/com/vaadin/tests/server/component/grid/GridHeaderFooterTest.java new file mode 100644 index 0000000000..87c2632b40 --- /dev/null +++ b/server/src/test/java/com/vaadin/tests/server/component/grid/GridHeaderFooterTest.java @@ -0,0 +1,155 @@ +/* + * 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.tests.server.component.grid; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.fail; + +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.ui.Grid; +import com.vaadin.ui.Grid.Column; +import com.vaadin.ui.Grid.HeaderRow; + +public class GridHeaderFooterTest { + + private Grid<String> grid; + private Column<?, ?> column1, column2; + + @Before + public void setUp() { + grid = new Grid<>(); + + column1 = grid.addColumn("First", s -> s.substring(0, 1)); + column2 = grid.addColumn("Rest", s -> s.substring(1)); + } + + @Test + public void initialState_hasDefaultHeader() { + assertEquals(1, grid.getHeaderRowCount()); + HeaderRow defaultHeader = grid.getHeaderRow(0); + assertEquals("First", defaultHeader.getCell(column1).getText()); + assertEquals("Rest", defaultHeader.getCell(column2).getText()); + } + + @Test + public void initialState_defaultHeaderRemovable() { + grid.removeHeaderRow(0); + assertEquals(0, grid.getHeaderRowCount()); + } + + @Test + public void appendHeaderRow_addedToBottom() { + HeaderRow defaultRow = grid.getHeaderRow(0); + HeaderRow addedRow = grid.appendHeaderRow(); + + assertSame(defaultRow, grid.getHeaderRow(0)); + assertSame(addedRow, grid.getHeaderRow(1)); + } + + @Test + public void prependHeaderRow_addedToTop() { + HeaderRow defaultRow = grid.getHeaderRow(0); + HeaderRow addedRow = grid.prependHeaderRow(); + + assertSame(addedRow, grid.getHeaderRow(0)); + assertSame(defaultRow, grid.getHeaderRow(1)); + } + + @Test + public void addHeaderRowAtZero_addedToTop() { + HeaderRow defaultRow = grid.getHeaderRow(0); + HeaderRow addedRow = grid.addHeaderRowAt(0); + + assertSame(addedRow, grid.getHeaderRow(0)); + assertSame(defaultRow, grid.getHeaderRow(1)); + } + + @Test + public void addHeaderRowAtRowCount_addedToBottom() { + HeaderRow defaultRow = grid.getHeaderRow(0); + HeaderRow addedRow = grid.addHeaderRowAt(grid.getHeaderRowCount()); + + assertSame(defaultRow, grid.getHeaderRow(0)); + assertSame(addedRow, grid.getHeaderRow(1)); + } + + @Test + public void removeExistingHeaderRow_removed() { + HeaderRow defaultRow = grid.getHeaderRow(0); + HeaderRow addedRow = grid.appendHeaderRow(); + + grid.removeHeaderRow(addedRow); + + assertEquals(1, grid.getHeaderRowCount()); + assertSame(defaultRow, grid.getHeaderRow(0)); + } + + @Test + public void removeDefaultHeaderRow_removed() { + HeaderRow defaultRow = grid.getHeaderRow(0); + HeaderRow addedRow = grid.appendHeaderRow(); + + grid.removeHeaderRow(defaultRow); + + assertEquals(1, grid.getHeaderRowCount()); + assertSame(addedRow, grid.getHeaderRow(0)); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void getHeaderRowNegativeIndex_throws() { + grid.getHeaderRow(-1); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void getHeaderRowIndexTooLarge_throws() { + grid.appendHeaderRow(); + grid.getHeaderRow(2); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void addHeaderRowAtNegativeIndex_throws() { + grid.addHeaderRowAt(-1); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void addHeaderRowAtIndexTooLarge_throws() { + grid.addHeaderRowAt(2); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void removeHeaderRowNegativeIndex_throws() { + grid.removeHeaderRow(-1); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void removeHeaderRowIndexTooLarge_throws() { + grid.removeHeaderRow(1); + } + + @Test(expected = IllegalArgumentException.class) + public void removeNonExistingHeaderRow_throws() { + HeaderRow row = grid.getHeaderRow(0); + try { + grid.removeHeaderRow(row); + } catch (Exception e) { + fail("unexpected exception: " + e); + } + grid.removeHeaderRow(row); + } +} |