From 4caa2f5b6e26ade52a4fba66a0a020b79f9008ea Mon Sep 17 00:00:00 2001 From: John Ahlroos Date: Wed, 6 Nov 2013 10:35:03 +0200 Subject: [PATCH] Multiple headers and footer rows #3153 Change-Id: Iadb0d8b051d0f0ef1303e0d7d740cf476cd81971 --- .../vaadin/client/ui/grid/ColumnGroup.java | 117 +++++ .../vaadin/client/ui/grid/ColumnGroupRow.java | 188 +++++++ .../src/com/vaadin/client/ui/grid/Grid.java | 482 +++++++++++++++--- .../vaadin/client/ui/grid/GridConnector.java | 92 +++- .../ui/components/grid/ColumnGroup.java | 141 +++++ .../ui/components/grid/ColumnGroupRow.java | 255 +++++++++ .../com/vaadin/ui/components/grid/Grid.java | 134 ++++- .../vaadin/ui/components/grid/GridColumn.java | 15 +- .../server/component/grid/GridColumns.java | 92 +++- .../shared/ui/grid/ColumnGroupRowState.java | 46 ++ .../shared/ui/grid/ColumnGroupState.java | 45 ++ .../shared/ui/grid/GridColumnState.java | 6 - .../com/vaadin/shared/ui/grid/GridState.java | 12 +- .../components/grid/GridBasicFeatures.java | 149 ++++-- .../components/grid/GridColumnGroups.java | 111 ++++ 15 files changed, 1679 insertions(+), 206 deletions(-) create mode 100644 client/src/com/vaadin/client/ui/grid/ColumnGroup.java create mode 100644 client/src/com/vaadin/client/ui/grid/ColumnGroupRow.java create mode 100644 server/src/com/vaadin/ui/components/grid/ColumnGroup.java create mode 100644 server/src/com/vaadin/ui/components/grid/ColumnGroupRow.java create mode 100644 shared/src/com/vaadin/shared/ui/grid/ColumnGroupRowState.java create mode 100644 shared/src/com/vaadin/shared/ui/grid/ColumnGroupState.java create mode 100644 uitest/src/com/vaadin/tests/components/grid/GridColumnGroups.java diff --git a/client/src/com/vaadin/client/ui/grid/ColumnGroup.java b/client/src/com/vaadin/client/ui/grid/ColumnGroup.java new file mode 100644 index 0000000000..c37068def7 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/ColumnGroup.java @@ -0,0 +1,117 @@ +/* + * 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.Collection; +import java.util.Collections; +import java.util.List; + +/** + * Column groups are used to group columns together for adding common auxiliary + * headers and footers. Columns groups are added to {@link ColumnGroupRow + * ColumnGroupRows}. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class ColumnGroup { + + /** + * The text shown in the header + */ + private String header; + + /** + * The text shown in the footer + */ + private String footer; + + /** + * The columns included in the group when also accounting for subgroup + * columns + */ + private final List columns; + + /** + * The grid associated with the column group + */ + private final Grid grid; + + /** + * Constructs a new column group + */ + ColumnGroup(Grid grid, Collection columns) { + if (columns == null) { + throw new IllegalArgumentException( + "columns cannot be null. Pass an empty list instead."); + } + this.grid = grid; + this.columns = Collections.unmodifiableList(new ArrayList( + columns)); + } + + /** + * Gets the header text. + * + * @return the header text + */ + public String getHeaderCaption() { + return header; + } + + /** + * Sets the text shown in the header. + * + * @param header + * the header to set + */ + public void setHeaderCaption(String header) { + this.header = header; + grid.refreshHeader(); + } + + /** + * Gets the text shown in the footer. + * + * @return the text in the footer + */ + public String getFooterCaption() { + return footer; + } + + /** + * Sets the text displayed in the footer. + * + * @param footer + * the footer to set + */ + public void setFooterCaption(String footer) { + this.footer = footer; + grid.refreshFooter(); + } + + /** + * Returns all column in this group. It includes the subgroups columns as + * well. + * + * @return unmodifiable list of columns + */ + public List getColumns() { + return columns; + } +} diff --git a/client/src/com/vaadin/client/ui/grid/ColumnGroupRow.java b/client/src/com/vaadin/client/ui/grid/ColumnGroupRow.java new file mode 100644 index 0000000000..6bbc9bc9eb --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/ColumnGroupRow.java @@ -0,0 +1,188 @@ +/* + * 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.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * A column group row represents an auxiliary header or footer row added to the + * grid. A column group row includes column groups that group columns together. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class ColumnGroupRow { + + /** + * The column groups in this row + */ + private List groups = new ArrayList(); + + /** + * The grid associated with the column row + */ + private final Grid grid; + + /** + * Is the header shown + */ + public boolean headerVisible = true; + + /** + * Is the footer shown + */ + public boolean footerVisible = false; + + /** + * Constructs a new column group row + * + * @param grid + * Grid associated with this column + * + */ + ColumnGroupRow(Grid grid) { + this.grid = grid; + } + + /** + * Add a new group to the row by using column instances. + * + * @param columns + * The columns that should belong to the group + * @return a column group representing the collection of columns added to + * the group. + */ + public ColumnGroup addGroup(GridColumn... columns) { + + for (GridColumn column : columns) { + if (isColumnGrouped(column)) { + throw new IllegalArgumentException("Column " + + String.valueOf(column.getHeaderCaption()) + + " already belongs to another group."); + } + } + + ColumnGroup group = new ColumnGroup(grid, Arrays.asList(columns)); + groups.add(group); + grid.refreshHeader(); + grid.refreshFooter(); + return group; + } + + /** + * Add a new group to the row by using other already greated groups + * + * @param groups + * The subgroups of the group. + * @return a column group representing the collection of columns added to + * the group. + * + */ + public ColumnGroup addGroup(ColumnGroup... groups) { + assert groups != null : "groups cannot be null"; + + Set columns = new HashSet(); + for (ColumnGroup group : groups) { + columns.addAll(group.getColumns()); + } + + ColumnGroup group = new ColumnGroup(grid, columns); + this.groups.add(group); + grid.refreshHeader(); + grid.refreshFooter(); + return group; + } + + /** + * Removes a group from the row. + * + * @param group + * The group to remove + */ + public void removeGroup(ColumnGroup group) { + groups.remove(group); + grid.refreshHeader(); + grid.refreshFooter(); + } + + /** + * Get the groups in the row + * + * @return unmodifiable list of groups in this row + */ + public List getGroups() { + return Collections.unmodifiableList(groups); + } + + /** + * Is the header visible for the row. + * + * @return true if header is visible + */ + public boolean isHeaderVisible() { + return headerVisible; + } + + /** + * Sets the header visible for the row. + * + * @param visible + * should the header be shown + */ + public void setHeaderVisible(boolean visible) { + headerVisible = visible; + grid.refreshHeader(); + } + + /** + * Is the footer visible for the row. + * + * @return true if footer is visible + */ + public boolean isFooterVisible() { + return footerVisible; + } + + /** + * Sets the footer visible for the row. + * + * @param visible + * should the footer be shown + */ + public void setFooterVisible(boolean visible) { + footerVisible = visible; + grid.refreshFooter(); + } + + /** + * Iterates all the column groups and checks if the columns alread has been + * added to a group. + */ + private boolean isColumnGrouped(GridColumn column) { + for (ColumnGroup group : groups) { + if (group.getColumns().contains(column)) { + return true; + } + } + return false; + } +} diff --git a/client/src/com/vaadin/client/ui/grid/Grid.java b/client/src/com/vaadin/client/ui/grid/Grid.java index 3c4e2d6e13..67f14301f0 100644 --- a/client/src/com/vaadin/client/ui/grid/Grid.java +++ b/client/src/com/vaadin/client/ui/grid/Grid.java @@ -55,7 +55,7 @@ import com.vaadin.shared.util.SharedUtil; public class Grid extends Composite { /** - * Escalator used internally by the grid to render the rows + * Escalator used internally by grid to render the rows */ private Escalator escalator = GWT.create(Escalator.class); @@ -65,8 +65,23 @@ public class Grid extends Composite { 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. + * The column groups rows added to the grid + */ + private final List columnGroupRows = new ArrayList(); + + /** + * Are the headers for the columns visible + */ + private boolean columnHeadersVisible = false; + + /** + * Are the footers for the columns visible + */ + private boolean columnFootersVisible = false; + + /** + * Base class for grid columns internally used by the Grid. The user should + * use {@link GridColumn} when creating new columns. * * @param * the row type @@ -74,24 +89,24 @@ public class Grid extends Composite { public static abstract class AbstractGridColumn { /** - * Grid associated with the column + * The grid the column is associated with */ private Grid grid; /** - * Text displayed in the column header + * Should the column be visible in the grid */ - private String header; + private boolean visible; /** - * Text displayed in the column footer + * The text displayed in the header of the column */ - private String footer; + private String header; /** - * Is the column visible + * Text displayed in the column footer */ - private boolean visible; + private String footer; /** * Internally used by the grid to set itself @@ -125,14 +140,15 @@ public class Grid extends Composite { * the text displayed in the column header */ public void setHeaderCaption(String caption) { - if (SharedUtil.equals(caption, this.header)) { + if (SharedUtil.equals(caption, header)) { return; } - this.header = caption; + header = caption; if (grid != null) { grid.refreshHeader(); + } } @@ -153,11 +169,11 @@ public class Grid extends Composite { * the text displayed in the footer of the column */ public void setFooterCaption(String caption) { - if (SharedUtil.equals(caption, this.footer)) { + if (SharedUtil.equals(caption, footer)) { return; } - this.footer = caption; + footer = caption; if (grid != null) { grid.refreshFooter(); @@ -177,7 +193,8 @@ public class Grid extends Composite { * Sets a column as visible in the grid. * * @param visible - * Set to true to show the column in the grid + * true if the column should be displayed in the + * grid */ public void setVisible(boolean visible) { if (this.visible == visible) { @@ -206,8 +223,9 @@ public class Grid extends Composite { * 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 + * The row object that provides the cell content. + * + * @return The cell content */ public abstract String getValue(T row); @@ -220,6 +238,122 @@ public class Grid extends Composite { } } + /** + * Base class for header / footer escalator updater + */ + protected abstract class HeaderFooterEscalatorUpdater implements + EscalatorUpdater { + + /** + * The row container which contains the header or footer rows + */ + private RowContainer rows; + + /** + * Should the index be counted from 0-> or 0<- + */ + private boolean inverted; + + /** + * Constructs an updater for updating a header / footer + * + * @param rows + * The row container + * @param inverted + * Should index counting be inverted + */ + public HeaderFooterEscalatorUpdater(RowContainer rows, boolean inverted) { + this.rows = rows; + this.inverted = inverted; + } + + /** + * Gets the header/footer caption value + * + * @return The value that should be rendered for the column caption + */ + public abstract String getColumnValue(GridColumn column); + + /** + * Gets the group caption value + * + * @param group + * The group for with the caption value should be returned + * @return The value that should be rendered for the column caption + */ + public abstract String getGroupValue(ColumnGroup group); + + /** + * Is the row visible in the header/footer + * + * @return true if the row should be visible + */ + public abstract boolean isRowVisible(ColumnGroupRow row); + + /** + * Should the first row be visible + * + * @return true if the first row should be visible + */ + public abstract boolean firstRowIsVisible(); + + @Override + public void updateCells(Row row, List cellsToUpdate) { + + int rowIndex; + if (inverted) { + rowIndex = rows.getRowCount() - row.getRow() - 1; + } else { + rowIndex = row.getRow(); + } + + if (firstRowIsVisible() && rowIndex == 0) { + // column headers + for (Cell cell : cellsToUpdate) { + int columnIndex = cell.getColumn(); + GridColumn column = columns.get(columnIndex); + cell.getElement().setInnerText(getColumnValue(column)); + } + + } else if (columnGroupRows.size() > 0) { + // Adjust for headers + if (firstRowIsVisible()) { + rowIndex--; + } + + // Adjust for previous invisible header rows + ColumnGroupRow groupRow = null; + for (int i = 0, realIndex = 0; i < columnGroupRows.size(); i++) { + groupRow = columnGroupRows.get(i); + if (isRowVisible(groupRow)) { + if (realIndex == rowIndex) { + rowIndex = realIndex; + break; + } + realIndex++; + } + } + + assert groupRow != null; + + for (Cell cell : cellsToUpdate) { + int columnIndex = cell.getColumn(); + GridColumn column = columns.get(columnIndex); + ColumnGroup group = getGroupForColumn(groupRow, column); + + if (group != null) { + // FIXME Should merge the group cells when escalator + // supports it + cell.getElement().setInnerText(getGroupValue(group)); + } else { + // Cells are reused + cell.getElement().setInnerHTML(null); + } + } + } + } + } + /** * Creates a new instance. */ @@ -229,6 +363,9 @@ public class Grid extends Composite { escalator.getHeader().setEscalatorUpdater(createHeaderUpdater()); escalator.getBody().setEscalatorUpdater(createBodyUpdater()); escalator.getFooter().setEscalatorUpdater(createFooterUpdater()); + + refreshHeader(); + refreshFooter(); } /** @@ -238,18 +375,26 @@ public class Grid extends Composite { * @return the updater that updates the data in the escalator. */ private EscalatorUpdater createHeaderUpdater() { - return new EscalatorUpdater() { + return new HeaderFooterEscalatorUpdater(escalator.getHeader(), true) { @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()); - } - } + public boolean isRowVisible(ColumnGroupRow row) { + return row.isHeaderVisible(); + } + + @Override + public String getGroupValue(ColumnGroup group) { + return group.getHeaderCaption(); + } + + @Override + public String getColumnValue(GridColumn column) { + return column.getHeaderCaption(); + } + + @Override + public boolean firstRowIsVisible() { + return isColumnHeadersVisible(); } }; } @@ -275,40 +420,80 @@ public class Grid extends Composite { * @return the updater that updates the data in the escalator. */ private EscalatorUpdater createFooterUpdater() { - return new EscalatorUpdater() { + return new HeaderFooterEscalatorUpdater(escalator.getFooter(), false) { @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()); - } - } + public boolean isRowVisible(ColumnGroupRow row) { + return row.isFooterVisible(); + } + + @Override + public String getGroupValue(ColumnGroup group) { + return group.getFooterCaption(); + } + + @Override + public String getColumnValue(GridColumn column) { + return column.getFooterCaption(); + } + + @Override + public boolean firstRowIsVisible() { + return isColumnFootersVisible(); } }; } /** - * Refreshes all header rows. + * Refreshes header or footer rows on demand + * + * @param rows + * The row container + * @param firstRowIsVisible + * is the first row visible + * @param isHeader + * true if we refreshing the header, else assumed + * the footer */ - private void refreshHeader() { - RowContainer header = escalator.getHeader(); - if (isHeaderVisible() && header.getRowCount() > 0) { - header.refreshRows(0, header.getRowCount()); + private void refreshRowContainer(RowContainer rows, + boolean firstRowIsVisible, boolean isHeader) { + + // Count needed rows + int totalRows = firstRowIsVisible ? 1 : 0; + for (ColumnGroupRow row : columnGroupRows) { + if (isHeader ? row.isHeaderVisible() : row.isFooterVisible()) { + totalRows++; + } + } + + // Add or Remove rows on demand + int rowDiff = totalRows - rows.getRowCount(); + if (rowDiff > 0) { + rows.insertRows(0, rowDiff); + } else if (rowDiff < 0) { + rows.removeRows(0, -rowDiff); + } + + // Refresh all the rows + if (rows.getRowCount() > 0) { + rows.refreshRows(0, rows.getRowCount()); } } /** - * Refreshes all footer rows. + * Refreshes all header rows */ - private void refreshFooter() { - RowContainer footer = escalator.getFooter(); - if (isFooterVisible() && footer.getRowCount() > 0) { - footer.refreshRows(0, footer.getRowCount()); - } + void refreshHeader() { + refreshRowContainer(escalator.getHeader(), isColumnHeadersVisible(), + true); + } + + /** + * Refreshes all footer rows + */ + void refreshFooter() { + refreshRowContainer(escalator.getFooter(), isColumnFootersVisible(), + false); } /** @@ -388,71 +573,200 @@ public class Grid extends Composite { * 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); + if (index < 0 || index >= columns.size()) { + throw new IllegalStateException("Column not found."); } + return columns.get(index); } /** - * Sets the header row visible. + * Set the column headers visible. + * + *

+ * A column header is a single cell header on top of each column reserved + * for a specific header for that column. The column header can be set by + * {@link GridColumn#setHeaderCaption(String)} and column headers cannot be + * merged with other column headers. + *

+ * + *

+ * All column headers occupy the first header row of the grid. If you do not + * wish to show the column headers in the grid you should hide the row by + * setting visibility of the header row to false. + *

+ * + *

+ * If you want to merge the column headers into groups you can use + * {@link ColumnGroupRow}s to group columns together and give them a common + * header. See {@link #addColumnGroupRow()} for details. + *

+ * + *

+ * The header row is by default visible. + *

* * @param visible - * true if header rows should be visible + * true if header rows should be visible */ - public void setHeaderVisible(boolean visible) { - if (visible == isHeaderVisible()) { + public void setColumnHeadersVisible(boolean visible) { + if (visible == isColumnHeadersVisible()) { return; } - - RowContainer header = escalator.getHeader(); - - // TODO Should support multiple headers - if (visible) { - header.insertRows(0, 1); - } else { - header.removeRows(0, 1); - } + columnHeadersVisible = visible; + refreshHeader(); } /** - * Are the header row(s) visible? + * Are the column headers visible * - * @return true if the header is visible + * @return true if they are visible */ - public boolean isHeaderVisible() { - return escalator.getHeader().getRowCount() > 0; + public boolean isColumnHeadersVisible() { + return columnHeadersVisible; } /** - * Sets the footer row(s) visible. + * Set the column footers visible. + * + *

+ * A column footer is a single cell footer below of each column reserved for + * a specific footer for that column. The column footer can be set by + * {@link GridColumn#setFooterCaption(String)} and column footers cannot be + * merged with other column footers. + *

+ * + *

+ * All column footers occupy the first footer row of the grid. If you do not + * wish to show the column footers in the grid you should hide the row by + * setting visibility of the footer row to false. + *

+ * + *

+ * If you want to merge the column footers into groups you can use + * {@link ColumnGroupRow}s to group columns together and give them a common + * footer. See {@link #addColumnGroupRow()} for details. + *

+ * + *

+ * The footer row is by default hidden. + *

* * @param visible - * true if header rows should be visible + * true if the footer row should be visible */ - public void setFooterVisible(boolean visible) { - if (visible == isFooterVisible()) { + public void setColumnFootersVisible(boolean visible) { + if (visible == isColumnFootersVisible()) { return; } + this.columnFootersVisible = visible; + refreshFooter(); + } - RowContainer footer = escalator.getFooter(); + /** + * Are the column footers visible + * + * @return true if they are visible + * + */ + public boolean isColumnFootersVisible() { + return columnFootersVisible; + } - // TODO Should support multiple footers - if (visible) { - footer.insertRows(0, 1); - } else { - footer.removeRows(0, 1); - } + /** + * Adds a new column group row to the grid. + * + *

+ * Column group rows are rendered in the header and footer of the grid. + * Column group rows are made up of column groups which groups together + * columns for adding a common auxiliary header or footer for the columns. + *

+ * + * Example usage: + * + *
+     * // Add a new column group row to the grid
+     * ColumnGroupRow row = grid.addColumnGroupRow();
+     * 
+     * // Group "Column1" and "Column2" together to form a header in the row
+     * ColumnGroup column12 = row.addGroup("Column1", "Column2");
+     * 
+     * // Set a common header for "Column1" and "Column2"
+     * column12.setHeader("Column 1&2");
+     * 
+     * // Set a common footer for "Column1" and "Column2"
+     * column12.setFooter("Column 1&2");
+     * 
+ * + * @return a column group row instance you can use to add column groups + */ + public ColumnGroupRow addColumnGroupRow() { + ColumnGroupRow row = new ColumnGroupRow(this); + columnGroupRows.add(row); + refreshHeader(); + refreshFooter(); + return row; + } + + /** + * Adds a new column group row to the grid at a specific index. + * + * @see #addColumnGroupRow() {@link Grid#addColumnGroupRow()} for example + * usage + * + * @param rowIndex + * the index where the column group row should be added + * @return a column group row instance you can use to add column groups + */ + public ColumnGroupRow addColumnGroupRow(int rowIndex) { + ColumnGroupRow row = new ColumnGroupRow(this); + columnGroupRows.add(rowIndex, row); + refreshHeader(); + refreshFooter(); + return row; } /** - * Are the footer row(s) visible? + * Removes a column group row * - * @return true if the footer is visible + * @param row + * The row to remove */ - public boolean isFooterVisible() { - return escalator.getFooter().getRowCount() > 0; + public void removeColumnGroupRow(ColumnGroupRow row) { + columnGroupRows.remove(row); + refreshHeader(); + refreshFooter(); + } + + /** + * Get the column group rows + * + * @return a unmodifiable list of column group rows + * + */ + public List getColumnGroupRows() { + return Collections.unmodifiableList(new ArrayList( + columnGroupRows)); + } + + /** + * Returns the column group for a row and column + * + * @param row + * The row of the column + * @param column + * the column to get the group for + * @return A column group for the row and column or null if not + * found. + */ + private static ColumnGroup getGroupForColumn(ColumnGroupRow row, + GridColumn column) { + for (ColumnGroup group : row.getGroups()) { + List columns = group.getColumns(); + if (columns.contains(column)) { + return group; + } + } + return null; } @Override diff --git a/client/src/com/vaadin/client/ui/grid/GridConnector.java b/client/src/com/vaadin/client/ui/grid/GridConnector.java index c48c9936bc..32907e1e29 100644 --- a/client/src/com/vaadin/client/ui/grid/GridConnector.java +++ b/client/src/com/vaadin/client/ui/grid/GridConnector.java @@ -16,15 +16,19 @@ package com.vaadin.client.ui.grid; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.List; 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.ColumnGroupRowState; +import com.vaadin.shared.ui.grid.ColumnGroupState; import com.vaadin.shared.ui.grid.GridColumnState; import com.vaadin.shared.ui.grid.GridState; @@ -38,6 +42,10 @@ import com.vaadin.shared.ui.grid.GridState; @Connect(com.vaadin.ui.components.grid.Grid.class) public class GridConnector extends AbstractComponentConnector { + /** + * Custom implementation of the custom grid column using a String[] to + * represent the cell value + */ private class CustomGridColumn extends GridColumn { @Override @@ -47,7 +55,9 @@ public class GridConnector extends AbstractComponentConnector { } } - // Maps a generated column id -> A grid column instance + /** + * Maps a generated column id to a grid column instance + */ private Map columnIdToColumn = new HashMap(); @Override @@ -71,16 +81,6 @@ public class GridConnector extends AbstractComponentConnector { 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")) { @@ -92,7 +92,7 @@ public class GridConnector extends AbstractComponentConnector { // Add new columns for (int columnIndex = currentColumns; columnIndex < totalColumns; columnIndex++) { - addColumnFromStateChangeEvent(columnIndex, stateChangeEvent); + addColumnFromStateChangeEvent(columnIndex); } // Update old columns @@ -100,9 +100,26 @@ public class GridConnector extends AbstractComponentConnector { // 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); + updateColumnFromStateChangeEvent(columnIndex); } } + + // Header + if (stateChangeEvent.hasPropertyChanged("columnHeadersVisible")) { + getWidget() + .setColumnHeadersVisible(getState().columnHeadersVisible); + } + + // Footer + if (stateChangeEvent.hasPropertyChanged("columnFootersVisible")) { + getWidget() + .setColumnFootersVisible(getState().columnFootersVisible); + } + + // Column row groups + if (stateChangeEvent.hasPropertyChanged("columnGroupRows")) { + updateColumnGroupsFromStateChangeEvent(); + } } /** @@ -110,12 +127,8 @@ public class GridConnector extends AbstractComponentConnector { * * @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) { + private void updateColumnFromStateChangeEvent(int columnIndex) { GridColumn column = getWidget().getColumn(columnIndex); GridColumnState columnState = getState().columns.get(columnIndex); updateColumnFromState(column, columnState); @@ -126,30 +139,30 @@ public class GridConnector extends AbstractComponentConnector { * * @param columnIndex * The index of the column, according to how it - * @param stateChangeEvent */ - private void addColumnFromStateChangeEvent(int columnIndex, - StateChangeEvent stateChangeEvent) { + private void addColumnFromStateChangeEvent(int columnIndex) { 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 + * Updates the column values from a state * * @param column * The column to update * @param state - * The state to update from + * The state to get the data from */ private static void updateColumnFromState(GridColumn column, GridColumnState state) { + column.setVisible(state.visible); column.setHeaderCaption(state.header); column.setFooterCaption(state.footer); - column.setVisible(state.visible); } /** @@ -176,4 +189,35 @@ public class GridConnector extends AbstractComponentConnector { } } } + + /** + * Updates the column groups from a state change + */ + private void updateColumnGroupsFromStateChangeEvent() { + + // FIXME When something changes the header/footer rows will be + // re-created. At some point we should optimize this so partial updates + // can be made on the header/footer. + for (ColumnGroupRow row : getWidget().getColumnGroupRows()) { + getWidget().removeColumnGroupRow(row); + } + + for (ColumnGroupRowState rowState : getState().columnGroupRows) { + ColumnGroupRow row = getWidget().addColumnGroupRow(); + row.setFooterVisible(rowState.footerVisible); + row.setHeaderVisible(rowState.headerVisible); + + for (ColumnGroupState groupState : rowState.groups) { + List columns = new ArrayList(); + for (String columnId : groupState.columns) { + CustomGridColumn column = columnIdToColumn.get(columnId); + columns.add(column); + } + ColumnGroup group = row.addGroup(columns + .toArray(new GridColumn[columns.size()])); + group.setFooterCaption(groupState.footer); + group.setHeaderCaption(groupState.header); + } + } + } } diff --git a/server/src/com/vaadin/ui/components/grid/ColumnGroup.java b/server/src/com/vaadin/ui/components/grid/ColumnGroup.java new file mode 100644 index 0000000000..0ab1f61a46 --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/ColumnGroup.java @@ -0,0 +1,141 @@ +/* + * 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.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import com.vaadin.shared.ui.grid.ColumnGroupState; + +/** + * Column groups are used to group columns together for adding common auxiliary + * headers and footers. Columns groups are added to {@link ColumnGroupRow}'s. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class ColumnGroup implements Serializable { + + /** + * List of property ids belonging to this group + */ + private List columns; + + /** + * The grid the column group is associated with + */ + private final Grid grid; + + /** + * The common state between the server and the client + */ + private final ColumnGroupState state; + + /** + * Constructs a new column group + * + * @param grid + * the grid the column group is associated with + * @param state + * the state representing the data of the grid. Sent to the + * client + * @param propertyIds + * the property ids of the columns that belongs to the group + * @param groups + * the sub groups who should be included in this group + * + */ + ColumnGroup(Grid grid, ColumnGroupState state, List propertyIds) { + if (propertyIds == null) { + throw new IllegalArgumentException( + "propertyIds cannot be null. Use empty list instead."); + } + + this.state = state; + columns = Collections.unmodifiableList(new ArrayList( + propertyIds)); + this.grid = grid; + } + + /** + * Sets the text displayed in the header of the column group. + * + * @param header + * the text displayed in the header of the column + */ + public void setHeaderCaption(String header) { + state.header = header; + grid.markAsDirty(); + } + + /** + * Sets the text displayed in the header of the column group. + * + * @return the text displayed in the header of the column + */ + public String getHeaderCaption() { + return state.header; + } + + /** + * Sets the text displayed in the footer of the column group. + * + * @param footer + * the text displayed in the footer of the column + */ + public void setFooterCaption(String footer) { + state.footer = footer; + grid.markAsDirty(); + } + + /** + * The text displayed in the footer of the column group. + * + * @return the text displayed in the footer of the column + */ + public String getFooterCaption() { + return state.footer; + } + + /** + * Is a property id in this group or in some sub group of this group. + * + * @param propertyId + * the property id to check for + * @return true if the property id is included in this group. + */ + public boolean isColumnInGroup(Object propertyId) { + if (columns.contains(propertyId)) { + return true; + } + return false; + } + + /** + * Returns a list of property ids where all also the child groups property + * ids are included. + * + * @return a unmodifiable list with all the columns in the group. Includes + * any subgroup columns as well. + */ + public List getColumns() { + return columns; + } + +} diff --git a/server/src/com/vaadin/ui/components/grid/ColumnGroupRow.java b/server/src/com/vaadin/ui/components/grid/ColumnGroupRow.java new file mode 100644 index 0000000000..326d2826f5 --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/ColumnGroupRow.java @@ -0,0 +1,255 @@ +/* + * 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.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import com.vaadin.server.KeyMapper; +import com.vaadin.shared.ui.grid.ColumnGroupRowState; +import com.vaadin.shared.ui.grid.ColumnGroupState; + +/** + * A column group row represents an auxiliary header or footer row added to the + * grid. A column group row includes column groups that group columns together. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class ColumnGroupRow implements Serializable { + + /** + * The common state shared between the client and server + */ + private final ColumnGroupRowState state; + + /** + * The column groups in this row + */ + private List groups = new ArrayList(); + + /** + * Grid that the group row belongs to + */ + private final Grid grid; + + /** + * The column keys used to identify the column on the client side + */ + private final KeyMapper columnKeys; + + /** + * Constructs a new column group + * + * @param grid + * The grid that the column group is associated to + * @param state + * The shared state which contains the data shared between server + * and client + * @param columnKeys + * The column key mapper for converting property ids to client + * side column identifiers + */ + ColumnGroupRow(Grid grid, ColumnGroupRowState state, + KeyMapper columnKeys) { + this.grid = grid; + this.columnKeys = columnKeys; + this.state = state; + } + + /** + * Gets the shared state for the column group row. Used internally to send + * the group row to the client. + * + * @return The current state of the row + */ + ColumnGroupRowState getState() { + return state; + } + + /** + * Add a new group to the row by using property ids for the columns. + * + * @param propertyIds + * The property ids of the columns that should be included in the + * group. A column can only belong in group on a row at a time. + * @return a column group representing the collection of columns added to + * the group + */ + public ColumnGroup addGroup(Object... propertyIds) { + assert propertyIds != null : "propertyIds cannot be null."; + + for (Object propertyId : propertyIds) { + if (hasColumnBeenGrouped(propertyId)) { + throw new IllegalArgumentException("Column " + + String.valueOf(propertyId) + + " already belongs to another group."); + } + } + + ColumnGroupState state = new ColumnGroupState(); + for (Object propertyId : propertyIds) { + assert propertyId != null : "null items in columns array not supported."; + state.columns.add(columnKeys.key(propertyId)); + } + this.state.groups.add(state); + + ColumnGroup group = new ColumnGroup(grid, state, + Arrays.asList(propertyIds)); + groups.add(group); + + grid.markAsDirty(); + return group; + } + + /** + * Add a new group to the row by using column instances. + * + * @param columns + * the columns that should belong to the group + * @return a column group representing the collection of columns added to + * the group + */ + public ColumnGroup addGroup(GridColumn... columns) { + assert columns != null : "columns cannot be null"; + + List propertyIds = new ArrayList(); + for (GridColumn column : columns) { + assert column != null : "null items in columns array not supported."; + + String columnId = column.getState().id; + Object propertyId = grid.getPropertyIdByColumnId(columnId); + propertyIds.add(propertyId); + } + return addGroup(propertyIds.toArray()); + } + + /** + * Add a new group to the row by using other already greated groups + * + * @param groups + * the subgroups of the group + * @return a column group representing the collection of columns added to + * the group + * + */ + public ColumnGroup addGroup(ColumnGroup... groups) { + assert groups != null : "groups cannot be null"; + + // Gather all groups columns into one list + List propertyIds = new ArrayList(); + for (ColumnGroup group : groups) { + propertyIds.addAll(group.getColumns()); + } + + ColumnGroupState state = new ColumnGroupState(); + ColumnGroup group = new ColumnGroup(grid, state, propertyIds); + this.groups.add(group); + + // Update state + for (Object propertyId : group.getColumns()) { + state.columns.add(columnKeys.key(propertyId)); + } + this.state.groups.add(state); + + grid.markAsDirty(); + return group; + } + + /** + * Removes a group from the row. Does not remove the group from subgroups, + * to remove it from the subgroup invoke removeGroup on the subgroup. + * + * @param group + * the group to remove + */ + public void removeGroup(ColumnGroup group) { + int index = groups.indexOf(group); + groups.remove(index); + state.groups.remove(index); + grid.markAsDirty(); + } + + /** + * Get the groups in the row. + * + * @return unmodifiable list of groups in this row + */ + public List getGroups() { + return Collections.unmodifiableList(groups); + } + + /** + * Checks if a property id has been added to a group in this row. + * + * @param propertyId + * the property id to check for + * @return true if the column is included in a group + */ + private boolean hasColumnBeenGrouped(Object propertyId) { + for (ColumnGroup group : groups) { + if (group.isColumnInGroup(propertyId)) { + return true; + } + } + return false; + } + + /** + * Is the header visible for the row. + * + * @return true if header is visible + */ + public boolean isHeaderVisible() { + return state.headerVisible; + } + + /** + * Sets the header visible for the row. + * + * @param visible + * should the header be shown + */ + public void setHeaderVisible(boolean visible) { + state.headerVisible = visible; + grid.markAsDirty(); + } + + /** + * Is the footer visible for the row. + * + * @return true if footer is visible + */ + public boolean isFooterVisible() { + return state.footerVisible; + } + + /** + * Sets the footer visible for the row. + * + * @param visible + * should the footer be shown + */ + public void setFooterVisible(boolean visible) { + state.footerVisible = visible; + grid.markAsDirty(); + } + +} diff --git a/server/src/com/vaadin/ui/components/grid/Grid.java b/server/src/com/vaadin/ui/components/grid/Grid.java index 25ac796d47..2b19043d93 100644 --- a/server/src/com/vaadin/ui/components/grid/Grid.java +++ b/server/src/com/vaadin/ui/components/grid/Grid.java @@ -16,7 +16,9 @@ package com.vaadin.ui.components.grid; +import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; @@ -28,6 +30,7 @@ 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.ColumnGroupRowState; import com.vaadin.shared.ui.grid.GridColumnState; import com.vaadin.shared.ui.grid.GridState; import com.vaadin.ui.AbstractComponent; @@ -52,18 +55,26 @@ import com.vaadin.ui.AbstractComponent; */ public class Grid extends AbstractComponent { + /** + * The data source attached to the grid + */ private Container.Indexed datasource; /** - * Property id -> Column instance mapping + * Property id to column instance mapping */ private final Map columns = new HashMap(); /** - * Key generator for column server->client communication + * Key generator for column server-to-client communication */ private final KeyMapper columnKeys = new KeyMapper(); + /** + * The column groups added to the grid + */ + private final List columnGroupRows = new ArrayList(); + /** * Property listener for listening to changes in data source properties. */ @@ -144,11 +155,11 @@ public class Grid extends AbstractComponent { if (!columns.containsKey(propertyId)) { GridColumn column = appendColumn(propertyId); - // By default use property id as column caption + // Add by default property id as column header column.setHeaderCaption(String.valueOf(propertyId)); - } } + } /** @@ -177,27 +188,27 @@ public class Grid extends AbstractComponent { * @param visible * true if the header rows should be visible */ - public void setHeaderVisible(boolean visible) { - getState().headerVisible = visible; + public void setColumnHeadersVisible(boolean visible) { + getState().columnHeadersVisible = visible; } /** * Are the header rows visible? * - * @return true if the header is visible + * @return true if the headers of the columns are visible */ - public boolean isHeaderVisible() { - return getState(false).headerVisible; + public boolean isColumnHeadersVisible() { + return getState(false).columnHeadersVisible; } /** * Sets the footer rows visible. * * @param visible - * true if the header rows should be visible + * true if the footer rows should be visible */ - public void setFooterVisible(boolean visible) { - getState().footerVisible = visible; + public void setColumnFootersVisible(boolean visible) { + getState().columnFootersVisible = visible; } /** @@ -205,25 +216,110 @@ public class Grid extends AbstractComponent { * * @return true if the footer rows should be visible */ - public boolean isFooterVisible() { - return getState(false).footerVisible; + public boolean isColumnFootersVisible() { + return getState(false).columnFootersVisible; + } + + /** + *

+ * Adds a new column group to the grid. + * + *

+ * Column group rows are rendered in the header and footer of the grid. + * Column group rows are made up of column groups which groups together + * columns for adding a common auxiliary header or footer for the columns. + *

+ *

+ * + *

+ * Example usage: + * + *

+     * // Add a new column group row to the grid
+     * ColumnGroupRow row = grid.addColumnGroupRow();
+     * 
+     * // Group "Column1" and "Column2" together to form a header in the row
+     * ColumnGroup column12 = row.addGroup("Column1", "Column2");
+     * 
+     * // Set a common header for "Column1" and "Column2"
+     * column12.setHeader("Column 1&2");
+     * 
+ * + *

+ * + * @return a column group instance you can use to add column groups + */ + public ColumnGroupRow addColumnGroupRow() { + ColumnGroupRowState state = new ColumnGroupRowState(); + ColumnGroupRow row = new ColumnGroupRow(this, state, columnKeys); + columnGroupRows.add(row); + getState().columnGroupRows.add(state); + return row; + } + + /** + * Adds a new column group to the grid at a specific index + * + * @param rowIndex + * the index of the row + * @return a column group instance you can use to add column groups + */ + public ColumnGroupRow addColumnGroupRow(int rowIndex) { + ColumnGroupRowState state = new ColumnGroupRowState(); + ColumnGroupRow row = new ColumnGroupRow(this, state, columnKeys); + columnGroupRows.add(rowIndex, row); + getState().columnGroupRows.add(rowIndex, state); + return row; + } + + /** + * Removes a column group. + * + * @param row + * the row to remove + */ + public void removeColumnGroupRow(ColumnGroupRow row) { + columnGroupRows.remove(row); + getState().columnGroupRows.remove(row.getState()); + } + + /** + * Gets the column group rows. + * + * @return an unmodifiable list of column group rows + */ + public List getColumnGroupRows() { + return Collections.unmodifiableList(new ArrayList( + columnGroupRows)); } /** * 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} + * verify if it has been detached from the {@link Grid}. * * @param columnId - * The client id generated for the column when the column is + * 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 + * @return the column with the id or null if not found */ GridColumn getColumnByColumnId(String columnId) { - Object propertyId = columnKeys.get(columnId); + Object propertyId = getPropertyIdByColumnId(columnId); return getColumn(propertyId); } + /** + * Used internally by the {@link Grid} to get a property id by referencing + * the columns generated state id. + * + * @param columnId + * The state id of the column + * @return The column instance or null if not found + */ + Object getPropertyIdByColumnId(String columnId) { + return columnKeys.get(columnId); + } + @Override protected GridState getState() { return (GridState) super.getState(); @@ -241,7 +337,7 @@ public class Grid extends AbstractComponent { * @param datasourcePropertyId * The property id of a property in the datasource */ - protected GridColumn appendColumn(Object datasourcePropertyId) { + private GridColumn appendColumn(Object datasourcePropertyId) { if (datasourcePropertyId == null) { throw new IllegalArgumentException("Property id cannot be null"); } diff --git a/server/src/com/vaadin/ui/components/grid/GridColumn.java b/server/src/com/vaadin/ui/components/grid/GridColumn.java index 505919b3cf..dde0669238 100644 --- a/server/src/com/vaadin/ui/components/grid/GridColumn.java +++ b/server/src/com/vaadin/ui/components/grid/GridColumn.java @@ -16,6 +16,8 @@ package com.vaadin.ui.components.grid; +import java.io.Serializable; + import com.vaadin.shared.ui.grid.GridColumnState; /** @@ -25,10 +27,10 @@ import com.vaadin.shared.ui.grid.GridColumnState; * @since 7.2 * @author Vaadin Ltd */ -public class GridColumn { +public class GridColumn implements Serializable { /** - * The shared state of the column + * The state of the column shared to the client */ private final GridColumnState state; @@ -138,9 +140,16 @@ public class GridColumn { * the new pixel width of the column * @throws IllegalStateException * if the column is no longer attached to any grid + * @throws IllegalArgumentException + * thrown if pixel width is less than zero */ - public void setWidth(int pixelWidth) throws IllegalStateException { + public void setWidth(int pixelWidth) throws IllegalStateException, + IllegalArgumentException { checkColumnIsAttached(); + if (pixelWidth < 0) { + throw new IllegalArgumentException( + "Pixel width should be greated than 0"); + } state.width = pixelWidth; grid.markAsDirty(); } 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 index 5989d537b4..85864160a8 100644 --- a/server/tests/src/com/vaadin/tests/server/component/grid/GridColumns.java +++ b/server/tests/src/com/vaadin/tests/server/component/grid/GridColumns.java @@ -32,6 +32,8 @@ 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.ColumnGroup; +import com.vaadin.ui.components.grid.ColumnGroupRow; import com.vaadin.ui.components.grid.Grid; import com.vaadin.ui.components.grid.GridColumn; @@ -110,9 +112,15 @@ public class GridColumns { assertEquals(100, column.getWidth()); assertEquals(column.getWidth(), getColumnState("column1").width); - column.setWidth(-1); - assertEquals(-1, column.getWidth()); - assertEquals(-1, getColumnState("column1").width); + try { + column.setWidth(-1); + fail("Setting width to -1 should throw exception"); + } catch (IllegalArgumentException iae) { + + } + + assertEquals(100, column.getWidth()); + assertEquals(100, getColumnState("column1").width); } @Test @@ -126,6 +134,7 @@ public class GridColumns { try { column.setHeaderCaption("asd"); + fail("Succeeded in modifying a detached column"); } catch (IllegalStateException ise) { // Detached state should throw exception @@ -157,7 +166,7 @@ public class GridColumns { } @Test - public void testAddingColumn() { + public void testAddingColumn() throws Exception { grid.getContainerDatasource().addContainerProperty("columnX", String.class, ""); GridColumn column = grid.getColumn("columnX"); @@ -165,33 +174,72 @@ public class GridColumns { } @Test - public void testHeaderVisiblility() { + public void testHeaderVisiblility() throws Exception { - assertTrue(grid.isHeaderVisible()); - assertTrue(state.headerVisible); + assertTrue(grid.isColumnHeadersVisible()); + assertTrue(state.columnHeadersVisible); - grid.setHeaderVisible(false); - assertFalse(grid.isHeaderVisible()); - assertFalse(state.headerVisible); + grid.setColumnHeadersVisible(false); + assertFalse(grid.isColumnHeadersVisible()); + assertFalse(state.columnHeadersVisible); - grid.setHeaderVisible(true); - assertTrue(grid.isHeaderVisible()); - assertTrue(state.headerVisible); + grid.setColumnHeadersVisible(true); + assertTrue(grid.isColumnHeadersVisible()); + assertTrue(state.columnHeadersVisible); } @Test - public void testFooterVisibility() { + public void testFooterVisibility() throws Exception { + + assertFalse(grid.isColumnFootersVisible()); + assertFalse(state.columnFootersVisible); - assertTrue(grid.isFooterVisible()); - assertTrue(state.footerVisible); + grid.setColumnFootersVisible(false); + assertFalse(grid.isColumnFootersVisible()); + assertFalse(state.columnFootersVisible); - grid.setFooterVisible(false); - assertFalse(grid.isFooterVisible()); - assertFalse(state.footerVisible); + grid.setColumnFootersVisible(true); + assertTrue(grid.isColumnFootersVisible()); + assertTrue(state.columnFootersVisible); + } - grid.setFooterVisible(true); - assertTrue(grid.isFooterVisible()); - assertTrue(state.footerVisible); + @Test + public void testColumnGroups() throws Exception { + + // Add a new row + ColumnGroupRow row = grid.addColumnGroupRow(); + assertTrue(state.columnGroupRows.size() == 1); + + // Add a group by property id + ColumnGroup columns12 = row.addGroup("column1", "column2"); + assertTrue(state.columnGroupRows.get(0).groups.size() == 1); + + // Set header of column + columns12.setHeaderCaption("Column12"); + assertEquals("Column12", + state.columnGroupRows.get(0).groups.get(0).header); + + // Set footer of column + columns12.setFooterCaption("Footer12"); + assertEquals("Footer12", + state.columnGroupRows.get(0).groups.get(0).footer); + + // Add another group by column instance + ColumnGroup columns34 = row.addGroup(grid.getColumn("column3"), + grid.getColumn("column4")); + assertTrue(state.columnGroupRows.get(0).groups.size() == 2); + + // add another group row + ColumnGroupRow row2 = grid.addColumnGroupRow(); + assertTrue(state.columnGroupRows.size() == 2); + + // add a group by combining the two previous groups + ColumnGroup columns1234 = row2.addGroup(columns12, columns34); + assertTrue(columns1234.getColumns().size() == 4); + + // Insert a group as the second group + ColumnGroupRow newRow2 = grid.addColumnGroupRow(1); + assertTrue(state.columnGroupRows.size() == 3); } private GridColumnState getColumnState(Object propertyId) { diff --git a/shared/src/com/vaadin/shared/ui/grid/ColumnGroupRowState.java b/shared/src/com/vaadin/shared/ui/grid/ColumnGroupRowState.java new file mode 100644 index 0000000000..a8e0f87457 --- /dev/null +++ b/shared/src/com/vaadin/shared/ui/grid/ColumnGroupRowState.java @@ -0,0 +1,46 @@ +/* + * 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; +import java.util.ArrayList; +import java.util.List; + +/** + * The column group row data shared between the server and client + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class ColumnGroupRowState implements Serializable { + + /** + * The groups that has been added to the row + */ + public List groups = new ArrayList(); + + /** + * Is the header shown + */ + public boolean headerVisible = true; + + /** + * Is the footer shown + */ + public boolean footerVisible = false; + +} diff --git a/shared/src/com/vaadin/shared/ui/grid/ColumnGroupState.java b/shared/src/com/vaadin/shared/ui/grid/ColumnGroupState.java new file mode 100644 index 0000000000..3992b6611f --- /dev/null +++ b/shared/src/com/vaadin/shared/ui/grid/ColumnGroupState.java @@ -0,0 +1,45 @@ +/* + * 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; +import java.util.ArrayList; +import java.util.List; + +/** + * The column group data shared between the server and the client + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class ColumnGroupState implements Serializable { + + /** + * The columns that is included in the group + */ + public List columns = new ArrayList(); + + /** + * The header text of the group + */ + public String header; + + /** + * The footer text of the group + */ + public String footer; +} diff --git a/shared/src/com/vaadin/shared/ui/grid/GridColumnState.java b/shared/src/com/vaadin/shared/ui/grid/GridColumnState.java index 391eb2a65c..0301c5ead2 100644 --- a/shared/src/com/vaadin/shared/ui/grid/GridColumnState.java +++ b/shared/src/com/vaadin/shared/ui/grid/GridColumnState.java @@ -34,17 +34,11 @@ public class GridColumnState implements Serializable { /** * 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; diff --git a/shared/src/com/vaadin/shared/ui/grid/GridState.java b/shared/src/com/vaadin/shared/ui/grid/GridState.java index e1e0fff354..d1167f3d4f 100644 --- a/shared/src/com/vaadin/shared/ui/grid/GridState.java +++ b/shared/src/com/vaadin/shared/ui/grid/GridState.java @@ -40,13 +40,17 @@ public class GridState extends AbstractComponentState { public List columns = new ArrayList(); /** - * Are the header row(s) visible. By default they are visible. + * Is the column header row visible */ - public boolean headerVisible = true; + public boolean columnHeadersVisible = true; /** - * Are the footer row(s) visible. By default they are visible. + * Is the column footer row visible */ - public boolean footerVisible = true; + public boolean columnFootersVisible = false; + /** + * The column groups added to the grid + */ + public List columnGroupRows = new ArrayList(); } diff --git a/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java b/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java index 5b3d742f19..bd3e96f84a 100644 --- a/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java +++ b/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java @@ -19,6 +19,8 @@ import java.util.ArrayList; import com.vaadin.data.util.IndexedContainer; import com.vaadin.tests.components.AbstractComponentTest; +import com.vaadin.ui.components.grid.ColumnGroup; +import com.vaadin.ui.components.grid.ColumnGroupRow; import com.vaadin.ui.components.grid.Grid; import com.vaadin.ui.components.grid.GridColumn; @@ -32,6 +34,8 @@ public class GridBasicFeatures extends AbstractComponentTest { private final int COLUMNS = 10; + private int columnGroupRows = 0; + @Override protected Grid constructComponent() { @@ -42,20 +46,51 @@ public class GridBasicFeatures extends AbstractComponentTest { ds.addContainerProperty("Column" + col, String.class, ""); } + // Create grid Grid grid = new Grid(ds); - // Headers and footers + // Add footer values (header values are automatically created) for (int col = 0; col < COLUMNS; col++) { - GridColumn column = grid.getColumn("Column" + col); - column.setHeaderCaption("Column " + col); - column.setFooterCaption("Footer " + col); + grid.getColumn("Column" + col).setFooterCaption("Footer " + col); } createColumnActions(); + createHeaderActions(); + + createFooterActions(); + + createColumnGroupActions(); + return grid; } + protected void createHeaderActions() { + createCategory("Headers", null); + + createBooleanAction("Visible", "Headers", true, + new Command() { + + @Override + public void execute(Grid grid, Boolean value, Object data) { + grid.setColumnHeadersVisible(value); + } + }); + } + + protected void createFooterActions() { + createCategory("Footers", null); + + createBooleanAction("Visible", "Footers", false, + new Command() { + + @Override + public void execute(Grid grid, Boolean value, Object data) { + grid.setColumnFootersVisible(value); + } + }); + } + protected void createColumnActions() { createCategory("Columns", null); @@ -77,46 +112,6 @@ public class GridBasicFeatures extends AbstractComponentTest { } }, 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() { @@ -131,6 +126,72 @@ public class GridBasicFeatures extends AbstractComponentTest { } + protected void createColumnGroupActions() { + createCategory("Column groups", null); + + createClickAction("Add group row", "Column groups", + new Command() { + + @Override + public void execute(Grid grid, String value, Object data) { + final ColumnGroupRow row = grid.addColumnGroupRow(); + columnGroupRows++; + createCategory("Column group row " + columnGroupRows, + "Column groups"); + + createBooleanAction("Header Visible", + "Column group row " + columnGroupRows, true, + new Command() { + + @Override + public void execute(Grid grid, + Boolean value, Object columnIndex) { + row.setHeaderVisible(value); + } + }, row); + + createBooleanAction("Footer Visible", + "Column group row " + columnGroupRows, false, + new Command() { + + @Override + public void execute(Grid grid, + Boolean value, Object columnIndex) { + row.setFooterVisible(value); + } + }, row); + + for (int i = 0; i < COLUMNS; i += 2) { + final int columnIndex = i; + createClickAction("Group Column " + columnIndex + + " & " + (columnIndex + 1), + "Column group row " + columnGroupRows, + new Command() { + + @Override + public void execute(Grid c, + Integer value, Object data) { + final ColumnGroup group = row + .addGroup( + "Column" + value, + "Column" + + (value + 1)); + + group.setHeaderCaption("Column " + + value + " & " + + (value + 1)); + + group.setFooterCaption("Column " + + value + " & " + + (value + 1)); + } + }, i, row); + } + } + }, null, null); + + } + @Override protected Integer getTicketNumber() { return 12829; diff --git a/uitest/src/com/vaadin/tests/components/grid/GridColumnGroups.java b/uitest/src/com/vaadin/tests/components/grid/GridColumnGroups.java new file mode 100644 index 0000000000..66e7651f76 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/GridColumnGroups.java @@ -0,0 +1,111 @@ +/* + * 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 com.vaadin.data.util.IndexedContainer; +import com.vaadin.server.VaadinRequest; +import com.vaadin.tests.components.AbstractTestUI; +import com.vaadin.ui.components.grid.ColumnGroup; +import com.vaadin.ui.components.grid.ColumnGroupRow; +import com.vaadin.ui.components.grid.Grid; +import com.vaadin.ui.components.grid.GridColumn; + +/** + * + * @since + * @author Vaadin Ltd + */ +public class GridColumnGroups extends AbstractTestUI { + + private final int COLUMNS = 4; + + @Override + protected void setup(VaadinRequest request) { + + // Setup grid + IndexedContainer ds = new IndexedContainer(); + for (int col = 0; col < COLUMNS; col++) { + ds.addContainerProperty("Column" + col, String.class, ""); + } + Grid grid = new Grid(ds); + addComponent(grid); + + /*- + * --------------------------------------------- + * | Header 1 | <- Auxiliary row 2 + * |-------------------------------------------| + * | Header 2 | Header 3 | <- Auxiliary row 1 + * |-------------------------------------------| + * | Column 1 | Column 2 | Column 3 | Column 4 | <- Column headers + * --------------------------------------------| + * | ... | ... | ... | ... | + * | ... | ... | ... | ... | + * --------------------------------------------| + * | Column 1 | Column 2 | Column 3 | Column 4 | <- Column footers + * --------------------------------------------| + * | Footer 2 | Footer 3 | <- Auxiliary row 1 + * --------------------------------------------| + * | Footer 1 | <- Auxiliary row 2 + * --------------------------------------------- + -*/ + + // Set column footers (headers are generated automatically) + grid.setColumnFootersVisible(true); + for (Object propertyId : ds.getContainerPropertyIds()) { + GridColumn column = grid.getColumn(propertyId); + column.setFooterCaption(String.valueOf(propertyId)); + } + + // First auxiliary row + ColumnGroupRow auxRow1 = grid.addColumnGroupRow(); + + // Using property id to create a column group + ColumnGroup columns12 = auxRow1.addGroup("Column0", "Column1"); + columns12.setHeaderCaption("Header 2"); + columns12.setFooterCaption("Footer 2"); + + // Using grid columns to create a column group + GridColumn column3 = grid.getColumn("Column2"); + GridColumn column4 = grid.getColumn("Column3"); + ColumnGroup columns34 = auxRow1.addGroup(column3, column4); + columns34.setHeaderCaption("Header 3"); + columns34.setFooterCaption("Footer 3"); + + // Second auxiliary row + ColumnGroupRow auxRow2 = grid.addColumnGroupRow(); + + // Using previous groups to create a column group + ColumnGroup columns1234 = auxRow2.addGroup(columns12, columns34); + columns1234.setHeaderCaption("Header 1"); + columns1234.setFooterCaption("Footer 1"); + + } + + @Override + protected String getTestDescription() { + return "Grid should support headers and footer groups"; + } + + @Override + protected Integer getTicketNumber() { + return 12894; + } + +} -- 2.39.5