From 1c1506ef0447b1d979a6adb4d812ae9858f00b67 Mon Sep 17 00:00:00 2001 From: John Ahlroos Date: Tue, 29 Oct 2013 13:44:49 +0200 Subject: Base grid component and column API (#12829, #12830) Change-Id: I6c4eae8a4369e9452dd56e764633cecfe9bf553a --- .../com/vaadin/shared/ui/grid/GridColumnState.java | 61 ++++++++++++++++++++++ .../src/com/vaadin/shared/ui/grid/GridState.java | 52 ++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 shared/src/com/vaadin/shared/ui/grid/GridColumnState.java create mode 100644 shared/src/com/vaadin/shared/ui/grid/GridState.java (limited to 'shared') diff --git a/shared/src/com/vaadin/shared/ui/grid/GridColumnState.java b/shared/src/com/vaadin/shared/ui/grid/GridColumnState.java new file mode 100644 index 0000000000..391eb2a65c --- /dev/null +++ b/shared/src/com/vaadin/shared/ui/grid/GridColumnState.java @@ -0,0 +1,61 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.shared.ui.grid; + +import java.io.Serializable; + +/** + * Column state DTO for transferring column properties from the server to the + * client + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class GridColumnState implements Serializable { + + /** + * Id used by grid connector to map server side column with client side + * column + */ + public String id; + + /** + * Header caption for the column + * + * FIXME Only single header currently supported. Should support many + * headers. + */ + public String header; + + /** + * Footer caption for the column + * + * FIXME Only single footer currently supported. Should support many + * footers. + */ + public String footer; + + /** + * Has the column been hidden. By default the column is visible. + */ + public boolean visible = true; + + /** + * Column width in pixels. Default column width is 100px. + */ + public int width = 100; + +} diff --git a/shared/src/com/vaadin/shared/ui/grid/GridState.java b/shared/src/com/vaadin/shared/ui/grid/GridState.java new file mode 100644 index 0000000000..e1e0fff354 --- /dev/null +++ b/shared/src/com/vaadin/shared/ui/grid/GridState.java @@ -0,0 +1,52 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.shared.ui.grid; + +import java.util.ArrayList; +import java.util.List; + +import com.vaadin.shared.AbstractComponentState; + +/** + * The shared state for the {@link com.vaadin.ui.components.grid.Grid} component + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class GridState extends AbstractComponentState { + { + // FIXME Grid currently does not support undefined size + width = "400px"; + height = "400px"; + } + + /** + * Columns in grid. Column order implicitly deferred from list order. + */ + public List columns = new ArrayList(); + + /** + * Are the header row(s) visible. By default they are visible. + */ + public boolean headerVisible = true; + + /** + * Are the footer row(s) visible. By default they are visible. + */ + public boolean footerVisible = true; + +} -- cgit v1.2.3 From 4caa2f5b6e26ade52a4fba66a0a020b79f9008ea Mon Sep 17 00:00:00 2001 From: John Ahlroos Date: Wed, 6 Nov 2013 10:35:03 +0200 Subject: Multiple headers and footer rows #3153 Change-Id: Iadb0d8b051d0f0ef1303e0d7d740cf476cd81971 --- .../src/com/vaadin/client/ui/grid/ColumnGroup.java | 117 +++++ .../com/vaadin/client/ui/grid/ColumnGroupRow.java | 188 ++++++++ client/src/com/vaadin/client/ui/grid/Grid.java | 482 +++++++++++++++++---- .../com/vaadin/client/ui/grid/GridConnector.java | 92 +++- .../com/vaadin/ui/components/grid/ColumnGroup.java | 141 ++++++ .../vaadin/ui/components/grid/ColumnGroupRow.java | 255 +++++++++++ server/src/com/vaadin/ui/components/grid/Grid.java | 134 +++++- .../com/vaadin/ui/components/grid/GridColumn.java | 15 +- .../tests/server/component/grid/GridColumns.java | 92 +++- .../vaadin/shared/ui/grid/ColumnGroupRowState.java | 46 ++ .../vaadin/shared/ui/grid/ColumnGroupState.java | 45 ++ .../com/vaadin/shared/ui/grid/GridColumnState.java | 6 - .../src/com/vaadin/shared/ui/grid/GridState.java | 12 +- .../tests/components/grid/GridBasicFeatures.java | 149 +++++-- .../tests/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 (limited to 'shared') 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; + } + +} -- cgit v1.2.3 From d54b02e31dad3e0b0a60e02750efb6bc268e3974 Mon Sep 17 00:00:00 2001 From: Leif Åstrand Date: Fri, 22 Nov 2013 15:40:34 +0200 Subject: Introduce initial data source support for Grid (#12878) Change-Id: I2d1b2e4a797b2dac9ee97c832fcd40fb472edc08 --- .../client/data/AbstractRemoteDataSource.java | 232 +++++++++++++++++++++ .../com/vaadin/client/data/DataChangeHandler.java | 59 ++++++ client/src/com/vaadin/client/data/DataSource.java | 76 +++++++ .../vaadin/client/data/RpcDataSourceConnector.java | 71 +++++++ .../src/com/vaadin/client/ui/grid/Escalator.java | 37 ++++ client/src/com/vaadin/client/ui/grid/Grid.java | 89 +++++++- .../com/vaadin/client/ui/grid/GridConnector.java | 11 +- client/src/com/vaadin/client/ui/grid/Range.java | 22 ++ .../client/ui/grid/RowVisibilityChangeEvent.java | 90 ++++++++ .../client/ui/grid/RowVisibilityChangeHandler.java | 38 ++++ .../src/com/vaadin/client/ui/grid/RangeTest.java | 76 +++++++ .../com/vaadin/data/RpcDataProviderExtension.java | 101 +++++++++ server/src/com/vaadin/ui/components/grid/Grid.java | 9 + .../com/vaadin/shared/data/DataProviderRpc.java | 40 ++++ .../com/vaadin/shared/data/DataProviderState.java | 32 +++ .../src/com/vaadin/shared/data/DataRequestRpc.java | 38 ++++ .../tests/components/grid/GridBasicFeatures.java | 11 + 17 files changed, 1026 insertions(+), 6 deletions(-) create mode 100644 client/src/com/vaadin/client/data/AbstractRemoteDataSource.java create mode 100644 client/src/com/vaadin/client/data/DataChangeHandler.java create mode 100644 client/src/com/vaadin/client/data/DataSource.java create mode 100644 client/src/com/vaadin/client/data/RpcDataSourceConnector.java create mode 100644 client/src/com/vaadin/client/ui/grid/RowVisibilityChangeEvent.java create mode 100644 client/src/com/vaadin/client/ui/grid/RowVisibilityChangeHandler.java create mode 100644 server/src/com/vaadin/data/RpcDataProviderExtension.java create mode 100644 shared/src/com/vaadin/shared/data/DataProviderRpc.java create mode 100644 shared/src/com/vaadin/shared/data/DataProviderState.java create mode 100644 shared/src/com/vaadin/shared/data/DataRequestRpc.java (limited to 'shared') diff --git a/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java b/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java new file mode 100644 index 0000000000..5790adada9 --- /dev/null +++ b/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java @@ -0,0 +1,232 @@ +/* + * 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.data; + +import java.util.HashMap; +import java.util.List; + +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.core.client.Scheduler.ScheduledCommand; +import com.vaadin.client.Profiler; +import com.vaadin.client.ui.grid.Range; + +/** + * Base implementation for data sources that fetch data from a remote system. + * This class takes care of caching data and communicating with the data source + * user. An implementation of this class should override + * {@link #requestRows(int, int)} to trigger asynchronously loading of data. + * When data is received from the server, new row data should be passed to + * {@link #setRowData(int, List)}. {@link #setEstimatedSize(int)} should be used + * based on estimations of how many rows are available. + * + * @since 7.2 + * @author Vaadin Ltd + * @param + * the row type + */ +public abstract class AbstractRemoteDataSource implements DataSource { + + private boolean requestPending = false; + + private boolean coverageCheckPending = false; + + private Range requestedAvailability = Range.between(0, 0); + + private Range cached = Range.between(0, 0); + + private final HashMap rowCache = new HashMap(); + + private DataChangeHandler dataChangeHandler; + + private int estimatedSize; + + private final ScheduledCommand coverageChecker = new ScheduledCommand() { + @Override + public void execute() { + coverageCheckPending = false; + checkCacheCoverage(); + } + }; + + /** + * Sets the estimated number of rows in the data source. + * + * @param estimatedSize + * the estimated number of available rows + */ + protected void setEstimatedSize(int estimatedSize) { + // TODO update dataChangeHandler if size changes + this.estimatedSize = estimatedSize; + } + + private void ensureCoverageCheck() { + if (!coverageCheckPending) { + coverageCheckPending = true; + Scheduler.get().scheduleDeferred(coverageChecker); + } + } + + @Override + public void ensureAvailability(int firstRowIndex, int numberOfRows) { + requestedAvailability = Range.withLength(firstRowIndex, numberOfRows); + + /* + * Don't request any data right away since the data might be included in + * a message that has been received but not yet fully processed. + */ + ensureCoverageCheck(); + } + + private void checkCacheCoverage() { + if (requestPending) { + // Anyone clearing requestPending should run this method again + return; + } + + Profiler.enter("AbstractRemoteDataSource.checkCacheCoverage"); + + if (!requestedAvailability.intersects(cached)) { + /* + * Simple case: no overlap between cached data and needed data. + * Clear the cache and request new data + */ + rowCache.clear(); + cached = Range.between(0, 0); + + handleMissingRows(requestedAvailability); + } else { + discardStaleCacheEntries(); + + // Might need more rows -> request them + Range[] availabilityPartition = requestedAvailability + .partitionWith(cached); + handleMissingRows(availabilityPartition[0]); + handleMissingRows(availabilityPartition[2]); + } + + Profiler.leave("AbstractRemoteDataSource.checkCacheCoverage"); + } + + private void discardStaleCacheEntries() { + Range[] cacheParition = cached.partitionWith(requestedAvailability); + dropFromCache(cacheParition[0]); + cached = cacheParition[1]; + dropFromCache(cacheParition[2]); + } + + private void dropFromCache(Range range) { + for (int i = range.getStart(); i < range.getEnd(); i++) { + rowCache.remove(Integer.valueOf(i)); + } + } + + private void handleMissingRows(Range range) { + if (range.isEmpty()) { + return; + } + requestPending = true; + requestRows(range.getStart(), range.length()); + } + + /** + * Triggers fetching rows from the remote data source. + * {@link #setRowData(int, List)} should be invoked with data for the + * requested rows when they have been received. + * + * @param firstRowIndex + * the index of the first row to fetch + * @param numberOfRows + * the number of rows to fetch + */ + protected abstract void requestRows(int firstRowIndex, int numberOfRows); + + @Override + public int getEstimatedSize() { + return estimatedSize; + } + + @Override + public T getRow(int rowIndex) { + return rowCache.get(Integer.valueOf(rowIndex)); + } + + @Override + public void setDataChangeHandler(DataChangeHandler dataChangeHandler) { + this.dataChangeHandler = dataChangeHandler; + + if (dataChangeHandler != null && !cached.isEmpty()) { + // Push currently cached data to the implementation + dataChangeHandler.dataUpdated(cached.getStart(), cached.length()); + } + } + + /** + * Informs this data source that updated data has been sent from the server. + * + * @param firstRowIndex + * the index of the first received row + * @param rowData + * a list of rows, starting from firstRowIndex + */ + protected void setRowData(int firstRowIndex, List rowData) { + requestPending = false; + + Profiler.enter("AbstractRemoteDataSource.setRowData"); + + Range received = Range.withLength(firstRowIndex, rowData.size()); + + Range[] partition = received.partitionWith(requestedAvailability); + + Range newUsefulData = partition[1]; + if (!newUsefulData.isEmpty()) { + // Update the parts that are actually inside + for (int i = newUsefulData.getStart(); i < newUsefulData.getEnd(); i++) { + rowCache.put(Integer.valueOf(i), rowData.get(i - firstRowIndex)); + } + + if (dataChangeHandler != null) { + Profiler.enter("AbstractRemoteDataSource.setRowData notify dataChangeHandler"); + dataChangeHandler.dataUpdated(newUsefulData.getStart(), + newUsefulData.length()); + Profiler.leave("AbstractRemoteDataSource.setRowData notify dataChangeHandler"); + } + + // Potentially extend the range + if (cached.isEmpty()) { + cached = newUsefulData; + } else { + discardStaleCacheEntries(); + cached = cached.combineWith(newUsefulData); + } + } + + if (!partition[0].isEmpty() || !partition[2].isEmpty()) { + /* + * FIXME + * + * Got data that we might need in a moment if the container is + * updated before the widget settings. Support for this will be + * implemented later on. + */ + } + + // Eventually check whether all needed rows are now available + ensureCoverageCheck(); + + Profiler.leave("AbstractRemoteDataSource.setRowData"); + } +} diff --git a/client/src/com/vaadin/client/data/DataChangeHandler.java b/client/src/com/vaadin/client/data/DataChangeHandler.java new file mode 100644 index 0000000000..4c4cc7656d --- /dev/null +++ b/client/src/com/vaadin/client/data/DataChangeHandler.java @@ -0,0 +1,59 @@ +/* + * 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.data; + +/** + * Callback interface used by {@link DataSource} to inform its user about + * updates to the data. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public interface DataChangeHandler { + /** + * Called when the contents of the data source has changed. If the number of + * rows has changed or if rows have been moved around, + * {@link #dataAdded(int, int)} or {@link #dataRemoved(int, int)} should + * ideally be used instead. + * + * @param firstRowIndex + * the index of the first changed row + * @param numberOfRows + * the number of changed rows + */ + public void dataUpdated(int firstRowIndex, int numberOfRows); + + /** + * Called when rows have been removed from the data source. + * + * @param firstRowIndex + * the index that the first removed row had prior to removal + * @param numberOfRows + * the number of removed rows + */ + public void dataRemoved(int firstRowIndex, int numberOfRows); + + /** + * Called when the new rows have been added to the container. + * + * @param firstRowIndex + * the index of the first added row + * @param numberOfRows + * the number of added rows + */ + public void dataAdded(int firstRowIndex, int numberOfRows); +} diff --git a/client/src/com/vaadin/client/data/DataSource.java b/client/src/com/vaadin/client/data/DataSource.java new file mode 100644 index 0000000000..9179b6d03d --- /dev/null +++ b/client/src/com/vaadin/client/data/DataSource.java @@ -0,0 +1,76 @@ +/* + * 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.data; + +/** + * Source of data for widgets showing lazily loaded data based on indexable + * items (e.g. rows) of a specified type. The data source is a lazy view into a + * larger data set. + * + * @since 7.2 + * @author Vaadin Ltd + * @param + * the row type + */ +public interface DataSource { + /** + * Informs the data source that data for the given range is needed. A data + * source only has one active region at a time, so calling this method + * discards the previously set range. + *

+ * This method triggers lazy loading of data if necessary. The change + * handler registered using {@link #setDataChangeHandler(DataChangeHandler)} + * is informed when new data has been loaded. + * + * @param firstRowIndex + * the index of the first needed row + * @param numberOfRows + * the number of needed rows + */ + public void ensureAvailability(int firstRowIndex, int numberOfRows); + + /** + * Retrieves the data for the row at the given index. If the row data is not + * available, returns null. + *

+ * This method does not trigger loading of unavailable data. + * {@link #ensureAvailability(int, int)} should be used to signal what data + * will be needed. + * + * @param rowIndex + * the index of the row to retrieve data for + * @return data for the row; or null if no data is available + */ + public T getRow(int rowIndex); + + /** + * Returns the current best guess for the number of rows in the container. + * + * @return the current estimation of the container size + */ + public int getEstimatedSize(); + + /** + * Sets a data change handler to inform when data is updated, added or + * removed. + * + * @param dataChangeHandler + * the data change handler + */ + public void setDataChangeHandler(DataChangeHandler dataChangeHandler); + +} diff --git a/client/src/com/vaadin/client/data/RpcDataSourceConnector.java b/client/src/com/vaadin/client/data/RpcDataSourceConnector.java new file mode 100644 index 0000000000..1785fc62c2 --- /dev/null +++ b/client/src/com/vaadin/client/data/RpcDataSourceConnector.java @@ -0,0 +1,71 @@ +/* + * 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.data; + +import java.util.List; + +import com.vaadin.client.ServerConnector; +import com.vaadin.client.extensions.AbstractExtensionConnector; +import com.vaadin.client.ui.grid.GridConnector; +import com.vaadin.shared.data.DataProviderRpc; +import com.vaadin.shared.data.DataProviderState; +import com.vaadin.shared.data.DataRequestRpc; +import com.vaadin.shared.ui.Connect; + +/** + * Connects a Vaadin server-side container data source to a Grid. This is + * currently implemented as an Extension hardcoded to support a specific + * connector type. This will be changed once framework support for something + * more flexible has been implemented. + * + * @since 7.2 + * @author Vaadin Ltd + */ +@Connect(com.vaadin.data.RpcDataProviderExtension.class) +public class RpcDataSourceConnector extends AbstractExtensionConnector { + + private final AbstractRemoteDataSource dataSource = new AbstractRemoteDataSource() { + @Override + protected void requestRows(int firstRowIndex, int numberOfRows) { + getRpcProxy(DataRequestRpc.class).requestRows(firstRowIndex, + numberOfRows); + } + }; + + @Override + protected void extend(ServerConnector target) { + dataSource.setEstimatedSize(getState().containerSize); + ((GridConnector) target).getWidget().setDataSource(dataSource); + + registerRpc(DataProviderRpc.class, new DataProviderRpc() { + @Override + public void setRowData(int firstRow, List rows) { + dataSource.setRowData(firstRow, rows); + } + }); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.client.ui.AbstractConnector#getState() + */ + @Override + public DataProviderState getState() { + return (DataProviderState) super.getState(); + } +} diff --git a/client/src/com/vaadin/client/ui/grid/Escalator.java b/client/src/com/vaadin/client/ui/grid/Escalator.java index 769a109569..540676653e 100644 --- a/client/src/com/vaadin/client/ui/grid/Escalator.java +++ b/client/src/com/vaadin/client/ui/grid/Escalator.java @@ -39,6 +39,7 @@ import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Element; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.Widget; +import com.google.web.bindery.event.shared.HandlerRegistration; import com.vaadin.client.Profiler; import com.vaadin.client.Util; import com.vaadin.client.ui.grid.Escalator.JsniUtil.TouchHandlerBundle; @@ -1522,6 +1523,8 @@ public class Escalator extends Widget { moveAndUpdateEscalatorRows(strayRow, 0, topLogicalIndex); } } + + fireRowVisibilityChangeEvent(); } @Override @@ -1715,6 +1718,8 @@ public class Escalator extends Widget { newRowTop += ROW_HEIGHT_PX; } } + + fireRowVisibilityChangeEvent(); } /** @@ -2101,6 +2106,8 @@ public class Escalator extends Widget { * or it won't work correctly (due to setScrollTop invocation) */ scroller.recalculateScrollbarsForVirtualViewport(); + + fireRowVisibilityChangeEvent(); } private void paintRemoveRowsAtMiddle(final Range removedLogicalInside, @@ -2420,6 +2427,10 @@ public class Escalator extends Widget { } } + if (neededEscalatorRowsDiff != 0) { + fireRowVisibilityChangeEvent(); + } + Profiler.leave("Escalator.BodyRowContainer.verifyEscalatorCount"); } } @@ -3116,4 +3127,30 @@ public class Escalator extends Widget { return array; } + + /** + * Adds an event handler that gets notified when the range of visible rows + * changes e.g. because of scrolling. + * + * @param rowVisibilityChangeHadler + * the event handler + * @return a handler registration for the added handler + */ + public HandlerRegistration addRowVisibilityChangeHandler( + RowVisibilityChangeHandler rowVisibilityChangeHadler) { + return addHandler(rowVisibilityChangeHadler, + RowVisibilityChangeEvent.TYPE); + } + + private void fireRowVisibilityChangeEvent() { + int visibleRangeStart = body.getLogicalRowIndex(body.visualRowOrder + .getFirst()); + int visibleRangeEnd = body.getLogicalRowIndex(body.visualRowOrder + .getLast()) + 1; + + int visibleRowCount = visibleRangeEnd - visibleRangeStart; + + fireEvent(new RowVisibilityChangeEvent(visibleRangeStart, + visibleRowCount)); + } } diff --git a/client/src/com/vaadin/client/ui/grid/Grid.java b/client/src/com/vaadin/client/ui/grid/Grid.java index 67f14301f0..d76424ae31 100644 --- a/client/src/com/vaadin/client/ui/grid/Grid.java +++ b/client/src/com/vaadin/client/ui/grid/Grid.java @@ -21,6 +21,8 @@ import java.util.List; import com.google.gwt.core.shared.GWT; import com.google.gwt.user.client.ui.Composite; +import com.vaadin.client.data.DataChangeHandler; +import com.vaadin.client.data.DataSource; import com.vaadin.shared.util.SharedUtil; /** @@ -64,6 +66,8 @@ public class Grid extends Composite { */ private final List> columns = new ArrayList>(); + private DataSource dataSource; + /** * The column groups rows added to the grid */ @@ -366,6 +370,20 @@ public class Grid extends Composite { refreshHeader(); refreshFooter(); + + escalator + .addRowVisibilityChangeHandler(new RowVisibilityChangeHandler() { + @Override + public void onRowVisibilityChange( + RowVisibilityChangeEvent event) { + if (dataSource != null) { + dataSource.ensureAvailability( + event.getFirstVisibleRow(), + event.getVisibleRowCount()); + } + } + }); + } /** @@ -399,15 +417,33 @@ public class Grid extends Composite { }; } - // TODO Should be implemented by the data sources - @SuppressWarnings("static-method") private EscalatorUpdater createBodyUpdater() { return new EscalatorUpdater() { @Override public void updateCells(Row row, List cellsToUpdate) { + int rowIndex = row.getRow(); + if (dataSource == null) { + setCellsLoading(cellsToUpdate); + return; + } + + T rowData = dataSource.getRow(rowIndex); + if (rowData == null) { + setCellsLoading(cellsToUpdate); + return; + } + for (Cell cell : cellsToUpdate) { - cell.getElement().setInnerHTML("-"); + String value = getColumn(cell.getColumn()) + .getValue(rowData); + cell.getElement().setInnerText(value); + } + } + + private void setCellsLoading(List cellsToUpdate) { + for (Cell cell : cellsToUpdate) { + cell.getElement().setInnerText("..."); } } }; @@ -778,4 +814,51 @@ public class Grid extends Composite { public void setWidth(String width) { escalator.setWidth(width); } + + /** + * Sets the data source used by this grid. + * + * @param dataSource + * the data source to use, not null + * @throws IllegalArgumentException + * if dataSource is null + */ + public void setDataSource(DataSource dataSource) + throws IllegalArgumentException { + if (dataSource == null) { + throw new IllegalArgumentException("dataSource can't be null."); + } + + if (this.dataSource != null) { + this.dataSource.setDataChangeHandler(null); + } + + this.dataSource = dataSource; + dataSource.setDataChangeHandler(new DataChangeHandler() { + @Override + public void dataUpdated(int firstIndex, int numberOfItems) { + escalator.getBody().refreshRows(firstIndex, numberOfItems); + } + + @Override + public void dataRemoved(int firstIndex, int numberOfItems) { + escalator.getBody().removeRows(firstIndex, numberOfItems); + } + + @Override + public void dataAdded(int firstIndex, int numberOfItems) { + escalator.getBody().insertRows(firstIndex, numberOfItems); + } + }); + + int previousRowCount = escalator.getBody().getRowCount(); + if (previousRowCount != 0) { + escalator.getBody().removeRows(0, previousRowCount); + } + + int estimatedSize = dataSource.getEstimatedSize(); + if (estimatedSize > 0) { + escalator.getBody().insertRows(0, estimatedSize); + } + } } diff --git a/client/src/com/vaadin/client/ui/grid/GridConnector.java b/client/src/com/vaadin/client/ui/grid/GridConnector.java index 32907e1e29..896a9998fb 100644 --- a/client/src/com/vaadin/client/ui/grid/GridConnector.java +++ b/client/src/com/vaadin/client/ui/grid/GridConnector.java @@ -48,10 +48,15 @@ public class GridConnector extends AbstractComponentConnector { */ private class CustomGridColumn extends GridColumn { + private final int columnIndex; + + public CustomGridColumn(int columnIndex) { + this.columnIndex = columnIndex; + } + @Override public String getValue(String[] obj) { - // FIXME Should return something from the data source. - return null; + return obj[columnIndex]; } } @@ -142,7 +147,7 @@ public class GridConnector extends AbstractComponentConnector { */ private void addColumnFromStateChangeEvent(int columnIndex) { GridColumnState state = getState().columns.get(columnIndex); - CustomGridColumn column = new CustomGridColumn(); + CustomGridColumn column = new CustomGridColumn(columnIndex); updateColumnFromState(column, state); columnIdToColumn.put(state.id, column); diff --git a/client/src/com/vaadin/client/ui/grid/Range.java b/client/src/com/vaadin/client/ui/grid/Range.java index 6dbb287e57..d3ae3c3753 100644 --- a/client/src/com/vaadin/client/ui/grid/Range.java +++ b/client/src/com/vaadin/client/ui/grid/Range.java @@ -359,4 +359,26 @@ public final class Range { public Range[] splitAtFromStart(final int length) { return splitAt(getStart() + length); } + + /** + * Combines two ranges to create a range containing all values in both + * ranges, provided there are no gaps between the ranges. + * + * @param other + * the range to combine with this range + * + * @return the combined range + * + * @throws IllegalArgumentException + * if the two ranges aren't connected + */ + public Range combineWith(Range other) throws IllegalArgumentException { + if (getStart() > other.getEnd() || other.getStart() > getEnd()) { + throw new IllegalArgumentException("There is a gap between " + this + + " and " + other); + } + + return Range.between(Math.min(getStart(), other.getStart()), + Math.max(getEnd(), other.getEnd())); + } } diff --git a/client/src/com/vaadin/client/ui/grid/RowVisibilityChangeEvent.java b/client/src/com/vaadin/client/ui/grid/RowVisibilityChangeEvent.java new file mode 100644 index 0000000000..0e9652e215 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/RowVisibilityChangeEvent.java @@ -0,0 +1,90 @@ +/* + * 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 com.google.gwt.event.shared.GwtEvent; + +/** + * Event fired when the range of visible rows changes e.g. because of scrolling. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class RowVisibilityChangeEvent extends + GwtEvent { + /** + * The type of this event. + */ + public static final Type TYPE = new Type(); + + private final int firstVisibleRow; + private final int visibleRowCount; + + /** + * Creates a new row visibility change event + * + * @param firstVisibleRow + * the index of the first visible row + * @param visibleRowCount + * the number of visible rows + */ + public RowVisibilityChangeEvent(int firstVisibleRow, int visibleRowCount) { + this.firstVisibleRow = firstVisibleRow; + this.visibleRowCount = visibleRowCount; + } + + /** + * Gets the index of the first row that is at least partially visible. + * + * @return the index of the first visible row + */ + public int getFirstVisibleRow() { + return firstVisibleRow; + } + + /** + * Gets the number of at least partially visible rows. + * + * @return the number of visible rows + */ + public int getVisibleRowCount() { + return visibleRowCount; + } + + /* + * (non-Javadoc) + * + * @see com.google.gwt.event.shared.GwtEvent#getAssociatedType() + */ + @Override + public Type getAssociatedType() { + return TYPE; + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.shared.GwtEvent#dispatch(com.google.gwt.event.shared + * .EventHandler) + */ + @Override + protected void dispatch(RowVisibilityChangeHandler handler) { + handler.onRowVisibilityChange(this); + } + +} diff --git a/client/src/com/vaadin/client/ui/grid/RowVisibilityChangeHandler.java b/client/src/com/vaadin/client/ui/grid/RowVisibilityChangeHandler.java new file mode 100644 index 0000000000..dd24521499 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/RowVisibilityChangeHandler.java @@ -0,0 +1,38 @@ +/* + * 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 com.google.gwt.event.shared.EventHandler; + +/** + * Event handler that gets notified when the range of visible rows changes e.g. + * because of scrolling. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public interface RowVisibilityChangeHandler extends EventHandler { + + /** + * Called when the range of visible rows changes e.g. because of scrolling. + * + * @param event + * the row visibility change event describing the change + */ + void onRowVisibilityChange(RowVisibilityChangeEvent event); + +} diff --git a/client/tests/src/com/vaadin/client/ui/grid/RangeTest.java b/client/tests/src/com/vaadin/client/ui/grid/RangeTest.java index a4715924b4..4441ee901d 100644 --- a/client/tests/src/com/vaadin/client/ui/grid/RangeTest.java +++ b/client/tests/src/com/vaadin/client/ui/grid/RangeTest.java @@ -209,4 +209,80 @@ public class RangeTest { assertTrue("no overlap allowed", !Range.between(0, 10).endsAfter(Range.between(5, 10))); } + + @Test(expected = IllegalArgumentException.class) + public void combine_notOverlappingFirstSmaller() { + Range.between(0, 10).combineWith(Range.between(11, 20)); + } + + @Test(expected = IllegalArgumentException.class) + public void combine_notOverlappingSecondLarger() { + Range.between(11, 20).combineWith(Range.between(0, 10)); + } + + @Test(expected = IllegalArgumentException.class) + public void combine_firstEmptyNotOverlapping() { + Range.between(15, 15).combineWith(Range.between(0, 10)); + } + + @Test(expected = IllegalArgumentException.class) + public void combine_secondEmptyNotOverlapping() { + Range.between(0, 10).combineWith(Range.between(15, 15)); + } + + @Test + public void combine_barelyOverlapping() { + Range r1 = Range.between(0, 10); + Range r2 = Range.between(10, 20); + + // Test both ways, should give the same result + Range combined1 = r1.combineWith(r2); + Range combined2 = r2.combineWith(r1); + assertEquals(combined1, combined2); + + assertEquals(0, combined1.getStart()); + assertEquals(20, combined1.getEnd()); + } + + @Test + public void combine_subRange() { + Range r1 = Range.between(0, 10); + Range r2 = Range.between(2, 8); + + // Test both ways, should give the same result + Range combined1 = r1.combineWith(r2); + Range combined2 = r2.combineWith(r1); + assertEquals(combined1, combined2); + + assertEquals(r1, combined1); + } + + @Test + public void combine_intersecting() { + Range r1 = Range.between(0, 10); + Range r2 = Range.between(5, 15); + + // Test both ways, should give the same result + Range combined1 = r1.combineWith(r2); + Range combined2 = r2.combineWith(r1); + assertEquals(combined1, combined2); + + assertEquals(0, combined1.getStart()); + assertEquals(15, combined1.getEnd()); + + } + + @Test + public void combine_emptyInside() { + Range r1 = Range.between(0, 10); + Range r2 = Range.between(5, 5); + + // Test both ways, should give the same result + Range combined1 = r1.combineWith(r2); + Range combined2 = r2.combineWith(r1); + assertEquals(combined1, combined2); + + assertEquals(r1, combined1); + } + } diff --git a/server/src/com/vaadin/data/RpcDataProviderExtension.java b/server/src/com/vaadin/data/RpcDataProviderExtension.java new file mode 100644 index 0000000000..48f03b98c0 --- /dev/null +++ b/server/src/com/vaadin/data/RpcDataProviderExtension.java @@ -0,0 +1,101 @@ +/* + * 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.data; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import com.vaadin.data.Container.Indexed; +import com.vaadin.server.AbstractExtension; +import com.vaadin.shared.data.DataProviderRpc; +import com.vaadin.shared.data.DataProviderState; +import com.vaadin.shared.data.DataRequestRpc; +import com.vaadin.ui.components.grid.Grid; + +/** + * Provides Vaadin server-side container data source to a + * {@link com.vaadin.client.ui.grid.GridConnector}. This is currently + * implemented as an Extension hardcoded to support a specific connector type. + * This will be changed once framework support for something more flexible has + * been implemented. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class RpcDataProviderExtension extends AbstractExtension { + + private final Indexed container; + + /** + * Creates a new data provider using the given container. + * + * @param container + * the container to make available + */ + public RpcDataProviderExtension(Indexed container) { + this.container = container; + + // TODO support for reacting to events from the container added later + + registerRpc(new DataRequestRpc() { + @Override + public void requestRows(int firstRow, int numberOfRows) { + pushRows(firstRow, numberOfRows); + } + }); + + getState().containerSize = container.size(); + } + + private void pushRows(int firstRow, int numberOfRows) { + List itemIds = container.getItemIds(firstRow, numberOfRows); + Collection propertyIds = container.getContainerPropertyIds(); + List rows = new ArrayList(itemIds.size()); + for (Object itemId : itemIds) { + Item item = container.getItem(itemId); + String[] row = new String[propertyIds.size()]; + + int i = 0; + for (Object propertyId : propertyIds) { + Object value = item.getItemProperty(propertyId).getValue(); + String stringValue = String.valueOf(value); + row[i++] = stringValue; + } + + rows.add(row); + } + + getRpcProxy(DataProviderRpc.class).setRowData(firstRow, rows); + } + + @Override + protected DataProviderState getState() { + return (DataProviderState) super.getState(); + } + + /** + * Makes the data source available to the given {@link Grid} component. + * + * @param component + * the remote data grid component to extend + */ + public void extend(Grid component) { + super.extend(component); + } + +} diff --git a/server/src/com/vaadin/ui/components/grid/Grid.java b/server/src/com/vaadin/ui/components/grid/Grid.java index 2b19043d93..79cc05e1a0 100644 --- a/server/src/com/vaadin/ui/components/grid/Grid.java +++ b/server/src/com/vaadin/ui/components/grid/Grid.java @@ -29,6 +29,7 @@ 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.data.RpcDataProviderExtension; import com.vaadin.server.KeyMapper; import com.vaadin.shared.ui.grid.ColumnGroupRowState; import com.vaadin.shared.ui.grid.GridColumnState; @@ -107,6 +108,8 @@ public class Grid extends AbstractComponent { } }; + private RpcDataProviderExtension datasourceExtension; + /** * Creates a new Grid using the given datasource. * @@ -140,7 +143,13 @@ public class Grid extends AbstractComponent { .removePropertySetChangeListener(propertyListener); } + if (datasourceExtension != null) { + removeExtension(datasourceExtension); + } + datasource = container; + datasourceExtension = new RpcDataProviderExtension(container); + datasourceExtension.extend(this); // Listen to changes in properties and remove columns if needed if (datasource instanceof PropertySetChangeNotifier) { diff --git a/shared/src/com/vaadin/shared/data/DataProviderRpc.java b/shared/src/com/vaadin/shared/data/DataProviderRpc.java new file mode 100644 index 0000000000..7d82ecc342 --- /dev/null +++ b/shared/src/com/vaadin/shared/data/DataProviderRpc.java @@ -0,0 +1,40 @@ +/* + * 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.data; + +import java.util.List; + +import com.vaadin.shared.communication.ClientRpc; + +/** + * RPC interface used for pushing container data to the client. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public interface DataProviderRpc extends ClientRpc { + + /** + * Sends updated row data to a client. + * + * @param firstRowIndex + * the index of the first updated row + * @param rowData + * the updated row data + */ + public void setRowData(int firstRowIndex, List rowData); +} diff --git a/shared/src/com/vaadin/shared/data/DataProviderState.java b/shared/src/com/vaadin/shared/data/DataProviderState.java new file mode 100644 index 0000000000..2eabe0b0e1 --- /dev/null +++ b/shared/src/com/vaadin/shared/data/DataProviderState.java @@ -0,0 +1,32 @@ +/* + * 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.data; + +import com.vaadin.shared.communication.SharedState; + +/** + * Shared state used by client-side data sources. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class DataProviderState extends SharedState { + /** + * The size of the container. + */ + public int containerSize; +} diff --git a/shared/src/com/vaadin/shared/data/DataRequestRpc.java b/shared/src/com/vaadin/shared/data/DataRequestRpc.java new file mode 100644 index 0000000000..eaf17df8f6 --- /dev/null +++ b/shared/src/com/vaadin/shared/data/DataRequestRpc.java @@ -0,0 +1,38 @@ +/* + * 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.data; + +import com.vaadin.shared.communication.ServerRpc; + +/** + * RPC interface used for requesting container data to the client. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public interface DataRequestRpc extends ServerRpc { + + /** + * Request rows from the server. + * + * @param firstRowIndex + * the index of the first requested row + * @param numberOfRows + * the number of requested rows + */ + public void requestRows(int firstRowIndex, int numberOfRows); +} diff --git a/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java b/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java index bd3e96f84a..7bf5d65e8b 100644 --- a/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java +++ b/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java @@ -17,6 +17,7 @@ package com.vaadin.tests.components.grid; import java.util.ArrayList; +import com.vaadin.data.Item; import com.vaadin.data.util.IndexedContainer; import com.vaadin.tests.components.AbstractComponentTest; import com.vaadin.ui.components.grid.ColumnGroup; @@ -36,6 +37,8 @@ public class GridBasicFeatures extends AbstractComponentTest { private int columnGroupRows = 0; + private final int ROWS = 1000; + @Override protected Grid constructComponent() { @@ -46,6 +49,14 @@ public class GridBasicFeatures extends AbstractComponentTest { ds.addContainerProperty("Column" + col, String.class, ""); } + for (int row = 0; row < ROWS; row++) { + Item item = ds.addItem(Integer.valueOf(row)); + for (int col = 0; col < COLUMNS; col++) { + item.getItemProperty("Column" + col).setValue( + "(" + row + ", " + col + ")"); + } + } + // Create grid Grid grid = new Grid(ds); -- cgit v1.2.3 From cff79054fb76f472eaf3d53d4da27fff6d1a95f7 Mon Sep 17 00:00:00 2001 From: Henrik Paul Date: Sun, 24 Nov 2013 16:38:16 +0200 Subject: Add server-side API for column freezing (#3087) Change-Id: I4704ab2bd2b1af31b4586e26cf89f03d97f136a4 --- client/src/com/vaadin/client/ui/grid/Grid.java | 60 ++++++++++++++++ .../com/vaadin/client/ui/grid/GridConnector.java | 12 ++++ server/src/com/vaadin/ui/components/grid/Grid.java | 82 ++++++++++++++++++++++ .../com/vaadin/ui/components/grid/GridColumn.java | 12 ++++ .../tests/server/component/grid/GridColumns.java | 14 ++++ .../src/com/vaadin/shared/ui/grid/GridState.java | 8 +++ .../tests/components/grid/GridBasicFeatures.java | 9 +++ 7 files changed, 197 insertions(+) (limited to 'shared') diff --git a/client/src/com/vaadin/client/ui/grid/Grid.java b/client/src/com/vaadin/client/ui/grid/Grid.java index d76424ae31..90c8b60474 100644 --- a/client/src/com/vaadin/client/ui/grid/Grid.java +++ b/client/src/com/vaadin/client/ui/grid/Grid.java @@ -83,6 +83,8 @@ public class Grid extends Composite { */ private boolean columnFootersVisible = false; + private GridColumn lastFrozenColumn; + /** * Base class for grid columns internally used by the Grid. The user should * use {@link GridColumn} when creating new columns. @@ -560,6 +562,12 @@ public class Grid extends Composite { ColumnConfiguration conf = escalator.getColumnConfiguration(); conf.insertColumns(index, 1); + + if (lastFrozenColumn != null + && ((AbstractGridColumn) lastFrozenColumn) + .findIndexOfColumn() < index) { + refreshFrozenColumns(); + } } /** @@ -578,6 +586,12 @@ public class Grid extends Composite { ColumnConfiguration conf = escalator.getColumnConfiguration(); conf.removeColumns(columnIndex, 1); + + if (column.equals(lastFrozenColumn)) { + setLastFrozenColumn(null); + } else { + refreshFrozenColumns(); + } } /** @@ -861,4 +875,50 @@ public class Grid extends Composite { escalator.getBody().insertRows(0, estimatedSize); } } + + /** + * Sets the rightmost frozen column in the grid. + *

+ * All columns up to and including the given column will be frozen in place + * when the grid is scrolled sideways. + * + * @param lastFrozenColumn + * the rightmost column to freeze, or null to not + * have any columns frozen + * @throws IllegalArgumentException + * if {@code lastFrozenColumn} is not a column from this grid + */ + public void setLastFrozenColumn(GridColumn lastFrozenColumn) { + this.lastFrozenColumn = lastFrozenColumn; + refreshFrozenColumns(); + } + + private void refreshFrozenColumns() { + final int frozenCount; + if (lastFrozenColumn != null) { + frozenCount = columns.indexOf(lastFrozenColumn) + 1; + if (frozenCount == 0) { + throw new IllegalArgumentException( + "The given column isn't attached to this grid"); + } + } else { + frozenCount = 0; + } + + escalator.getColumnConfiguration().setFrozenColumnCount(frozenCount); + } + + /** + * Gets the rightmost frozen column in the grid. + *

+ * Note: Most usually, this method returns the very value set with + * {@link #setLastFrozenColumn(GridColumn)}. This value, however, can be + * reset to null if the column is removed from this grid. + * + * @return the rightmost frozen column in the grid, or null if + * no columns are frozen. + */ + public GridColumn getLastFrozenColumn() { + return lastFrozenColumn; + } } diff --git a/client/src/com/vaadin/client/ui/grid/GridConnector.java b/client/src/com/vaadin/client/ui/grid/GridConnector.java index 896a9998fb..befbc5a50b 100644 --- a/client/src/com/vaadin/client/ui/grid/GridConnector.java +++ b/client/src/com/vaadin/client/ui/grid/GridConnector.java @@ -125,6 +125,18 @@ public class GridConnector extends AbstractComponentConnector { if (stateChangeEvent.hasPropertyChanged("columnGroupRows")) { updateColumnGroupsFromStateChangeEvent(); } + + if (stateChangeEvent.hasPropertyChanged("lastFrozenColumnId")) { + String frozenColId = getState().lastFrozenColumnId; + if (frozenColId != null) { + CustomGridColumn column = columnIdToColumn.get(frozenColId); + assert column != null : "Column to be frozen could not be found (id:" + + frozenColId + ")"; + getWidget().setLastFrozenColumn(column); + } else { + getWidget().setLastFrozenColumn(null); + } + } } /** diff --git a/server/src/com/vaadin/ui/components/grid/Grid.java b/server/src/com/vaadin/ui/components/grid/Grid.java index 79cc05e1a0..1fb0692104 100644 --- a/server/src/com/vaadin/ui/components/grid/Grid.java +++ b/server/src/com/vaadin/ui/components/grid/Grid.java @@ -105,6 +105,12 @@ public class Grid extends AbstractComponent { appendColumn(propertyId); } } + + Object frozenPropertyId = columnKeys + .get(getState(false).lastFrozenColumnId); + if (!columns.containsKey(frozenPropertyId)) { + setLastFrozenPropertyId(null); + } } }; @@ -158,6 +164,7 @@ public class Grid extends AbstractComponent { } getState().columns.clear(); + setLastFrozenPropertyId(null); // Add columns for (Object propertyId : datasource.getContainerPropertyIds()) { @@ -362,4 +369,79 @@ public class Grid extends AbstractComponent { return column; } + + /** + * Sets (or unsets) the rightmost frozen column in the grid. + *

+ * All columns up to and including the given column will be frozen in place + * when the grid is scrolled sideways. + * + * @param lastFrozenColumn + * the rightmost column to freeze, or null to not + * have any columns frozen + * @throws IllegalArgumentException + * if {@code lastFrozenColumn} is not a column from this grid + */ + void setLastFrozenColumn(GridColumn lastFrozenColumn) { + /* + * TODO: If and when Grid supports column reordering or insertion of + * columns before other columns, make sure to mention that adding + * columns before lastFrozenColumn will change the frozen column count + */ + + if (lastFrozenColumn == null) { + getState().lastFrozenColumnId = null; + } else if (columns.containsValue(lastFrozenColumn)) { + getState().lastFrozenColumnId = lastFrozenColumn.getState().id; + } else { + throw new IllegalArgumentException( + "The given column isn't attached to this grid"); + } + } + + /** + * Sets (or unsets) the rightmost frozen column in the grid. + *

+ * All columns up to and including the indicated property will be frozen in + * place when the grid is scrolled sideways. + *

+ * Note: If the container used by this grid supports a propertyId + * null, it can never be defined as the last frozen column, as + * a null parameter will always reset the frozen columns in + * Grid. + * + * @param propertyId + * the property id corresponding to the column that should be the + * last frozen column, or null to not have any + * columns frozen. + * @throws IllegalArgumentException + * if {@code lastFrozenColumn} is not a column from this grid + */ + public void setLastFrozenPropertyId(Object propertyId) { + final GridColumn column; + if (propertyId == null) { + column = null; + } else { + column = getColumn(propertyId); + if (column == null) { + throw new IllegalArgumentException( + "property id does not exist."); + } + } + setLastFrozenColumn(column); + } + + /** + * Gets the rightmost frozen column in the grid. + *

+ * Note: Most often, this method returns the very value set with + * {@link #setLastFrozenPropertyId(Object)}. This value, however, can be + * reset to null if the column is detached from this grid. + * + * @return the rightmost frozen column in the grid, or null if + * no columns are frozen. + */ + public Object getLastFrozenPropertyId() { + return columnKeys.get(getState().lastFrozenColumnId); + } } diff --git a/server/src/com/vaadin/ui/components/grid/GridColumn.java b/server/src/com/vaadin/ui/components/grid/GridColumn.java index dde0669238..8dae9428e5 100644 --- a/server/src/com/vaadin/ui/components/grid/GridColumn.java +++ b/server/src/com/vaadin/ui/components/grid/GridColumn.java @@ -192,4 +192,16 @@ public class GridColumn implements Serializable { throw new IllegalStateException("Column no longer exists."); } } + + /** + * Sets this column as the last frozen column in its grid. + * + * @throws IllegalArgumentException + * if the column is no longer attached to any grid + * @see Grid#setLastFrozenColumn(GridColumn) + */ + public void setLastFrozenColumn() { + checkColumnIsAttached(); + grid.setLastFrozenColumn(this); + } } 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 85864160a8..c129db0264 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 @@ -242,6 +242,20 @@ public class GridColumns { assertTrue(state.columnGroupRows.size() == 3); } + @Test + public void testFrozenColumnByPropertyId() { + assertNull("Grid should not start with a frozen column", + grid.getLastFrozenPropertyId()); + + Object propertyId = grid.getContainerDatasource() + .getContainerPropertyIds().iterator().next(); + grid.setLastFrozenPropertyId(propertyId); + assertEquals(propertyId, grid.getLastFrozenPropertyId()); + + grid.getContainerDatasource().removeContainerProperty(propertyId); + assertNull(grid.getLastFrozenPropertyId()); + } + private GridColumnState getColumnState(Object propertyId) { String columnId = columnIdMapper.key(propertyId); for (GridColumnState columnState : state.columns) { diff --git a/shared/src/com/vaadin/shared/ui/grid/GridState.java b/shared/src/com/vaadin/shared/ui/grid/GridState.java index d1167f3d4f..93e602a539 100644 --- a/shared/src/com/vaadin/shared/ui/grid/GridState.java +++ b/shared/src/com/vaadin/shared/ui/grid/GridState.java @@ -53,4 +53,12 @@ public class GridState extends AbstractComponentState { * The column groups added to the grid */ public List columnGroupRows = new ArrayList(); + + /** + * The id for the last frozen column. + * + * @see GridColumnState#id + */ + public String lastFrozenColumnId = null; + } diff --git a/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java b/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java index 7bf5d65e8b..afc9f91e68 100644 --- a/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java +++ b/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java @@ -133,6 +133,15 @@ public class GridBasicFeatures extends AbstractComponentTest { } }, null, c); + createClickAction("Freeze", "Column" + c, + new Command() { + + @Override + public void execute(Grid grid, String value, Object data) { + grid.setLastFrozenPropertyId("Column" + data); + } + }, null, c); + } } -- cgit v1.2.3 From 7670a020c7447d7ae2fe2c3e1fb1fa966b138e65 Mon Sep 17 00:00:00 2001 From: Henrik Paul Date: Tue, 17 Dec 2013 11:08:25 +0200 Subject: Grid supports data set changes (#12645) Change-Id: I5ceb52dea079f48b0065c1b2dbdc35b30fe8c4ee --- .../client/data/AbstractRemoteDataSource.java | 84 ++++- .../vaadin/client/data/RpcDataSourceConnector.java | 10 + .../src/com/vaadin/client/ui/grid/Escalator.java | 47 ++- client/src/com/vaadin/client/ui/grid/Grid.java | 15 + .../com/vaadin/client/ui/grid/GridConnector.java | 16 + client/src/com/vaadin/client/ui/grid/Range.java | 378 --------------------- .../vaadin/client/ui/grid/PartitioningTest.java | 2 + .../src/com/vaadin/client/ui/grid/RangeTest.java | 318 ----------------- .../com/vaadin/data/RpcDataProviderExtension.java | 71 +++- server/src/com/vaadin/ui/components/grid/Grid.java | 362 +++++++++++++++++++- .../com/vaadin/shared/data/DataProviderRpc.java | 21 ++ .../com/vaadin/shared/ui/grid/GridServerRpc.java | 39 +++ shared/src/com/vaadin/shared/ui/grid/Range.java | 378 +++++++++++++++++++++ .../src/com/vaadin/shared/ui/grid/RangeTest.java | 318 +++++++++++++++++ .../tests/components/grid/GridBasicFeatures.java | 79 ++++- .../components/grid/GridBasicFeaturesTest.java | 50 +++ 16 files changed, 1455 insertions(+), 733 deletions(-) delete mode 100644 client/src/com/vaadin/client/ui/grid/Range.java delete mode 100644 client/tests/src/com/vaadin/client/ui/grid/RangeTest.java create mode 100644 shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java create mode 100644 shared/src/com/vaadin/shared/ui/grid/Range.java create mode 100644 shared/tests/src/com/vaadin/shared/ui/grid/RangeTest.java (limited to 'shared') diff --git a/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java b/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java index ff8847ea44..127eb80696 100644 --- a/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java +++ b/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java @@ -22,7 +22,7 @@ import java.util.List; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.vaadin.client.Profiler; -import com.vaadin.client.ui.grid.Range; +import com.vaadin.shared.ui.grid.Range; /** * Base implementation for data sources that fetch data from a remote system. @@ -238,4 +238,86 @@ public abstract class AbstractRemoteDataSource implements DataSource { Profiler.leave("AbstractRemoteDataSource.setRowData"); } + + /** + * Informs this data source that the server has removed data. + * + * @param firstRowIndex + * the index of the first removed row + * @param count + * the number of removed rows, starting from + * firstRowIndex + */ + protected void removeRowData(int firstRowIndex, int count) { + Profiler.enter("AbstractRemoteDataSource.removeRowData"); + + // pack the cached data + for (int i = 0; i < count; i++) { + Integer oldIndex = Integer.valueOf(firstRowIndex + count + i); + if (rowCache.containsKey(oldIndex)) { + Integer newIndex = Integer.valueOf(firstRowIndex + i); + rowCache.put(newIndex, rowCache.remove(oldIndex)); + } + } + + Range removedRange = Range.withLength(firstRowIndex, count); + if (removedRange.intersects(cached)) { + Range[] partitions = cached.partitionWith(removedRange); + Range remainsBefore = partitions[0]; + Range transposedRemainsAfter = partitions[2].offsetBy(-removedRange + .length()); + cached = remainsBefore.combineWith(transposedRemainsAfter); + } + estimatedSize -= count; + dataChangeHandler.dataRemoved(firstRowIndex, count); + checkCacheCoverage(); + + Profiler.leave("AbstractRemoteDataSource.removeRowData"); + } + + /** + * Informs this data source that new data has been inserted from the server. + * + * @param firstRowIndex + * the destination index of the new row data + * @param count + * the number of rows inserted + */ + protected void insertRowData(int firstRowIndex, int count) { + Profiler.enter("AbstractRemoteDataSource.insertRowData"); + + if (cached.contains(firstRowIndex)) { + int oldCacheEnd = cached.getEnd(); + /* + * We need to invalidate the cache from the inserted row onwards, + * since the cache wants to be a contiguous range. It doesn't + * support holes. + * + * If holes were supported, we could shift the higher part of + * "cached" and leave a hole the size of "count" in the middle. + */ + cached = cached.splitAt(firstRowIndex)[0]; + + for (int i = firstRowIndex; i < oldCacheEnd; i++) { + rowCache.remove(Integer.valueOf(i)); + } + } + + else if (firstRowIndex < cached.getStart()) { + Range oldCached = cached; + cached = cached.offsetBy(count); + + for (int i = 0; i < rowCache.size(); i++) { + Integer oldIndex = Integer.valueOf(oldCached.getEnd() - i); + Integer newIndex = Integer.valueOf(cached.getEnd() - i); + rowCache.put(newIndex, rowCache.remove(oldIndex)); + } + } + + estimatedSize += count; + dataChangeHandler.dataAdded(firstRowIndex, count); + checkCacheCoverage(); + + Profiler.leave("AbstractRemoteDataSource.insertRowData"); + } } diff --git a/client/src/com/vaadin/client/data/RpcDataSourceConnector.java b/client/src/com/vaadin/client/data/RpcDataSourceConnector.java index 1785fc62c2..4d22c10197 100644 --- a/client/src/com/vaadin/client/data/RpcDataSourceConnector.java +++ b/client/src/com/vaadin/client/data/RpcDataSourceConnector.java @@ -56,6 +56,16 @@ public class RpcDataSourceConnector extends AbstractExtensionConnector { public void setRowData(int firstRow, List rows) { dataSource.setRowData(firstRow, rows); } + + @Override + public void removeRowData(int firstRow, int count) { + dataSource.removeRowData(firstRow, count); + } + + @Override + public void insertRowData(int firstRow, int count) { + dataSource.insertRowData(firstRow, count); + } }); } diff --git a/client/src/com/vaadin/client/ui/grid/Escalator.java b/client/src/com/vaadin/client/ui/grid/Escalator.java index 20a187e1a5..a395038890 100644 --- a/client/src/com/vaadin/client/ui/grid/Escalator.java +++ b/client/src/com/vaadin/client/ui/grid/Escalator.java @@ -35,12 +35,12 @@ import com.google.gwt.dom.client.NodeList; import com.google.gwt.dom.client.Style; import com.google.gwt.dom.client.Style.Display; import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Element; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.UIObject; import com.google.gwt.user.client.ui.Widget; -import com.google.web.bindery.event.shared.HandlerRegistration; import com.vaadin.client.Profiler; import com.vaadin.client.Util; import com.vaadin.client.ui.grid.Escalator.JsniUtil.TouchHandlerBundle; @@ -50,6 +50,7 @@ import com.vaadin.client.ui.grid.PositionFunction.TranslatePosition; import com.vaadin.client.ui.grid.PositionFunction.WebkitTranslate3DPosition; import com.vaadin.client.ui.grid.ScrollbarBundle.HorizontalScrollbarBundle; import com.vaadin.client.ui.grid.ScrollbarBundle.VerticalScrollbarBundle; +import com.vaadin.shared.ui.grid.Range; import com.vaadin.shared.util.SharedUtil; /*- @@ -1633,6 +1634,8 @@ public class Escalator extends Widget { return; } + boolean rowsWereMoved = false; + final int topRowPos = getRowTop(visualRowOrder.getFirst()); // TODO [[mpixscroll]] final int scrollTop = tBodyScrollTop; @@ -1655,6 +1658,8 @@ public class Escalator extends Widget { final int logicalRowIndex = scrollTop / ROW_HEIGHT_PX; moveAndUpdateEscalatorRows(Range.between(start, end), 0, logicalRowIndex); + + rowsWereMoved = (rowsToMove != 0); } else if (viewportOffset + ROW_HEIGHT_PX <= 0) { @@ -1723,9 +1728,13 @@ public class Escalator extends Widget { .get(1)) - 1; moveAndUpdateEscalatorRows(strayRow, 0, topLogicalIndex); } + + rowsWereMoved = (rowsToMove != 0); } - fireRowVisibilityChangeEvent(); + if (rowsWereMoved) { + fireRowVisibilityChangeEvent(); + } } @Override @@ -1805,6 +1814,8 @@ public class Escalator extends Widget { setRowPosition(tr, 0, rowTop); rowTop += ROW_HEIGHT_PX; } + + fireRowVisibilityChangeEvent(); } return addedRows; } @@ -1919,8 +1930,6 @@ public class Escalator extends Widget { newRowTop += ROW_HEIGHT_PX; } } - - fireRowVisibilityChangeEvent(); } /** @@ -3181,9 +3190,15 @@ public class Escalator extends Widget { */ @Override public void setHeight(final String height) { + final int escalatorRowsBefore = body.visualRowOrder.size(); + super.setHeight(height != null && !height.isEmpty() ? height : DEFAULT_HEIGHT); recalculateElementSizes(); + + if (escalatorRowsBefore != body.visualRowOrder.size()) { + fireRowVisibilityChangeEvent(); + } } /** @@ -3437,26 +3452,30 @@ public class Escalator extends Widget { * Adds an event handler that gets notified when the range of visible rows * changes e.g. because of scrolling. * - * @param rowVisibilityChangeHadler + * @param rowVisibilityChangeHandler * the event handler * @return a handler registration for the added handler */ public HandlerRegistration addRowVisibilityChangeHandler( - RowVisibilityChangeHandler rowVisibilityChangeHadler) { - return addHandler(rowVisibilityChangeHadler, + RowVisibilityChangeHandler rowVisibilityChangeHandler) { + return addHandler(rowVisibilityChangeHandler, RowVisibilityChangeEvent.TYPE); } private void fireRowVisibilityChangeEvent() { - int visibleRangeStart = body.getLogicalRowIndex(body.visualRowOrder - .getFirst()); - int visibleRangeEnd = body.getLogicalRowIndex(body.visualRowOrder - .getLast()) + 1; + if (!body.visualRowOrder.isEmpty()) { + int visibleRangeStart = body.getLogicalRowIndex(body.visualRowOrder + .getFirst()); + int visibleRangeEnd = body.getLogicalRowIndex(body.visualRowOrder + .getLast()) + 1; - int visibleRowCount = visibleRangeEnd - visibleRangeStart; + int visibleRowCount = visibleRangeEnd - visibleRangeStart; - fireEvent(new RowVisibilityChangeEvent(visibleRangeStart, - visibleRowCount)); + fireEvent(new RowVisibilityChangeEvent(visibleRangeStart, + visibleRowCount)); + } else { + fireEvent(new RowVisibilityChangeEvent(0, 0)); + } } /** diff --git a/client/src/com/vaadin/client/ui/grid/Grid.java b/client/src/com/vaadin/client/ui/grid/Grid.java index 2dbb0275cd..7f8ab408a9 100644 --- a/client/src/com/vaadin/client/ui/grid/Grid.java +++ b/client/src/com/vaadin/client/ui/grid/Grid.java @@ -21,6 +21,7 @@ import java.util.List; import com.google.gwt.core.shared.GWT; import com.google.gwt.dom.client.Element; +import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.HasVisibility; import com.google.gwt.user.client.ui.Widget; @@ -73,6 +74,10 @@ public class Grid extends Composite { */ private final List> columns = new ArrayList>(); + /** + * The datasource currently in use. Note: it is null + * on initialization, but not after that. + */ private DataSource dataSource; /** @@ -1211,4 +1216,14 @@ public class Grid extends Composite { public GridColumn getLastFrozenColumn() { return lastFrozenColumn; } + + public HandlerRegistration addRowVisibilityChangeHandler( + RowVisibilityChangeHandler handler) { + /* + * Reusing Escalator's RowVisibilityChangeHandler, since a scroll + * concept is too abstract. e.g. the event needs to be re-sent when the + * widget is resized. + */ + return escalator.addRowVisibilityChangeHandler(handler); + } } diff --git a/client/src/com/vaadin/client/ui/grid/GridConnector.java b/client/src/com/vaadin/client/ui/grid/GridConnector.java index ffe1444942..f04326c7e6 100644 --- a/client/src/com/vaadin/client/ui/grid/GridConnector.java +++ b/client/src/com/vaadin/client/ui/grid/GridConnector.java @@ -30,6 +30,7 @@ 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.GridServerRpc; import com.vaadin.shared.ui.grid.GridState; /** @@ -82,6 +83,21 @@ public class GridConnector extends AbstractComponentConnector { return (GridState) super.getState(); } + @Override + protected void init() { + super.init(); + getWidget().addRowVisibilityChangeHandler( + new RowVisibilityChangeHandler() { + @Override + public void onRowVisibilityChange( + RowVisibilityChangeEvent event) { + getRpcProxy(GridServerRpc.class).setVisibleRows( + event.getFirstVisibleRow(), + event.getVisibleRowCount()); + } + }); + } + @Override public void onStateChanged(StateChangeEvent stateChangeEvent) { super.onStateChanged(stateChangeEvent); diff --git a/client/src/com/vaadin/client/ui/grid/Range.java b/client/src/com/vaadin/client/ui/grid/Range.java deleted file mode 100644 index 634a182421..0000000000 --- a/client/src/com/vaadin/client/ui/grid/Range.java +++ /dev/null @@ -1,378 +0,0 @@ -/* - * 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; - -/** - * An immutable representation of a range, marked by start and end points. - *

- * The range is treated as inclusive at the start, and exclusive at the end. - * I.e. the range [0..1[ has the length 1, and represents one integer: 0. - *

- * The range is considered {@link #isEmpty() empty} if the start is the same as - * the end. - * - * @since 7.2 - * @author Vaadin Ltd - */ -public final class Range { - private final int start; - private final int end; - - /** - * Creates a range object representing a single integer. - * - * @param integer - * the number to represent as a range - * @return the range represented by integer - */ - public static Range withOnly(final int integer) { - return new Range(integer, integer + 1); - } - - /** - * Creates a range between two integers. - *

- * The range start is inclusive and the end is exclusive. - * So, a range "between" 0 and 5 represents the numbers 0, 1, 2, 3 and 4, - * but not 5. - * - * @param start - * the start of the the range, inclusive - * @param end - * the end of the range, exclusive - * @return a range representing [start..end[ - * @throws IllegalArgumentException - * if start > end - */ - public static Range between(final int start, final int end) - throws IllegalArgumentException { - return new Range(start, end); - } - - /** - * Creates a range from a start point, with a given length. - * - * @param start - * the first integer to include in the range - * @param length - * the length of the resulting range - * @return a range starting from start, with - * length number of integers following - * @throws IllegalArgumentException - * if length < 0 - */ - public static Range withLength(final int start, final int length) - throws IllegalArgumentException { - if (length < 0) { - /* - * The constructor of Range will throw an exception if start > - * start+length (i.e. if length is negative). We're throwing the - * same exception type, just with a more descriptive message. - */ - throw new IllegalArgumentException("length must not be negative"); - } - return new Range(start, start + length); - } - - /** - * Creates a new range between two numbers: [start..end[. - * - * @param start - * the start integer, inclusive - * @param end - * the end integer, exclusive - * @throws IllegalArgumentException - * if start > end - */ - private Range(final int start, final int end) - throws IllegalArgumentException { - if (start > end) { - throw new IllegalArgumentException( - "start must not be greater than end"); - } - - this.start = start; - this.end = end; - } - - /** - * Returns the inclusive start point of this range. - * - * @return the start point of this range - */ - public int getStart() { - return start; - } - - /** - * Returns the exclusive end point of this range. - * - * @return the end point of this range - */ - public int getEnd() { - return end; - } - - /** - * The number of integers contained in the range. - * - * @return the number of integers contained in the range - */ - public int length() { - return getEnd() - getStart(); - } - - /** - * Checks whether the range has no elements between the start and end. - * - * @return true iff the range contains no elements. - */ - public boolean isEmpty() { - return getStart() >= getEnd(); - } - - /** - * Checks whether this range and another range are at least partially - * covering the same values. - * - * @param other - * the other range to check against - * @return true if this and other intersect - */ - public boolean intersects(final Range other) { - return getStart() < other.getEnd() && other.getStart() < getEnd(); - } - - /** - * Checks whether an integer is found within this range. - * - * @param integer - * an integer to test for presence in this range - * @return true iff integer is in this range - */ - public boolean contains(final int integer) { - return getStart() <= integer && integer < getEnd(); - } - - /** - * Checks whether this range is a subset of another range. - * - * @return true iff other completely wraps this - * range - */ - public boolean isSubsetOf(final Range other) { - return other.getStart() <= getStart() && getEnd() <= other.getEnd(); - } - - /** - * Overlay this range with another one, and partition the ranges according - * to how they position relative to each other. - *

- * The three partitions are returned as a three-element Range array: - *

    - *
  • Elements in this range that occur before elements in - * other. - *
  • Elements that are shared between the two ranges. - *
  • Elements in this range that occur after elements in - * other. - *
- * - * @param other - * the other range to act as delimiters. - * @return a three-element Range array of partitions depicting the elements - * before (index 0), shared/inside (index 1) and after (index 2). - */ - public Range[] partitionWith(final Range other) { - final Range[] splitBefore = splitAt(other.getStart()); - final Range rangeBefore = splitBefore[0]; - final Range[] splitAfter = splitBefore[1].splitAt(other.getEnd()); - final Range rangeInside = splitAfter[0]; - final Range rangeAfter = splitAfter[1]; - return new Range[] { rangeBefore, rangeInside, rangeAfter }; - } - - /** - * Get a range that is based on this one, but offset by a number. - * - * @param offset - * the number to offset by - * @return a copy of this range, offset by offset - */ - public Range offsetBy(final int offset) { - if (offset == 0) { - return this; - } else { - return new Range(start + offset, end + offset); - } - } - - @Override - public String toString() { - return getClass().getSimpleName() + " [" + getStart() + ".." + getEnd() - + "[" + (isEmpty() ? " (empty)" : ""); - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + end; - result = prime * result + start; - return result; - } - - @Override - public boolean equals(final Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - final Range other = (Range) obj; - if (end != other.end) { - return false; - } - if (start != other.start) { - return false; - } - return true; - } - - /** - * Checks whether this range starts before the start of another range. - * - * @param other - * the other range to compare against - * @return true iff this range starts before the - * other - */ - public boolean startsBefore(final Range other) { - return getStart() < other.getStart(); - } - - /** - * Checks whether this range ends before the start of another range. - * - * @param other - * the other range to compare against - * @return true iff this range ends before the - * other - */ - public boolean endsBefore(final Range other) { - return getEnd() <= other.getStart(); - } - - /** - * Checks whether this range ends after the end of another range. - * - * @param other - * the other range to compare against - * @return true iff this range ends after the - * other - */ - public boolean endsAfter(final Range other) { - return getEnd() > other.getEnd(); - } - - /** - * Checks whether this range starts after the end of another range. - * - * @param other - * the other range to compare against - * @return true iff this range starts after the - * other - */ - public boolean startsAfter(final Range other) { - return getStart() >= other.getEnd(); - } - - /** - * Split the range into two at a certain integer. - *

- * Example: [5..10[.splitAt(7) == [5..7[, [7..10[ - * - * @param integer - * the integer at which to split the range into two - * @return an array of two ranges, with [start..integer[ in the - * first element, and [integer..end[ in the second - * element. - *

- * If {@code integer} is less than {@code start}, [empty, - * {@code this} ] is returned. if integer is equal to - * or greater than {@code end}, [{@code this}, empty] is returned - * instead. - */ - public Range[] splitAt(final int integer) { - if (integer < start) { - return new Range[] { Range.withLength(start, 0), this }; - } else if (integer >= end) { - return new Range[] { this, Range.withLength(end, 0) }; - } else { - return new Range[] { new Range(start, integer), - new Range(integer, end) }; - } - } - - /** - * Split the range into two after a certain number of integers into the - * range. - *

- * Calling this method is equivalent to calling - * {@link #splitAt(int) splitAt}({@link #getStart()}+length); - *

- * Example: - * [5..10[.splitAtFromStart(2) == [5..7[, [7..10[ - * - * @param length - * the length at which to split this range into two - * @return an array of two ranges, having the length-first - * elements of this range, and the second range having the rest. If - * length ≤ 0, the first element will be empty, and - * the second element will be this range. If length - * ≥ {@link #length()}, the first element will be this range, - * and the second element will be empty. - */ - public Range[] splitAtFromStart(final int length) { - return splitAt(getStart() + length); - } - - /** - * Combines two ranges to create a range containing all values in both - * ranges, provided there are no gaps between the ranges. - * - * @param other - * the range to combine with this range - * - * @return the combined range - * - * @throws IllegalArgumentException - * if the two ranges aren't connected - */ - public Range combineWith(Range other) throws IllegalArgumentException { - if (getStart() > other.getEnd() || other.getStart() > getEnd()) { - throw new IllegalArgumentException("There is a gap between " + this - + " and " + other); - } - - return Range.between(Math.min(getStart(), other.getStart()), - Math.max(getEnd(), other.getEnd())); - } -} diff --git a/client/tests/src/com/vaadin/client/ui/grid/PartitioningTest.java b/client/tests/src/com/vaadin/client/ui/grid/PartitioningTest.java index 3cbc6351b1..e97bb339e4 100644 --- a/client/tests/src/com/vaadin/client/ui/grid/PartitioningTest.java +++ b/client/tests/src/com/vaadin/client/ui/grid/PartitioningTest.java @@ -21,6 +21,8 @@ import static org.junit.Assert.assertTrue; import org.junit.Test; +import com.vaadin.shared.ui.grid.Range; + @SuppressWarnings("static-method") public class PartitioningTest { diff --git a/client/tests/src/com/vaadin/client/ui/grid/RangeTest.java b/client/tests/src/com/vaadin/client/ui/grid/RangeTest.java deleted file mode 100644 index d73b0fb02f..0000000000 --- a/client/tests/src/com/vaadin/client/ui/grid/RangeTest.java +++ /dev/null @@ -1,318 +0,0 @@ -/* - * 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 static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -import org.junit.Test; - -@SuppressWarnings("static-method") -public class RangeTest { - - @Test(expected = IllegalArgumentException.class) - public void startAfterEndTest() { - Range.between(10, 9); - } - - @Test(expected = IllegalArgumentException.class) - public void negativeLengthTest() { - Range.withLength(10, -1); - } - - @Test - public void constructorEquivalenceTest() { - assertEquals("10 == [10,11[", Range.withOnly(10), Range.between(10, 11)); - assertEquals("[10,20[ == 10, length 10", Range.between(10, 20), - Range.withLength(10, 10)); - assertEquals("10 == 10, length 1", Range.withOnly(10), - Range.withLength(10, 1)); - } - - @Test - public void boundsTest() { - { - final Range range = Range.between(0, 10); - assertEquals("between(0, 10) start", 0, range.getStart()); - assertEquals("between(0, 10) end", 10, range.getEnd()); - } - - { - final Range single = Range.withOnly(10); - assertEquals("withOnly(10) start", 10, single.getStart()); - assertEquals("withOnly(10) end", 11, single.getEnd()); - } - - { - final Range length = Range.withLength(10, 5); - assertEquals("withLength(10, 5) start", 10, length.getStart()); - assertEquals("withLength(10, 5) end", 15, length.getEnd()); - } - } - - @Test - @SuppressWarnings("boxing") - public void equalsTest() { - final Range range1 = Range.between(0, 10); - final Range range2 = Range.withLength(0, 11); - - assertTrue("null", !range1.equals(null)); - assertTrue("reflexive", range1.equals(range1)); - assertEquals("symmetric", range1.equals(range2), range2.equals(range1)); - } - - @Test - public void containsTest() { - final int start = 0; - final int end = 10; - final Range range = Range.between(start, end); - - assertTrue("start should be contained", range.contains(start)); - assertTrue("start-1 should not be contained", - !range.contains(start - 1)); - assertTrue("end should not be contained", !range.contains(end)); - assertTrue("end-1 should be contained", range.contains(end - 1)); - - assertTrue("[0..10[ contains 5", Range.between(0, 10).contains(5)); - assertTrue("empty range does not contain 5", !Range.between(5, 5) - .contains(5)); - } - - @Test - public void emptyTest() { - assertTrue("[0..0[ should be empty", Range.between(0, 0).isEmpty()); - assertTrue("Range of length 0 should be empty", Range.withLength(0, 0) - .isEmpty()); - - assertTrue("[0..1[ should not be empty", !Range.between(0, 1).isEmpty()); - assertTrue("Range of length 1 should not be empty", - !Range.withLength(0, 1).isEmpty()); - } - - @Test - public void splitTest() { - final Range startRange = Range.between(0, 10); - final Range[] splitRanges = startRange.splitAt(5); - assertEquals("[0..10[ split at 5, lower", Range.between(0, 5), - splitRanges[0]); - assertEquals("[0..10[ split at 5, upper", Range.between(5, 10), - splitRanges[1]); - } - - @Test - public void split_valueBefore() { - Range range = Range.between(10, 20); - Range[] splitRanges = range.splitAt(5); - - assertEquals(Range.between(10, 10), splitRanges[0]); - assertEquals(range, splitRanges[1]); - } - - @Test - public void split_valueAfter() { - Range range = Range.between(10, 20); - Range[] splitRanges = range.splitAt(25); - - assertEquals(range, splitRanges[0]); - assertEquals(Range.between(20, 20), splitRanges[1]); - } - - @Test - public void emptySplitTest() { - final Range range = Range.between(5, 10); - final Range[] split1 = range.splitAt(0); - assertTrue("split1, [0]", split1[0].isEmpty()); - assertEquals("split1, [1]", range, split1[1]); - - final Range[] split2 = range.splitAt(15); - assertEquals("split2, [0]", range, split2[0]); - assertTrue("split2, [1]", split2[1].isEmpty()); - } - - @Test - public void lengthTest() { - assertEquals("withLength length", 5, Range.withLength(10, 5).length()); - assertEquals("between length", 5, Range.between(10, 15).length()); - assertEquals("withOnly 10 length", 1, Range.withOnly(10).length()); - } - - @Test - public void intersectsTest() { - assertTrue("[0..10[ intersects [5..15[", Range.between(0, 10) - .intersects(Range.between(5, 15))); - assertTrue("[0..10[ does not intersect [10..20[", !Range.between(0, 10) - .intersects(Range.between(10, 20))); - } - - @Test - public void intersects_emptyInside() { - assertTrue("[5..5[ does intersect with [0..10[", Range.between(5, 5) - .intersects(Range.between(0, 10))); - assertTrue("[0..10[ does intersect with [5..5[", Range.between(0, 10) - .intersects(Range.between(5, 5))); - } - - @Test - public void intersects_emptyOutside() { - assertTrue("[15..15[ does not intersect with [0..10[", - !Range.between(15, 15).intersects(Range.between(0, 10))); - assertTrue("[0..10[ does not intersect with [15..15[", - !Range.between(0, 10).intersects(Range.between(15, 15))); - } - - @Test - public void subsetTest() { - assertTrue("[5..10[ is subset of [0..20[", Range.between(5, 10) - .isSubsetOf(Range.between(0, 20))); - - final Range range = Range.between(0, 10); - assertTrue("range is subset of self", range.isSubsetOf(range)); - - assertTrue("[0..10[ is not subset of [5..15[", !Range.between(0, 10) - .isSubsetOf(Range.between(5, 15))); - } - - @Test - public void offsetTest() { - assertEquals(Range.between(5, 15), Range.between(0, 10).offsetBy(5)); - } - - @Test - public void rangeStartsBeforeTest() { - final Range former = Range.between(0, 5); - final Range latter = Range.between(1, 5); - assertTrue("former should starts before latter", - former.startsBefore(latter)); - assertTrue("latter shouldn't start before latter", - !latter.startsBefore(former)); - - assertTrue("no overlap allowed", - !Range.between(0, 5).startsBefore(Range.between(0, 10))); - } - - @Test - public void rangeStartsAfterTest() { - final Range former = Range.between(0, 5); - final Range latter = Range.between(5, 10); - assertTrue("latter should start after former", - latter.startsAfter(former)); - assertTrue("former shouldn't start after latter", - !former.startsAfter(latter)); - - assertTrue("no overlap allowed", - !Range.between(5, 10).startsAfter(Range.between(0, 6))); - } - - @Test - public void rangeEndsBeforeTest() { - final Range former = Range.between(0, 5); - final Range latter = Range.between(5, 10); - assertTrue("latter should end before former", former.endsBefore(latter)); - assertTrue("former shouldn't end before latter", - !latter.endsBefore(former)); - - assertTrue("no overlap allowed", - !Range.between(5, 10).endsBefore(Range.between(9, 15))); - } - - @Test - public void rangeEndsAfterTest() { - final Range former = Range.between(1, 5); - final Range latter = Range.between(1, 6); - assertTrue("latter should end after former", latter.endsAfter(former)); - assertTrue("former shouldn't end after latter", - !former.endsAfter(latter)); - - assertTrue("no overlap allowed", - !Range.between(0, 10).endsAfter(Range.between(5, 10))); - } - - @Test(expected = IllegalArgumentException.class) - public void combine_notOverlappingFirstSmaller() { - Range.between(0, 10).combineWith(Range.between(11, 20)); - } - - @Test(expected = IllegalArgumentException.class) - public void combine_notOverlappingSecondLarger() { - Range.between(11, 20).combineWith(Range.between(0, 10)); - } - - @Test(expected = IllegalArgumentException.class) - public void combine_firstEmptyNotOverlapping() { - Range.between(15, 15).combineWith(Range.between(0, 10)); - } - - @Test(expected = IllegalArgumentException.class) - public void combine_secondEmptyNotOverlapping() { - Range.between(0, 10).combineWith(Range.between(15, 15)); - } - - @Test - public void combine_barelyOverlapping() { - Range r1 = Range.between(0, 10); - Range r2 = Range.between(10, 20); - - // Test both ways, should give the same result - Range combined1 = r1.combineWith(r2); - Range combined2 = r2.combineWith(r1); - assertEquals(combined1, combined2); - - assertEquals(0, combined1.getStart()); - assertEquals(20, combined1.getEnd()); - } - - @Test - public void combine_subRange() { - Range r1 = Range.between(0, 10); - Range r2 = Range.between(2, 8); - - // Test both ways, should give the same result - Range combined1 = r1.combineWith(r2); - Range combined2 = r2.combineWith(r1); - assertEquals(combined1, combined2); - - assertEquals(r1, combined1); - } - - @Test - public void combine_intersecting() { - Range r1 = Range.between(0, 10); - Range r2 = Range.between(5, 15); - - // Test both ways, should give the same result - Range combined1 = r1.combineWith(r2); - Range combined2 = r2.combineWith(r1); - assertEquals(combined1, combined2); - - assertEquals(0, combined1.getStart()); - assertEquals(15, combined1.getEnd()); - - } - - @Test - public void combine_emptyInside() { - Range r1 = Range.between(0, 10); - Range r2 = Range.between(5, 5); - - // Test both ways, should give the same result - Range combined1 = r1.combineWith(r2); - Range combined2 = r2.combineWith(r1); - assertEquals(combined1, combined2); - - assertEquals(r1, combined1); - } - -} diff --git a/server/src/com/vaadin/data/RpcDataProviderExtension.java b/server/src/com/vaadin/data/RpcDataProviderExtension.java index 48f03b98c0..b22e6a209b 100644 --- a/server/src/com/vaadin/data/RpcDataProviderExtension.java +++ b/server/src/com/vaadin/data/RpcDataProviderExtension.java @@ -18,6 +18,7 @@ package com.vaadin.data; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; import com.vaadin.data.Container.Indexed; @@ -67,22 +68,24 @@ public class RpcDataProviderExtension extends AbstractExtension { Collection propertyIds = container.getContainerPropertyIds(); List rows = new ArrayList(itemIds.size()); for (Object itemId : itemIds) { - Item item = container.getItem(itemId); - String[] row = new String[propertyIds.size()]; - - int i = 0; - for (Object propertyId : propertyIds) { - Object value = item.getItemProperty(propertyId).getValue(); - String stringValue = String.valueOf(value); - row[i++] = stringValue; - } - - rows.add(row); + rows.add(getRowData(propertyIds, itemId)); } - getRpcProxy(DataProviderRpc.class).setRowData(firstRow, rows); } + private String[] getRowData(Collection propertyIds, Object itemId) { + Item item = container.getItem(itemId); + String[] row = new String[propertyIds.size()]; + + int i = 0; + for (Object propertyId : propertyIds) { + Object value = item.getItemProperty(propertyId).getValue(); + String stringValue = String.valueOf(value); + row[i++] = stringValue; + } + return row; + } + @Override protected DataProviderState getState() { return (DataProviderState) super.getState(); @@ -98,4 +101,48 @@ public class RpcDataProviderExtension extends AbstractExtension { super.extend(component); } + /** + * Informs the client side that new rows have been inserted into the data + * source. + * + * @param index + * the index at which new rows have been inserted + * @param count + * the number of rows inserted at index + */ + public void insertRowData(int index, int count) { + getState().containerSize += count; + getRpcProxy(DataProviderRpc.class).insertRowData(index, count); + } + + /** + * Informs the client side that rows have been removed from the data source. + * + * @param firstIndex + * the index of the first row removed + * @param count + * the number of rows removed + */ + public void removeRowData(int firstIndex, int count) { + getState().containerSize -= count; + getRpcProxy(DataProviderRpc.class).removeRowData(firstIndex, count); + } + + /** + * Informs the client side that data of a row has been modified in the data + * source. + * + * @param index + * the index of the row that was updated + */ + public void updateRowData(int index) { + /* + * TODO: ignore duplicate requests for the same index during the same + * roundtrip. + */ + Object itemId = container.getIdByIndex(index); + String[] row = getRowData(container.getContainerPropertyIds(), itemId); + getRpcProxy(DataProviderRpc.class).setRowData(index, + Collections.singletonList(row)); + } } diff --git a/server/src/com/vaadin/ui/components/grid/Grid.java b/server/src/com/vaadin/ui/components/grid/Grid.java index 1fb0692104..08685874c1 100644 --- a/server/src/com/vaadin/ui/components/grid/Grid.java +++ b/server/src/com/vaadin/ui/components/grid/Grid.java @@ -26,15 +26,28 @@ import java.util.List; import java.util.Map; import com.vaadin.data.Container; +import com.vaadin.data.Container.Indexed.ItemAddEvent; +import com.vaadin.data.Container.Indexed.ItemRemoveEvent; +import com.vaadin.data.Container.ItemSetChangeEvent; +import com.vaadin.data.Container.ItemSetChangeListener; +import com.vaadin.data.Container.ItemSetChangeNotifier; import com.vaadin.data.Container.PropertySetChangeEvent; import com.vaadin.data.Container.PropertySetChangeListener; import com.vaadin.data.Container.PropertySetChangeNotifier; +import com.vaadin.data.Item; +import com.vaadin.data.Property; +import com.vaadin.data.Property.ValueChangeEvent; +import com.vaadin.data.Property.ValueChangeListener; +import com.vaadin.data.Property.ValueChangeNotifier; import com.vaadin.data.RpcDataProviderExtension; 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.GridServerRpc; import com.vaadin.shared.ui.grid.GridState; +import com.vaadin.shared.ui.grid.Range; import com.vaadin.ui.AbstractComponent; +import com.vaadin.ui.Component; /** * Data grid component @@ -56,6 +69,282 @@ import com.vaadin.ui.AbstractComponent; */ public class Grid extends AbstractComponent { + /** + * A helper class that handles the client-side Escalator logic relating to + * making sure that whatever is currently visible to the user, is properly + * initialized and otherwise handled on the server side (as far as + * requried). + *

+ * This bookeeping includes, but is not limited to: + *

    + *
  • listening to the currently visible {@link Property Properties'} value + * changes on the server side and sending those back to the client; and + *
  • attaching and detaching {@link Component Components} from the Vaadin + * Component hierarchy. + *
+ */ + private final class ActiveRowHandler { + /** + * A map from itemId to the value change listener used for all of its + * properties + */ + private final Map valueChangeListeners = new HashMap(); + + /** + * The currently active range. Practically, it's the range of row + * indices being displayed currently. + */ + private Range activeRange = Range.withLength(0, 0); + + /** + * A hook for making sure that appropriate data is "active". All other + * rows should be "inactive". + *

+ * "Active" can mean different things in different contexts. For + * example, only the Properties in the active range need + * ValueChangeListeners. Also, whenever a row with a Component becomes + * active, it needs to be attached (and conversely, when inactive, it + * needs to be detached). + * + * @param firstActiveRow + * the first active row + * @param activeRowCount + * the number of active rows + */ + public void setActiveRows(int firstActiveRow, int activeRowCount) { + + final Range newActiveRange = Range.withLength(firstActiveRow, + activeRowCount); + + // TODO [[Components]] attach and detach components + + /*- + * Example + * + * New Range: [3, 4, 5, 6, 7] + * Old Range: [1, 2, 3, 4, 5] + * Result: [1, 2][3, 4, 5] [] + */ + final Range[] depractionPartition = activeRange + .partitionWith(newActiveRange); + removeValueChangeListeners(depractionPartition[0]); + removeValueChangeListeners(depractionPartition[2]); + + /*- + * Example + * + * Old Range: [1, 2, 3, 4, 5] + * New Range: [3, 4, 5, 6, 7] + * Result: [] [3, 4, 5][6, 7] + */ + final Range[] activationPartition = newActiveRange + .partitionWith(activeRange); + addValueChangeListeners(activationPartition[0]); + addValueChangeListeners(activationPartition[2]); + + activeRange = newActiveRange; + } + + private void addValueChangeListeners(Range range) { + for (int i = range.getStart(); i < range.getEnd(); i++) { + + final Object itemId = datasource.getIdByIndex(i); + final Item item = datasource.getItem(itemId); + + if (valueChangeListeners.containsKey(itemId)) { + /* + * This might occur when items are removed from above the + * viewport, the escalator scrolls up to compensate, but the + * same items remain in the view: It looks as if one row was + * scrolled, when in fact the whole viewport was shifted up. + */ + continue; + } + + GridValueChangeListener listener = new GridValueChangeListener( + itemId); + valueChangeListeners.put(itemId, listener); + + for (final Object propertyId : item.getItemPropertyIds()) { + final Property property = item + .getItemProperty(propertyId); + if (property instanceof ValueChangeNotifier) { + ((ValueChangeNotifier) property) + .addValueChangeListener(listener); + } + } + } + } + + private void removeValueChangeListeners(Range range) { + for (int i = range.getStart(); i < range.getEnd(); i++) { + final Object itemId = datasource.getIdByIndex(i); + final Item item = datasource.getItem(itemId); + final GridValueChangeListener listener = valueChangeListeners + .remove(itemId); + + if (listener != null) { + for (final Object propertyId : item.getItemPropertyIds()) { + final Property property = item + .getItemProperty(propertyId); + + /* + * Because listener != null, we can be certain that this + * property is a ValueChangeNotifier: It wouldn't be + * inserted in addValueChangeListeners if the property + * wasn't a suitable type. I.e. No need for "instanceof" + * check. + */ + ((ValueChangeNotifier) property) + .removeValueChangeListener(listener); + } + } + } + } + + public void clear() { + removeValueChangeListeners(activeRange); + /* + * we're doing an assert for emptiness there (instead of a + * carte-blanche ".clear()"), to be absolutely sure that everything + * is cleaned up properly, and that we have no dangling listeners. + */ + assert valueChangeListeners.isEmpty() : "GridValueChangeListeners are leaking"; + + activeRange = Range.withLength(0, 0); + } + + /** + * Manages removed properties in active rows. + * + * @param removedPropertyIds + * the property ids that have been removed from the container + */ + public void propertiesRemoved(Collection removedPropertyIds) { + /* + * no-op, for now. + * + * The Container should be responsible for cleaning out any + * ValueChangeListeners from removed Properties. Components will + * benefit from this, however. + */ + } + + /** + * Manages added properties in active rows. + * + * @param addedPropertyIds + * the property ids that have been added to the container + */ + public void propertiesAdded(Collection addedPropertyIds) { + for (int i = activeRange.getStart(); i < activeRange.getEnd(); i++) { + final Object itemId = datasource.getIdByIndex(i); + final Item item = datasource.getItem(itemId); + final GridValueChangeListener listener = valueChangeListeners + .get(itemId); + assert (listener != null) : "a listener should've been pre-made by addValueChangeListeners"; + + for (final Object propertyId : addedPropertyIds) { + final Property property = item + .getItemProperty(propertyId); + if (property instanceof ValueChangeNotifier) { + ((ValueChangeNotifier) property) + .addValueChangeListener(listener); + } + } + } + } + + /** + * Handles the insertion of rows. + *

+ * This method's responsibilities are to: + *

    + *
  • shift the internal bookkeeping by count if the + * insertion happens above currently active range + *
  • ignore rows inserted below the currently active range + *
  • shift (and deactivate) rows pushed out of view + *
  • activate rows that are inserted in the current viewport + *
+ * + * @param firstIndex + * the index of the first inserted rows + * @param count + * the number of rows inserted at firstIndex + */ + public void insertRows(int firstIndex, int count) { + if (firstIndex < activeRange.getStart()) { + activeRange = activeRange.offsetBy(count); + } else if (firstIndex < activeRange.getEnd()) { + final Range deprecatedRange = Range.withLength( + activeRange.getEnd(), count); + removeValueChangeListeners(deprecatedRange); + + final Range freshRange = Range.between(firstIndex, count); + addValueChangeListeners(freshRange); + } else { + // out of view, noop + } + } + + /** + * Removes a single item by its id. + * + * @param itemId + * the id of the removed id. Note: this item does + * not exist anymore in the datasource + */ + public void removeItemId(Object itemId) { + final GridValueChangeListener removedListener = valueChangeListeners + .remove(itemId); + if (removedListener != null) { + /* + * We removed an item from somewhere in the visible range, so we + * make the active range shorter. The empty hole will be filled + * by the client-side code when it asks for more information. + */ + activeRange = Range.withLength(activeRange.getStart(), + activeRange.length() - 1); + } + } + } + + /** + * A class to listen to changes in property values in the Container added + * with {@link Grid#setContainerDatasource(Container.Indexed)}, and notifies + * the data source to update the client-side representation of the modified + * item. + *

+ * One instance of this class can (and should) be reused for all the + * properties in an item, since this class will inform that the entire row + * needs to be re-evaluated (in contrast to a property-based change + * management) + *

+ * Since there's no Container-wide possibility to listen to any kind of + * value changes, an instance of this class needs to be attached to each and + * every Item's Property in the container. + * + * @see Grid#addValueChangeListener(Container, Object, Object) + * @see Grid#valueChangeListeners + */ + private class GridValueChangeListener implements ValueChangeListener { + private final Object itemId; + + public GridValueChangeListener(Object itemId) { + /* + * Using an assert instead of an exception throw, just to optimize + * prematurely + */ + assert itemId != null : "null itemId not accepted"; + this.itemId = itemId; + } + + @Override + public void valueChange(ValueChangeEvent event) { + datasourceExtension.updateRowData(datasource.indexOfId(itemId)); + } + } + /** * The data source attached to the grid */ @@ -98,13 +387,17 @@ public class Grid extends AbstractComponent { columnKeys.remove(columnId); getState().columns.remove(column.getState()); } + activeRowHandler.propertiesRemoved(removedColumns); // Add new columns + HashSet addedPropertyIds = new HashSet(); for (Object propertyId : properties) { if (!columns.containsKey(propertyId)) { appendColumn(propertyId); + addedPropertyIds.add(propertyId); } } + activeRowHandler.propertiesAdded(addedPropertyIds); Object frozenPropertyId = columnKeys .get(getState(false).lastFrozenColumnId); @@ -114,8 +407,53 @@ public class Grid extends AbstractComponent { } }; + private ItemSetChangeListener itemListener = new ItemSetChangeListener() { + @Override + public void containerItemSetChange(ItemSetChangeEvent event) { + + if (event instanceof ItemAddEvent) { + ItemAddEvent addEvent = (ItemAddEvent) event; + int firstIndex = addEvent.getFirstIndex(); + int count = addEvent.getAddedItemsCount(); + datasourceExtension.insertRowData(firstIndex, count); + activeRowHandler.insertRows(firstIndex, count); + } + + else if (event instanceof ItemRemoveEvent) { + ItemRemoveEvent removeEvent = (ItemRemoveEvent) event; + int firstIndex = removeEvent.getFirstIndex(); + int count = removeEvent.getRemovedItemsCount(); + datasourceExtension.removeRowData(firstIndex, count); + + /* + * Unfortunately, there's no sane way of getting the rest of the + * removed itemIds. + * + * Fortunately, the only time _currently_ an event with more + * than one removed item seems to be when calling + * AbstractInMemoryContainer.removeAllElements(). Otherwise, + * it's only removing one item at a time. + * + * We _could_ have a backup of all the itemIds, and compare to + * that one, but we really really don't want to go there. + */ + activeRowHandler.removeItemId(removeEvent.getFirstItemId()); + } + + else { + // TODO no diff info available, redraw everything + throw new UnsupportedOperationException("bare " + + "ItemSetChangeEvents are currently " + + "not supported, use a container that " + + "uses AddItemEvents and RemoveItemEvents."); + } + } + }; + private RpcDataProviderExtension datasourceExtension; + private final ActiveRowHandler activeRowHandler = new ActiveRowHandler(); + /** * Creates a new Grid using the given datasource. * @@ -124,6 +462,14 @@ public class Grid extends AbstractComponent { */ public Grid(Container.Indexed datasource) { setContainerDatasource(datasource); + + registerRpc(new GridServerRpc() { + @Override + public void setVisibleRows(int firstVisibleRow, int visibleRowCount) { + activeRowHandler + .setActiveRows(firstVisibleRow, visibleRowCount); + } + }); } /** @@ -143,11 +489,16 @@ public class Grid extends AbstractComponent { return; } - // Remove old listener + // Remove old listeners if (datasource instanceof PropertySetChangeNotifier) { ((PropertySetChangeNotifier) datasource) .removePropertySetChangeListener(propertyListener); } + if (datasource instanceof ItemSetChangeNotifier) { + ((ItemSetChangeNotifier) datasource) + .removeItemSetChangeListener(itemListener); + } + activeRowHandler.clear(); if (datasourceExtension != null) { removeExtension(datasourceExtension); @@ -162,6 +513,15 @@ public class Grid extends AbstractComponent { ((PropertySetChangeNotifier) datasource) .addPropertySetChangeListener(propertyListener); } + if (datasource instanceof ItemSetChangeNotifier) { + ((ItemSetChangeNotifier) datasource) + .addItemSetChangeListener(itemListener); + } + /* + * activeRowHandler will be updated by the client-side request that + * occurs on container change - no need to actively re-insert any + * ValueChangeListeners at this point. + */ getState().columns.clear(); setLastFrozenPropertyId(null); diff --git a/shared/src/com/vaadin/shared/data/DataProviderRpc.java b/shared/src/com/vaadin/shared/data/DataProviderRpc.java index 7d82ecc342..79e3f17f8d 100644 --- a/shared/src/com/vaadin/shared/data/DataProviderRpc.java +++ b/shared/src/com/vaadin/shared/data/DataProviderRpc.java @@ -37,4 +37,25 @@ public interface DataProviderRpc extends ClientRpc { * the updated row data */ public void setRowData(int firstRowIndex, List rowData); + + /** + * Informs the client to remove row data. + * + * @param firstRowIndex + * the index of the first removed row + * @param count + * the number of rows removed from firstRowIndex and + * onwards + */ + public void removeRowData(int firstRowIndex, int count); + + /** + * Informs the client to insert new row data. + * + * @param firstRowIndex + * the index of the first new row + * @param count + * the number of rows inserted at firstRowIndex + */ + public void insertRowData(int firstRowIndex, int count); } diff --git a/shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java b/shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java new file mode 100644 index 0000000000..db0a31ed2c --- /dev/null +++ b/shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java @@ -0,0 +1,39 @@ +/* + * 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 com.vaadin.shared.communication.ServerRpc; + +/** + * TODO + * + * @since 7.2 + * @author Vaadin Ltd + */ +public interface GridServerRpc extends ServerRpc { + + /** + * TODO + * + * @param firstVisibleRow + * the index of the first visible row + * @param visibleRowCount + * the number of rows visible, counted from + * firstVisibleRow + */ + void setVisibleRows(int firstVisibleRow, int visibleRowCount); + +} diff --git a/shared/src/com/vaadin/shared/ui/grid/Range.java b/shared/src/com/vaadin/shared/ui/grid/Range.java new file mode 100644 index 0000000000..3114a79c82 --- /dev/null +++ b/shared/src/com/vaadin/shared/ui/grid/Range.java @@ -0,0 +1,378 @@ +/* + * 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; + +/** + * An immutable representation of a range, marked by start and end points. + *

+ * The range is treated as inclusive at the start, and exclusive at the end. + * I.e. the range [0..1[ has the length 1, and represents one integer: 0. + *

+ * The range is considered {@link #isEmpty() empty} if the start is the same as + * the end. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public final class Range { + private final int start; + private final int end; + + /** + * Creates a range object representing a single integer. + * + * @param integer + * the number to represent as a range + * @return the range represented by integer + */ + public static Range withOnly(final int integer) { + return new Range(integer, integer + 1); + } + + /** + * Creates a range between two integers. + *

+ * The range start is inclusive and the end is exclusive. + * So, a range "between" 0 and 5 represents the numbers 0, 1, 2, 3 and 4, + * but not 5. + * + * @param start + * the start of the the range, inclusive + * @param end + * the end of the range, exclusive + * @return a range representing [start..end[ + * @throws IllegalArgumentException + * if start > end + */ + public static Range between(final int start, final int end) + throws IllegalArgumentException { + return new Range(start, end); + } + + /** + * Creates a range from a start point, with a given length. + * + * @param start + * the first integer to include in the range + * @param length + * the length of the resulting range + * @return a range starting from start, with + * length number of integers following + * @throws IllegalArgumentException + * if length < 0 + */ + public static Range withLength(final int start, final int length) + throws IllegalArgumentException { + if (length < 0) { + /* + * The constructor of Range will throw an exception if start > + * start+length (i.e. if length is negative). We're throwing the + * same exception type, just with a more descriptive message. + */ + throw new IllegalArgumentException("length must not be negative"); + } + return new Range(start, start + length); + } + + /** + * Creates a new range between two numbers: [start..end[. + * + * @param start + * the start integer, inclusive + * @param end + * the end integer, exclusive + * @throws IllegalArgumentException + * if start > end + */ + private Range(final int start, final int end) + throws IllegalArgumentException { + if (start > end) { + throw new IllegalArgumentException( + "start must not be greater than end"); + } + + this.start = start; + this.end = end; + } + + /** + * Returns the inclusive start point of this range. + * + * @return the start point of this range + */ + public int getStart() { + return start; + } + + /** + * Returns the exclusive end point of this range. + * + * @return the end point of this range + */ + public int getEnd() { + return end; + } + + /** + * The number of integers contained in the range. + * + * @return the number of integers contained in the range + */ + public int length() { + return getEnd() - getStart(); + } + + /** + * Checks whether the range has no elements between the start and end. + * + * @return true iff the range contains no elements. + */ + public boolean isEmpty() { + return getStart() >= getEnd(); + } + + /** + * Checks whether this range and another range are at least partially + * covering the same values. + * + * @param other + * the other range to check against + * @return true if this and other intersect + */ + public boolean intersects(final Range other) { + return getStart() < other.getEnd() && other.getStart() < getEnd(); + } + + /** + * Checks whether an integer is found within this range. + * + * @param integer + * an integer to test for presence in this range + * @return true iff integer is in this range + */ + public boolean contains(final int integer) { + return getStart() <= integer && integer < getEnd(); + } + + /** + * Checks whether this range is a subset of another range. + * + * @return true iff other completely wraps this + * range + */ + public boolean isSubsetOf(final Range other) { + return other.getStart() <= getStart() && getEnd() <= other.getEnd(); + } + + /** + * Overlay this range with another one, and partition the ranges according + * to how they position relative to each other. + *

+ * The three partitions are returned as a three-element Range array: + *

    + *
  • Elements in this range that occur before elements in + * other. + *
  • Elements that are shared between the two ranges. + *
  • Elements in this range that occur after elements in + * other. + *
+ * + * @param other + * the other range to act as delimiters. + * @return a three-element Range array of partitions depicting the elements + * before (index 0), shared/inside (index 1) and after (index 2). + */ + public Range[] partitionWith(final Range other) { + final Range[] splitBefore = splitAt(other.getStart()); + final Range rangeBefore = splitBefore[0]; + final Range[] splitAfter = splitBefore[1].splitAt(other.getEnd()); + final Range rangeInside = splitAfter[0]; + final Range rangeAfter = splitAfter[1]; + return new Range[] { rangeBefore, rangeInside, rangeAfter }; + } + + /** + * Get a range that is based on this one, but offset by a number. + * + * @param offset + * the number to offset by + * @return a copy of this range, offset by offset + */ + public Range offsetBy(final int offset) { + if (offset == 0) { + return this; + } else { + return new Range(start + offset, end + offset); + } + } + + @Override + public String toString() { + return getClass().getSimpleName() + " [" + getStart() + ".." + getEnd() + + "[" + (isEmpty() ? " (empty)" : ""); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + end; + result = prime * result + start; + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Range other = (Range) obj; + if (end != other.end) { + return false; + } + if (start != other.start) { + return false; + } + return true; + } + + /** + * Checks whether this range starts before the start of another range. + * + * @param other + * the other range to compare against + * @return true iff this range starts before the + * other + */ + public boolean startsBefore(final Range other) { + return getStart() < other.getStart(); + } + + /** + * Checks whether this range ends before the start of another range. + * + * @param other + * the other range to compare against + * @return true iff this range ends before the + * other + */ + public boolean endsBefore(final Range other) { + return getEnd() <= other.getStart(); + } + + /** + * Checks whether this range ends after the end of another range. + * + * @param other + * the other range to compare against + * @return true iff this range ends after the + * other + */ + public boolean endsAfter(final Range other) { + return getEnd() > other.getEnd(); + } + + /** + * Checks whether this range starts after the end of another range. + * + * @param other + * the other range to compare against + * @return true iff this range starts after the + * other + */ + public boolean startsAfter(final Range other) { + return getStart() >= other.getEnd(); + } + + /** + * Split the range into two at a certain integer. + *

+ * Example: [5..10[.splitAt(7) == [5..7[, [7..10[ + * + * @param integer + * the integer at which to split the range into two + * @return an array of two ranges, with [start..integer[ in the + * first element, and [integer..end[ in the second + * element. + *

+ * If {@code integer} is less than {@code start}, [empty, + * {@code this} ] is returned. if integer is equal to + * or greater than {@code end}, [{@code this}, empty] is returned + * instead. + */ + public Range[] splitAt(final int integer) { + if (integer < start) { + return new Range[] { Range.withLength(start, 0), this }; + } else if (integer >= end) { + return new Range[] { this, Range.withLength(end, 0) }; + } else { + return new Range[] { new Range(start, integer), + new Range(integer, end) }; + } + } + + /** + * Split the range into two after a certain number of integers into the + * range. + *

+ * Calling this method is equivalent to calling + * {@link #splitAt(int) splitAt}({@link #getStart()}+length); + *

+ * Example: + * [5..10[.splitAtFromStart(2) == [5..7[, [7..10[ + * + * @param length + * the length at which to split this range into two + * @return an array of two ranges, having the length-first + * elements of this range, and the second range having the rest. If + * length ≤ 0, the first element will be empty, and + * the second element will be this range. If length + * ≥ {@link #length()}, the first element will be this range, + * and the second element will be empty. + */ + public Range[] splitAtFromStart(final int length) { + return splitAt(getStart() + length); + } + + /** + * Combines two ranges to create a range containing all values in both + * ranges, provided there are no gaps between the ranges. + * + * @param other + * the range to combine with this range + * + * @return the combined range + * + * @throws IllegalArgumentException + * if the two ranges aren't connected + */ + public Range combineWith(Range other) throws IllegalArgumentException { + if (getStart() > other.getEnd() || other.getStart() > getEnd()) { + throw new IllegalArgumentException("There is a gap between " + this + + " and " + other); + } + + return Range.between(Math.min(getStart(), other.getStart()), + Math.max(getEnd(), other.getEnd())); + } +} diff --git a/shared/tests/src/com/vaadin/shared/ui/grid/RangeTest.java b/shared/tests/src/com/vaadin/shared/ui/grid/RangeTest.java new file mode 100644 index 0000000000..b042cee509 --- /dev/null +++ b/shared/tests/src/com/vaadin/shared/ui/grid/RangeTest.java @@ -0,0 +1,318 @@ +/* + * 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +@SuppressWarnings("static-method") +public class RangeTest { + + @Test(expected = IllegalArgumentException.class) + public void startAfterEndTest() { + Range.between(10, 9); + } + + @Test(expected = IllegalArgumentException.class) + public void negativeLengthTest() { + Range.withLength(10, -1); + } + + @Test + public void constructorEquivalenceTest() { + assertEquals("10 == [10,11[", Range.withOnly(10), Range.between(10, 11)); + assertEquals("[10,20[ == 10, length 10", Range.between(10, 20), + Range.withLength(10, 10)); + assertEquals("10 == 10, length 1", Range.withOnly(10), + Range.withLength(10, 1)); + } + + @Test + public void boundsTest() { + { + final Range range = Range.between(0, 10); + assertEquals("between(0, 10) start", 0, range.getStart()); + assertEquals("between(0, 10) end", 10, range.getEnd()); + } + + { + final Range single = Range.withOnly(10); + assertEquals("withOnly(10) start", 10, single.getStart()); + assertEquals("withOnly(10) end", 11, single.getEnd()); + } + + { + final Range length = Range.withLength(10, 5); + assertEquals("withLength(10, 5) start", 10, length.getStart()); + assertEquals("withLength(10, 5) end", 15, length.getEnd()); + } + } + + @Test + @SuppressWarnings("boxing") + public void equalsTest() { + final Range range1 = Range.between(0, 10); + final Range range2 = Range.withLength(0, 11); + + assertTrue("null", !range1.equals(null)); + assertTrue("reflexive", range1.equals(range1)); + assertEquals("symmetric", range1.equals(range2), range2.equals(range1)); + } + + @Test + public void containsTest() { + final int start = 0; + final int end = 10; + final Range range = Range.between(start, end); + + assertTrue("start should be contained", range.contains(start)); + assertTrue("start-1 should not be contained", + !range.contains(start - 1)); + assertTrue("end should not be contained", !range.contains(end)); + assertTrue("end-1 should be contained", range.contains(end - 1)); + + assertTrue("[0..10[ contains 5", Range.between(0, 10).contains(5)); + assertTrue("empty range does not contain 5", !Range.between(5, 5) + .contains(5)); + } + + @Test + public void emptyTest() { + assertTrue("[0..0[ should be empty", Range.between(0, 0).isEmpty()); + assertTrue("Range of length 0 should be empty", Range.withLength(0, 0) + .isEmpty()); + + assertTrue("[0..1[ should not be empty", !Range.between(0, 1).isEmpty()); + assertTrue("Range of length 1 should not be empty", + !Range.withLength(0, 1).isEmpty()); + } + + @Test + public void splitTest() { + final Range startRange = Range.between(0, 10); + final Range[] splitRanges = startRange.splitAt(5); + assertEquals("[0..10[ split at 5, lower", Range.between(0, 5), + splitRanges[0]); + assertEquals("[0..10[ split at 5, upper", Range.between(5, 10), + splitRanges[1]); + } + + @Test + public void split_valueBefore() { + Range range = Range.between(10, 20); + Range[] splitRanges = range.splitAt(5); + + assertEquals(Range.between(10, 10), splitRanges[0]); + assertEquals(range, splitRanges[1]); + } + + @Test + public void split_valueAfter() { + Range range = Range.between(10, 20); + Range[] splitRanges = range.splitAt(25); + + assertEquals(range, splitRanges[0]); + assertEquals(Range.between(20, 20), splitRanges[1]); + } + + @Test + public void emptySplitTest() { + final Range range = Range.between(5, 10); + final Range[] split1 = range.splitAt(0); + assertTrue("split1, [0]", split1[0].isEmpty()); + assertEquals("split1, [1]", range, split1[1]); + + final Range[] split2 = range.splitAt(15); + assertEquals("split2, [0]", range, split2[0]); + assertTrue("split2, [1]", split2[1].isEmpty()); + } + + @Test + public void lengthTest() { + assertEquals("withLength length", 5, Range.withLength(10, 5).length()); + assertEquals("between length", 5, Range.between(10, 15).length()); + assertEquals("withOnly 10 length", 1, Range.withOnly(10).length()); + } + + @Test + public void intersectsTest() { + assertTrue("[0..10[ intersects [5..15[", Range.between(0, 10) + .intersects(Range.between(5, 15))); + assertTrue("[0..10[ does not intersect [10..20[", !Range.between(0, 10) + .intersects(Range.between(10, 20))); + } + + @Test + public void intersects_emptyInside() { + assertTrue("[5..5[ does intersect with [0..10[", Range.between(5, 5) + .intersects(Range.between(0, 10))); + assertTrue("[0..10[ does intersect with [5..5[", Range.between(0, 10) + .intersects(Range.between(5, 5))); + } + + @Test + public void intersects_emptyOutside() { + assertTrue("[15..15[ does not intersect with [0..10[", + !Range.between(15, 15).intersects(Range.between(0, 10))); + assertTrue("[0..10[ does not intersect with [15..15[", + !Range.between(0, 10).intersects(Range.between(15, 15))); + } + + @Test + public void subsetTest() { + assertTrue("[5..10[ is subset of [0..20[", Range.between(5, 10) + .isSubsetOf(Range.between(0, 20))); + + final Range range = Range.between(0, 10); + assertTrue("range is subset of self", range.isSubsetOf(range)); + + assertTrue("[0..10[ is not subset of [5..15[", !Range.between(0, 10) + .isSubsetOf(Range.between(5, 15))); + } + + @Test + public void offsetTest() { + assertEquals(Range.between(5, 15), Range.between(0, 10).offsetBy(5)); + } + + @Test + public void rangeStartsBeforeTest() { + final Range former = Range.between(0, 5); + final Range latter = Range.between(1, 5); + assertTrue("former should starts before latter", + former.startsBefore(latter)); + assertTrue("latter shouldn't start before latter", + !latter.startsBefore(former)); + + assertTrue("no overlap allowed", + !Range.between(0, 5).startsBefore(Range.between(0, 10))); + } + + @Test + public void rangeStartsAfterTest() { + final Range former = Range.between(0, 5); + final Range latter = Range.between(5, 10); + assertTrue("latter should start after former", + latter.startsAfter(former)); + assertTrue("former shouldn't start after latter", + !former.startsAfter(latter)); + + assertTrue("no overlap allowed", + !Range.between(5, 10).startsAfter(Range.between(0, 6))); + } + + @Test + public void rangeEndsBeforeTest() { + final Range former = Range.between(0, 5); + final Range latter = Range.between(5, 10); + assertTrue("latter should end before former", former.endsBefore(latter)); + assertTrue("former shouldn't end before latter", + !latter.endsBefore(former)); + + assertTrue("no overlap allowed", + !Range.between(5, 10).endsBefore(Range.between(9, 15))); + } + + @Test + public void rangeEndsAfterTest() { + final Range former = Range.between(1, 5); + final Range latter = Range.between(1, 6); + assertTrue("latter should end after former", latter.endsAfter(former)); + assertTrue("former shouldn't end after latter", + !former.endsAfter(latter)); + + assertTrue("no overlap allowed", + !Range.between(0, 10).endsAfter(Range.between(5, 10))); + } + + @Test(expected = IllegalArgumentException.class) + public void combine_notOverlappingFirstSmaller() { + Range.between(0, 10).combineWith(Range.between(11, 20)); + } + + @Test(expected = IllegalArgumentException.class) + public void combine_notOverlappingSecondLarger() { + Range.between(11, 20).combineWith(Range.between(0, 10)); + } + + @Test(expected = IllegalArgumentException.class) + public void combine_firstEmptyNotOverlapping() { + Range.between(15, 15).combineWith(Range.between(0, 10)); + } + + @Test(expected = IllegalArgumentException.class) + public void combine_secondEmptyNotOverlapping() { + Range.between(0, 10).combineWith(Range.between(15, 15)); + } + + @Test + public void combine_barelyOverlapping() { + Range r1 = Range.between(0, 10); + Range r2 = Range.between(10, 20); + + // Test both ways, should give the same result + Range combined1 = r1.combineWith(r2); + Range combined2 = r2.combineWith(r1); + assertEquals(combined1, combined2); + + assertEquals(0, combined1.getStart()); + assertEquals(20, combined1.getEnd()); + } + + @Test + public void combine_subRange() { + Range r1 = Range.between(0, 10); + Range r2 = Range.between(2, 8); + + // Test both ways, should give the same result + Range combined1 = r1.combineWith(r2); + Range combined2 = r2.combineWith(r1); + assertEquals(combined1, combined2); + + assertEquals(r1, combined1); + } + + @Test + public void combine_intersecting() { + Range r1 = Range.between(0, 10); + Range r2 = Range.between(5, 15); + + // Test both ways, should give the same result + Range combined1 = r1.combineWith(r2); + Range combined2 = r2.combineWith(r1); + assertEquals(combined1, combined2); + + assertEquals(0, combined1.getStart()); + assertEquals(15, combined1.getEnd()); + + } + + @Test + public void combine_emptyInside() { + Range r1 = Range.between(0, 10); + Range r2 = Range.between(5, 5); + + // Test both ways, should give the same result + Range combined1 = r1.combineWith(r2); + Range combined2 = r2.combineWith(r1); + assertEquals(combined1, combined2); + + assertEquals(r1, combined1); + } + +} diff --git a/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java b/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java index 82b2d7a4e8..c28feb8d10 100644 --- a/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java +++ b/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java @@ -40,20 +40,22 @@ public class GridBasicFeatures extends AbstractComponentTest { private final int ROWS = 1000; + private IndexedContainer ds; + @Override protected Grid constructComponent() { // Build data source - IndexedContainer ds = new IndexedContainer(); + ds = new IndexedContainer(); for (int col = 0; col < COLUMNS; col++) { - ds.addContainerProperty("Column" + col, String.class, ""); + ds.addContainerProperty(getColumnProperty(col), String.class, ""); } for (int row = 0; row < ROWS; row++) { Item item = ds.addItem(Integer.valueOf(row)); for (int col = 0; col < COLUMNS; col++) { - item.getItemProperty("Column" + col).setValue( + item.getItemProperty(getColumnProperty(col)).setValue( "(" + row + ", " + col + ")"); } } @@ -63,7 +65,8 @@ public class GridBasicFeatures extends AbstractComponentTest { // Add footer values (header values are automatically created) for (int col = 0; col < COLUMNS; col++) { - grid.getColumn("Column" + col).setFooterCaption("Footer " + col); + grid.getColumn(getColumnProperty(col)).setFooterCaption( + "Footer " + col); } // Set varying column widths @@ -81,6 +84,8 @@ public class GridBasicFeatures extends AbstractComponentTest { createColumnGroupActions(); + createRowActions(); + return grid; } @@ -131,9 +136,9 @@ public class GridBasicFeatures extends AbstractComponentTest { createCategory("Columns", null); for (int c = 0; c < COLUMNS; c++) { - createCategory("Column" + c, "Columns"); + createCategory(getColumnProperty(c), "Columns"); - createBooleanAction("Visible", "Column" + c, true, + createBooleanAction("Visible", getColumnProperty(c), true, new Command() { @Override @@ -148,7 +153,7 @@ public class GridBasicFeatures extends AbstractComponentTest { } }, c); - createClickAction("Remove", "Column" + c, + createClickAction("Remove", getColumnProperty(c), new Command() { @Override @@ -158,7 +163,7 @@ public class GridBasicFeatures extends AbstractComponentTest { } }, null, c); - createClickAction("Freeze", "Column" + c, + createClickAction("Freeze", getColumnProperty(c), new Command() { @Override @@ -167,7 +172,7 @@ public class GridBasicFeatures extends AbstractComponentTest { } }, null, c); - createCategory("Column" + c + " Width", "Column" + c); + createCategory("Column" + c + " Width", getColumnProperty(c)); createClickAction("Auto", "Column" + c + " Width", new Command() { @@ -203,6 +208,10 @@ public class GridBasicFeatures extends AbstractComponentTest { } } + private static String getColumnProperty(int c) { + return "Column" + c; + } + protected void createColumnGroupActions() { createCategory("Column groups", null); @@ -269,6 +278,58 @@ public class GridBasicFeatures extends AbstractComponentTest { } + protected void createRowActions() { + createCategory("Body rows", null); + + createClickAction("Add first row", "Body rows", + new Command() { + @Override + public void execute(Grid c, String value, Object data) { + Item item = ds.addItemAt(0, new Object()); + for (int i = 0; i < COLUMNS; i++) { + item.getItemProperty(getColumnProperty(i)) + .setValue("newcell: " + i); + } + } + }, null); + + createClickAction("Remove first row", "Body rows", + new Command() { + @Override + public void execute(Grid c, String value, Object data) { + Object firstItemId = ds.getIdByIndex(0); + ds.removeItem(firstItemId); + } + }, null); + + createClickAction("Modify first row (getItemProperty)", "Body rows", + new Command() { + @Override + public void execute(Grid c, String value, Object data) { + Object firstItemId = ds.getIdByIndex(0); + Item item = ds.getItem(firstItemId); + for (int i = 0; i < COLUMNS; i++) { + item.getItemProperty(getColumnProperty(i)) + .setValue("modified: " + i); + } + } + }, null); + + createClickAction("Modify first row (getContainerProperty)", + "Body rows", new Command() { + @Override + public void execute(Grid c, String value, Object data) { + Object firstItemId = ds.getIdByIndex(0); + for (Object containerPropertyId : ds + .getContainerPropertyIds()) { + ds.getContainerProperty(firstItemId, + containerPropertyId).setValue( + "modified: " + containerPropertyId); + } + } + }, null); + } + @Override protected Integer getTicketNumber() { return 12829; diff --git a/uitest/src/com/vaadin/tests/components/grid/GridBasicFeaturesTest.java b/uitest/src/com/vaadin/tests/components/grid/GridBasicFeaturesTest.java index 8beee46156..bc43f2be98 100644 --- a/uitest/src/com/vaadin/tests/components/grid/GridBasicFeaturesTest.java +++ b/uitest/src/com/vaadin/tests/components/grid/GridBasicFeaturesTest.java @@ -22,6 +22,7 @@ import java.util.List; import org.junit.Test; import org.openqa.selenium.By; +import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.WebElement; import org.openqa.selenium.interactions.Actions; @@ -253,6 +254,55 @@ public class GridBasicFeaturesTest extends MultiBrowserTest { assertPrimaryStylename("v-grid"); } + /** + * Test that the current view is updated when a server-side container change + * occurs (without scrolling back and forth) + */ + @Test + public void testItemSetChangeEvent() throws Exception { + openTestURL(); + + final By newRow = By.xpath("//td[text()='newcell: 0']"); + + assertTrue("Unexpected initial state", !elementIsFound(newRow)); + + selectMenuPath("Component", "Body rows", "Add first row"); + assertTrue("Add row failed", elementIsFound(newRow)); + + selectMenuPath("Component", "Body rows", "Remove first row"); + assertTrue("Remove row failed", !elementIsFound(newRow)); + } + + /** + * Test that the current view is updated when a property's value is reflect + * to the client, when the value is modified server-side. + */ + @Test + public void testPropertyValueChangeEvent() throws Exception { + openTestURL(); + + assertEquals("Unexpected cell initial state", "(0, 0)", + getBodyCellByRowAndColumn(1, 1).getText()); + + selectMenuPath("Component", "Body rows", + "Modify first row (getItemProperty)"); + assertEquals("(First) modification with getItemProperty failed", + "modified: 0", getBodyCellByRowAndColumn(1, 1).getText()); + + selectMenuPath("Component", "Body rows", + "Modify first row (getContainerProperty)"); + assertEquals("(Second) modification with getItemProperty failed", + "modified: Column0", getBodyCellByRowAndColumn(1, 1).getText()); + } + + private boolean elementIsFound(By locator) { + try { + return driver.findElement(locator) != null; + } catch (NoSuchElementException e) { + return false; + } + } + private void assertPrimaryStylename(String stylename) { assertTrue(getGridElement().getAttribute("class").contains(stylename)); -- cgit v1.2.3 From d2027d8344313b048bf3b82e2a988ab40b0bb596 Mon Sep 17 00:00:00 2001 From: Patrik Lindström Date: Tue, 11 Feb 2014 15:36:24 +0200 Subject: Implement programmatic scrolling (#13327) Further changes required for this, included in the same patch: - created GridClientRpc interface - created test case UI for server-side controlled Grid programmatic scrolling - refactored getScrollPos logic into Escalator and moved ScrollDestination enum to shared package Change-Id: Ibf72a4f75831807d83fb5941597a6ce3fda08e17 --- .../src/com/vaadin/client/ui/grid/Escalator.java | 143 +++++++++++++-------- client/src/com/vaadin/client/ui/grid/Grid.java | 90 +++++++++++++ .../com/vaadin/client/ui/grid/GridConnector.java | 19 +++ .../vaadin/client/ui/grid/ScrollDestination.java | 102 --------------- .../vaadin/shared/ui/grid/ScrollDestination.java | 55 ++++++++ server/src/com/vaadin/ui/components/grid/Grid.java | 54 ++++++++ .../com/vaadin/shared/ui/grid/GridClientRpc.java | 53 ++++++++ .../com/vaadin/shared/ui/grid/GridConstants.java | 33 +++++ .../tests/components/grid/GridScrolling.java | 115 +++++++++++++++++ .../widgetset/client/grid/TestGridConnector.java | 2 +- .../tests/widgetset/client/grid/VTestGrid.java | 14 +- 11 files changed, 509 insertions(+), 171 deletions(-) delete mode 100644 client/src/com/vaadin/client/ui/grid/ScrollDestination.java create mode 100644 client/src/com/vaadin/shared/ui/grid/ScrollDestination.java create mode 100644 shared/src/com/vaadin/shared/ui/grid/GridClientRpc.java create mode 100644 shared/src/com/vaadin/shared/ui/grid/GridConstants.java create mode 100644 uitest/src/com/vaadin/tests/components/grid/GridScrolling.java (limited to 'shared') diff --git a/client/src/com/vaadin/client/ui/grid/Escalator.java b/client/src/com/vaadin/client/ui/grid/Escalator.java index a1d895c2dd..77a8c2dbd9 100644 --- a/client/src/com/vaadin/client/ui/grid/Escalator.java +++ b/client/src/com/vaadin/client/ui/grid/Escalator.java @@ -52,6 +52,7 @@ import com.vaadin.client.ui.grid.PositionFunction.WebkitTranslate3DPosition; import com.vaadin.client.ui.grid.ScrollbarBundle.HorizontalScrollbarBundle; import com.vaadin.client.ui.grid.ScrollbarBundle.VerticalScrollbarBundle; import com.vaadin.shared.ui.grid.Range; +import com.vaadin.shared.ui.grid.ScrollDestination; import com.vaadin.shared.util.SharedUtil; /*- @@ -548,6 +549,80 @@ public class Escalator extends Widget { private static final int ROW_HEIGHT_PX = 20; + /** + * ScrollDestination case-specific handling logic. + */ + private static double getScrollPos(final ScrollDestination destination, + final double targetStartPx, final double targetEndPx, + final double viewportStartPx, final double viewportEndPx, + final int padding) { + + final double viewportLength = viewportEndPx - viewportStartPx; + + switch (destination) { + + /* + * Scroll as little as possible to show the target element. If the + * element fits into view, this works as START or END depending on the + * current scroll position. If the element does not fit into view, this + * works as START. + */ + case ANY: { + final double startScrollPos = targetStartPx - padding; + final double endScrollPos = targetEndPx + padding - viewportLength; + + if (startScrollPos < viewportStartPx) { + return startScrollPos; + } else if (targetEndPx + padding > viewportEndPx) { + return endScrollPos; + } else { + // NOOP, it's already visible + return viewportStartPx; + } + } + + /* + * Scrolls so that the element is shown at the end of the viewport. The + * viewport will, however, not scroll before its first element. + */ + case END: { + return targetEndPx + padding - viewportLength; + } + + /* + * Scrolls so that the element is shown in the middle of the viewport. + * The viewport will, however, not scroll beyond its contents, given + * more elements than what the viewport is able to show at once. Under + * no circumstances will the viewport scroll before its first element. + */ + case MIDDLE: { + final double targetMiddle = targetStartPx + + (targetEndPx - targetStartPx) / 2; + return targetMiddle - viewportLength / 2; + } + + /* + * Scrolls so that the element is shown at the start of the viewport. + * The viewport will, however, not scroll beyond its contents. + */ + case START: { + return targetStartPx - padding; + } + + /* + * Throw an error if we're here. This can only mean that + * ScrollDestination has been carelessly amended.. + */ + default: { + throw new IllegalArgumentException( + "Internal: ScrollDestination has been modified, " + + "but Escalator.getScrollPos has not been updated " + + "to match new values."); + } + } + + } + /** An inner class that handles all logic related to scrolling. */ private class Scroller extends JsniWorkaround { private double lastScrollTop = 0; @@ -900,7 +975,7 @@ public class Escalator extends Widget { viewportEndPx -= Util.getNativeScrollbarSize(); } - final double scrollLeft = destination.getScrollPos(targetStartPx, + final double scrollLeft = getScrollPos(destination, targetStartPx, targetEndPx, viewportStartPx, viewportEndPx, padding); /* @@ -921,7 +996,7 @@ public class Escalator extends Widget { final double viewportEndPx = viewportStartPx + body.calculateHeight(); - final double scrollTop = destination.getScrollPos(targetStartPx, + final double scrollTop = getScrollPos(destination, targetStartPx, targetEndPx, viewportStartPx, viewportEndPx, padding); /* @@ -3305,33 +3380,6 @@ public class Escalator extends Widget { horizontalScrollbar.setScrollPos(scrollLeft); } - /** - * Scrolls the body horizontally so that the column at the given index is - * visible. - * - * @param columnIndex - * the index of the column to scroll to - * @param destination - * where the column should be aligned visually after scrolling - * @throws IndexOutOfBoundsException - * if {@code columnIndex} is not a valid index for an existing - * column - * @throws IllegalArgumentException - * if the column is frozen - */ - public void scrollToColumn(final int columnIndex, - final ScrollDestination destination) - throws IndexOutOfBoundsException, IllegalArgumentException { - verifyValidColumnIndex(columnIndex); - - if (columnIndex < columnConfiguration.frozenColumns) { - throw new IllegalArgumentException("The given column index " - + columnIndex + " is frozen."); - } - - scroller.scrollToColumn(columnIndex, destination, 0); - } - /** * Scrolls the body horizontally so that the column at the given index is * visible and there is at least {@code padding} pixels to the given scroll @@ -3348,14 +3396,15 @@ public class Escalator extends Widget { * if {@code columnIndex} is not a valid index for an existing * column * @throws IllegalArgumentException - * if {@code destination} is {@link ScrollDestination#MIDDLE}, - * because having a padding on a centered column is undefined - * behavior or if the column is frozen + * if {@code destination} is {@link ScrollDestination#MIDDLE} + * and padding is nonzero, because having a padding on a + * centered column is undefined behavior, or if the column is + * frozen */ public void scrollToColumn(final int columnIndex, final ScrollDestination destination, final int padding) throws IndexOutOfBoundsException, IllegalArgumentException { - if (destination == ScrollDestination.MIDDLE) { + if (destination == ScrollDestination.MIDDLE && padding != 0) { throw new IllegalArgumentException( "You cannot have a padding with a MIDDLE destination"); } @@ -3378,26 +3427,6 @@ public class Escalator extends Widget { } } - /** - * Scrolls the body vertically so that the row at the given index is - * visible. - * - * @param rowIndex - * the index of the row to scroll to - * @param destination - * where the row should be aligned visually after scrolling - * @throws IndexOutOfBoundsException - * if {@code rowIndex} is not a valid index for an existing - * logical row - */ - public void scrollToRow(final int rowIndex, - final ScrollDestination destination) - throws IndexOutOfBoundsException { - verifyValidRowIndex(rowIndex); - - scroller.scrollToRow(rowIndex, destination, 0); - } - /** * Scrolls the body vertically so that the row at the given index is visible * and there is at least {@literal padding} pixels to the given scroll @@ -3413,14 +3442,14 @@ public class Escalator extends Widget { * @throws IndexOutOfBoundsException * if {@code rowIndex} is not a valid index for an existing row * @throws IllegalArgumentException - * if {@code destination} is {@link ScrollDestination#MIDDLE}, - * because having a padding on a centered row is undefined - * behavior + * if {@code destination} is {@link ScrollDestination#MIDDLE} + * and padding is nonzero, because having a padding on a + * centered row is undefined behavior */ public void scrollToRow(final int rowIndex, final ScrollDestination destination, final int padding) throws IndexOutOfBoundsException, IllegalArgumentException { - if (destination == ScrollDestination.MIDDLE) { + if (destination == ScrollDestination.MIDDLE && padding != 0) { throw new IllegalArgumentException( "You cannot have a padding with a MIDDLE destination"); } diff --git a/client/src/com/vaadin/client/ui/grid/Grid.java b/client/src/com/vaadin/client/ui/grid/Grid.java index 015028765b..02aa194655 100644 --- a/client/src/com/vaadin/client/ui/grid/Grid.java +++ b/client/src/com/vaadin/client/ui/grid/Grid.java @@ -18,6 +18,7 @@ package com.vaadin.client.ui.grid; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.logging.Logger; import com.google.gwt.core.shared.GWT; import com.google.gwt.dom.client.Element; @@ -28,6 +29,8 @@ import com.google.gwt.user.client.ui.Widget; import com.vaadin.client.data.DataChangeHandler; import com.vaadin.client.data.DataSource; import com.vaadin.client.ui.grid.renderers.TextRenderer; +import com.vaadin.shared.ui.grid.GridConstants; +import com.vaadin.shared.ui.grid.ScrollDestination; import com.vaadin.shared.util.SharedUtil; /** @@ -1225,4 +1228,91 @@ public class Grid extends Composite { */ return escalator.addRowVisibilityChangeHandler(handler); } + + /** + * Scrolls to a certain row, using {@link ScrollDestination#ANY}. + * + * @param rowIndex + * zero-based index of the row to scroll to. + * @throws IllegalArgumentException + * if rowIndex is below zero, or above the maximum value + * supported by the data source. + */ + public void scrollToRow(int rowIndex) throws IllegalArgumentException { + scrollToRow(rowIndex, ScrollDestination.ANY, + GridConstants.DEFAULT_PADDING); + } + + /** + * Scrolls to a certain row, using user-specified scroll destination. + * + * @param rowIndex + * zero-based index of the row to scroll to. + * @param destination + * desired destination placement of scrolled-to-row. See + * {@link ScrollDestination} for more information. + * @throws IllegalArgumentException + * if rowIndex is below zero, or above the maximum value + * supported by the data source. + */ + public void scrollToRow(int rowIndex, ScrollDestination destination) + throws IllegalArgumentException { + scrollToRow(rowIndex, destination, + destination == ScrollDestination.MIDDLE ? 0 + : GridConstants.DEFAULT_PADDING); + } + + /** + * Scrolls to a certain row using only user-specified parameters. + * + * @param rowIndex + * zero-based index of the row to scroll to. + * @param destination + * desired destination placement of scrolled-to-row. See + * {@link ScrollDestination} for more information. + * @param paddingPx + * number of pixels to overscroll. Behavior depends on + * destination. + * @throws IllegalArgumentException + * if {@code destination} is {@link ScrollDestination#MIDDLE} + * and padding is nonzero, because having a padding on a + * centered row is undefined behavior, or if rowIndex is below + * zero or above the row count of the data source. + */ + private void scrollToRow(int rowIndex, ScrollDestination destination, + int paddingPx) throws IllegalArgumentException { + int maxsize = escalator.getBody().getRowCount() - 1; + + if (rowIndex < 0) { + throw new IllegalArgumentException("Row index (" + rowIndex + + ") is below zero!"); + } + + if (rowIndex > maxsize) { + throw new IllegalArgumentException("Row index (" + rowIndex + + ") is above maximum (" + maxsize + ")!"); + } + + escalator.scrollToRow(rowIndex, destination, paddingPx); + } + + /** + * Scrolls to the beginning of the very first row. + */ + public void scrollToStart() { + scrollToRow(0, ScrollDestination.START); + } + + /** + * Scrolls to the end of the very last row. + */ + public void scrollToEnd() { + scrollToRow(escalator.getBody().getRowCount() - 1, + ScrollDestination.END); + } + + private static final Logger getLogger() { + return Logger.getLogger(Grid.class.getName()); + } + } diff --git a/client/src/com/vaadin/client/ui/grid/GridConnector.java b/client/src/com/vaadin/client/ui/grid/GridConnector.java index f04326c7e6..5e0664667d 100644 --- a/client/src/com/vaadin/client/ui/grid/GridConnector.java +++ b/client/src/com/vaadin/client/ui/grid/GridConnector.java @@ -29,9 +29,11 @@ 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.GridClientRpc; import com.vaadin.shared.ui.grid.GridColumnState; import com.vaadin.shared.ui.grid.GridServerRpc; import com.vaadin.shared.ui.grid.GridState; +import com.vaadin.shared.ui.grid.ScrollDestination; /** * Connects the client side {@link Grid} widget with the server side @@ -96,6 +98,23 @@ public class GridConnector extends AbstractComponentConnector { event.getVisibleRowCount()); } }); + + registerRpc(GridClientRpc.class, new GridClientRpc() { + @Override + public void scrollToStart() { + getWidget().scrollToStart(); + } + + @Override + public void scrollToEnd() { + getWidget().scrollToEnd(); + } + + @Override + public void scrollToRow(int row, ScrollDestination destination) { + getWidget().scrollToRow(row, destination); + } + }); } @Override diff --git a/client/src/com/vaadin/client/ui/grid/ScrollDestination.java b/client/src/com/vaadin/client/ui/grid/ScrollDestination.java deleted file mode 100644 index e14f50ff7c..0000000000 --- a/client/src/com/vaadin/client/ui/grid/ScrollDestination.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * 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; - -/** - * The destinations that are supported in an Escalator when scrolling rows or - * columns into view. - * - * @since 7.2 - * @author Vaadin Ltd - */ -public enum ScrollDestination { - - /** - * Scroll as little as possible to show the target element. If the element - * fits into view, this works as START or END depending on the current - * scroll position. If the element does not fit into view, this works as - * START. - */ - ANY { - @Override - double getScrollPos(final double targetStartPx, - final double targetEndPx, final double viewportStartPx, - final double viewportEndPx, final int padding) { - - final double startScrollPos = targetStartPx - padding; - final double viewportLength = viewportEndPx - viewportStartPx; - final double endScrollPos = targetEndPx + padding - viewportLength; - - if (startScrollPos < viewportStartPx) { - return startScrollPos; - } else if (targetEndPx + padding > viewportEndPx) { - return endScrollPos; - } else { - // NOOP, it's already visible - return viewportStartPx; - } - } - }, - - /** - * Scrolls so that the element is shown at the start of the viewport. The - * viewport will, however, not scroll beyond its contents. - */ - START { - @Override - double getScrollPos(final double targetStartPx, - final double targetEndPx, final double viewportStartPx, - final double viewportEndPx, final int padding) { - return targetStartPx - padding; - } - }, - - /** - * Scrolls so that the element is shown in the middle of the viewport. The - * viewport will, however, not scroll beyond its contents, given more - * elements than what the viewport is able to show at once. Under no - * circumstances will the viewport scroll before its first element. - */ - MIDDLE { - @Override - double getScrollPos(final double targetStartPx, - final double targetEndPx, final double viewportStartPx, - final double viewportEndPx, final int padding) { - final double targetMiddle = targetStartPx - + (targetEndPx - targetStartPx) / 2; - final double viewportLength = viewportEndPx - viewportStartPx; - return targetMiddle - viewportLength / 2; - } - }, - - /** - * Scrolls so that the element is shown at the end of the viewport. The - * viewport will, however, not scroll before its first element. - */ - END { - @Override - double getScrollPos(final double targetStartPx, - final double targetEndPx, final double viewportStartPx, - final double viewportEndPx, final int padding) { - final double viewportLength = viewportEndPx - viewportStartPx; - return targetEndPx + padding - viewportLength; - } - }; - - abstract double getScrollPos(final double targetStartPx, - final double targetEndPx, final double viewportStartPx, - final double viewportEndPx, final int padding); -} diff --git a/client/src/com/vaadin/shared/ui/grid/ScrollDestination.java b/client/src/com/vaadin/shared/ui/grid/ScrollDestination.java new file mode 100644 index 0000000000..decc2fab5f --- /dev/null +++ b/client/src/com/vaadin/shared/ui/grid/ScrollDestination.java @@ -0,0 +1,55 @@ +/* + * 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; + +/** + * Enumeration, specifying the destinations that are supported when scrolling + * rows or columns into view. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public enum ScrollDestination { + + /** + * Scroll as little as possible to show the target element. If the element + * fits into view, this works as START or END depending on the current + * scroll position. If the element does not fit into view, this works as + * START. + */ + ANY, + + /** + * Scrolls so that the element is shown at the start of the viewport. The + * viewport will, however, not scroll beyond its contents. + */ + START, + + /** + * Scrolls so that the element is shown in the middle of the viewport. The + * viewport will, however, not scroll beyond its contents, given more + * elements than what the viewport is able to show at once. Under no + * circumstances will the viewport scroll before its first element. + */ + MIDDLE, + + /** + * Scrolls so that the element is shown at the end of the viewport. The + * viewport will, however, not scroll before its first element. + */ + END + +} diff --git a/server/src/com/vaadin/ui/components/grid/Grid.java b/server/src/com/vaadin/ui/components/grid/Grid.java index 08685874c1..4126ec6d93 100644 --- a/server/src/com/vaadin/ui/components/grid/Grid.java +++ b/server/src/com/vaadin/ui/components/grid/Grid.java @@ -42,10 +42,12 @@ import com.vaadin.data.Property.ValueChangeNotifier; import com.vaadin.data.RpcDataProviderExtension; import com.vaadin.server.KeyMapper; import com.vaadin.shared.ui.grid.ColumnGroupRowState; +import com.vaadin.shared.ui.grid.GridClientRpc; import com.vaadin.shared.ui.grid.GridColumnState; import com.vaadin.shared.ui.grid.GridServerRpc; import com.vaadin.shared.ui.grid.GridState; import com.vaadin.shared.ui.grid.Range; +import com.vaadin.shared.ui.grid.ScrollDestination; import com.vaadin.ui.AbstractComponent; import com.vaadin.ui.Component; @@ -804,4 +806,56 @@ public class Grid extends AbstractComponent { public Object getLastFrozenPropertyId() { return columnKeys.get(getState().lastFrozenColumnId); } + + /** + * Scrolls to a certain item, using {@link ScrollDestination#ANY}. + * + * @param itemId + * id of item to scroll to. + * @throws IllegalArgumentException + * if the provided id is not recognized by the data source. + */ + public void scrollToItem(Object itemId) throws IllegalArgumentException { + scrollToItem(itemId, ScrollDestination.ANY); + } + + /** + * Scrolls to a certain item, using user-specified scroll destination. + * + * @param itemId + * id of item to scroll to. + * @param destination + * value specifying desired position of scrolled-to row. + * @throws IllegalArgumentException + * if the provided id is not recognized by the data source. + */ + public void scrollToItem(Object itemId, ScrollDestination destination) + throws IllegalArgumentException { + + int row = datasource.indexOfId(itemId); + + if (row == -1) { + throw new IllegalArgumentException( + "Item with specified ID does not exist in data source"); + } + + GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class); + clientRPC.scrollToRow(row, destination); + } + + /** + * Scrolls to the beginning of the first data row. + */ + public void scrollToStart() { + GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class); + clientRPC.scrollToStart(); + } + + /** + * Scrolls to the end of the last data row. + */ + public void scrollToEnd() { + GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class); + clientRPC.scrollToEnd(); + } } diff --git a/shared/src/com/vaadin/shared/ui/grid/GridClientRpc.java b/shared/src/com/vaadin/shared/ui/grid/GridClientRpc.java new file mode 100644 index 0000000000..00cc93d371 --- /dev/null +++ b/shared/src/com/vaadin/shared/ui/grid/GridClientRpc.java @@ -0,0 +1,53 @@ +/* + * 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 com.vaadin.shared.communication.ClientRpc; + +/** + * Server-to-client RPC interface for the Grid component. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public interface GridClientRpc extends ClientRpc { + + /** + * Command client Grid to scroll to a specific data row. + * + * @param row + * zero-based row index. If the row index is below zero or above + * the row count of the client-side data source, a client-side + * exception will be triggered. Since this exception has no + * handling by default, an out-of-bounds value will cause a + * client-side crash. + * @param destination + * desired placement of scrolled-to row. See the documentation + * for {@link ScrollDestination} for more information. + */ + public void scrollToRow(int row, ScrollDestination destination); + + /** + * Command client Grid to scroll to the first row. + */ + public void scrollToStart(); + + /** + * Command client Grid to scroll to the last row. + */ + public void scrollToEnd(); + +} diff --git a/shared/src/com/vaadin/shared/ui/grid/GridConstants.java b/shared/src/com/vaadin/shared/ui/grid/GridConstants.java new file mode 100644 index 0000000000..5b88fad5a8 --- /dev/null +++ b/shared/src/com/vaadin/shared/ui/grid/GridConstants.java @@ -0,0 +1,33 @@ +/* + * 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; + +/** + * Container class for common constants and default values used by the Grid + * component. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public final class GridConstants { + + /** + * Default padding in pixels when scrolling programmatically, without an + * explicitly defined padding value. + */ + public static final int DEFAULT_PADDING = 0; + +} diff --git a/uitest/src/com/vaadin/tests/components/grid/GridScrolling.java b/uitest/src/com/vaadin/tests/components/grid/GridScrolling.java new file mode 100644 index 0000000000..d514fbd0c5 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/GridScrolling.java @@ -0,0 +1,115 @@ +/* + * 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.Item; +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.server.VaadinRequest; +import com.vaadin.shared.ui.grid.ScrollDestination; +import com.vaadin.tests.components.AbstractTestUI; +import com.vaadin.ui.Button; +import com.vaadin.ui.Button.ClickEvent; +import com.vaadin.ui.Button.ClickListener; +import com.vaadin.ui.HorizontalLayout; +import com.vaadin.ui.VerticalLayout; +import com.vaadin.ui.components.grid.Grid; + +/** + * + */ +@SuppressWarnings("serial") +public class GridScrolling extends AbstractTestUI { + + private Grid grid; + + private IndexedContainer ds; + + @Override + @SuppressWarnings("unchecked") + protected void setup(VaadinRequest request) { + // Build data source + ds = new IndexedContainer(); + + for (int col = 0; col < 5; col++) { + ds.addContainerProperty("col" + col, String.class, ""); + } + + for (int row = 0; row < 65536; row++) { + Item item = ds.addItem(Integer.valueOf(row)); + for (int col = 0; col < 5; col++) { + item.getItemProperty("col" + col).setValue( + "(" + row + ", " + col + ")"); + } + } + + grid = new Grid(ds); + + HorizontalLayout hl = new HorizontalLayout(); + hl.addComponent(grid); + hl.setMargin(true); + hl.setSpacing(true); + + VerticalLayout vl = new VerticalLayout(); + vl.setSpacing(true); + + // Add scroll buttons + Button scrollUpButton = new Button("Top", new ClickListener() { + @Override + public void buttonClick(ClickEvent event) { + grid.scrollToStart(); + } + }); + scrollUpButton.setSizeFull(); + vl.addComponent(scrollUpButton); + + for (int i = 1; i < 7; ++i) { + final int row = (ds.size() / 7) * i; + Button scrollButton = new Button("Scroll to row " + row, + new ClickListener() { + @Override + public void buttonClick(ClickEvent event) { + grid.scrollToItem(Integer.valueOf(row), + ScrollDestination.MIDDLE); + } + }); + scrollButton.setSizeFull(); + vl.addComponent(scrollButton); + } + + Button scrollDownButton = new Button("Bottom", new ClickListener() { + @Override + public void buttonClick(ClickEvent event) { + grid.scrollToEnd(); + } + }); + scrollDownButton.setSizeFull(); + vl.addComponent(scrollDownButton); + + hl.addComponent(vl); + addComponent(hl); + } + + @Override + protected String getTestDescription() { + return "Test Grid programmatic scrolling features"; + } + + @Override + protected Integer getTicketNumber() { + return 13327; + } + +} diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridConnector.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridConnector.java index f9286091c0..b8ea380301 100644 --- a/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridConnector.java +++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridConnector.java @@ -16,8 +16,8 @@ package com.vaadin.tests.widgetset.client.grid; import com.vaadin.client.ui.AbstractComponentConnector; -import com.vaadin.client.ui.grid.ScrollDestination; import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.grid.ScrollDestination; import com.vaadin.tests.widgetset.server.grid.TestGrid; /** diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/VTestGrid.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/VTestGrid.java index 5c8dd4a609..0230367b85 100644 --- a/uitest/src/com/vaadin/tests/widgetset/client/grid/VTestGrid.java +++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/VTestGrid.java @@ -10,7 +10,7 @@ import com.vaadin.client.ui.grid.Escalator; import com.vaadin.client.ui.grid.EscalatorUpdater; import com.vaadin.client.ui.grid.Row; import com.vaadin.client.ui.grid.RowContainer; -import com.vaadin.client.ui.grid.ScrollDestination; +import com.vaadin.shared.ui.grid.ScrollDestination; public class VTestGrid extends Composite { @@ -174,20 +174,12 @@ public class VTestGrid extends Composite { public void scrollToRow(final int index, final ScrollDestination destination, final int padding) { - if (padding != 0) { - escalator.scrollToRow(index, destination, padding); - } else { - escalator.scrollToRow(index, destination); - } + escalator.scrollToRow(index, destination, padding); } public void scrollToColumn(final int index, final ScrollDestination destination, final int padding) { - if (padding != 0) { - escalator.scrollToColumn(index, destination, padding); - } else { - escalator.scrollToColumn(index, destination); - } + escalator.scrollToColumn(index, destination, padding); } public void removeRows(final int offset, final int amount) { -- cgit v1.2.3 From 54b448d018922f6315bb756c24c6ce7feff6e14d Mon Sep 17 00:00:00 2001 From: Leif Åstrand Date: Fri, 28 Feb 2014 13:42:56 +0200 Subject: Fix compile errors (#13334) Change-Id: Ib9210685ed70f98af78b5cc3ad756f37b09c326a --- .../com/vaadin/client/ui/grid/FlyweightCell.java | 3 +- .../vaadin/shared/ui/grid/ScrollDestination.java | 55 ---------------------- .../vaadin/shared/ui/grid/ScrollDestination.java | 55 ++++++++++++++++++++++ 3 files changed, 56 insertions(+), 57 deletions(-) delete mode 100644 client/src/com/vaadin/shared/ui/grid/ScrollDestination.java create mode 100644 shared/src/com/vaadin/shared/ui/grid/ScrollDestination.java (limited to 'shared') diff --git a/client/src/com/vaadin/client/ui/grid/FlyweightCell.java b/client/src/com/vaadin/client/ui/grid/FlyweightCell.java index 8f52e415e5..296a70934b 100644 --- a/client/src/com/vaadin/client/ui/grid/FlyweightCell.java +++ b/client/src/com/vaadin/client/ui/grid/FlyweightCell.java @@ -20,7 +20,6 @@ import java.util.List; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.Style.Display; import com.google.gwt.dom.client.Style.Unit; -import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.ui.IsWidget; import com.google.gwt.user.client.ui.Widget; import com.vaadin.client.ui.grid.FlyweightRow.CellIterator; @@ -192,7 +191,7 @@ class FlyweightCell implements Cell { widget.removeFromParent(); // Physical attach. - DOM.appendChild(getElement(), widget.getElement()); + getElement().appendChild(widget.getElement()); Escalator.setParent(widget, escalator); } diff --git a/client/src/com/vaadin/shared/ui/grid/ScrollDestination.java b/client/src/com/vaadin/shared/ui/grid/ScrollDestination.java deleted file mode 100644 index decc2fab5f..0000000000 --- a/client/src/com/vaadin/shared/ui/grid/ScrollDestination.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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; - -/** - * Enumeration, specifying the destinations that are supported when scrolling - * rows or columns into view. - * - * @since 7.2 - * @author Vaadin Ltd - */ -public enum ScrollDestination { - - /** - * Scroll as little as possible to show the target element. If the element - * fits into view, this works as START or END depending on the current - * scroll position. If the element does not fit into view, this works as - * START. - */ - ANY, - - /** - * Scrolls so that the element is shown at the start of the viewport. The - * viewport will, however, not scroll beyond its contents. - */ - START, - - /** - * Scrolls so that the element is shown in the middle of the viewport. The - * viewport will, however, not scroll beyond its contents, given more - * elements than what the viewport is able to show at once. Under no - * circumstances will the viewport scroll before its first element. - */ - MIDDLE, - - /** - * Scrolls so that the element is shown at the end of the viewport. The - * viewport will, however, not scroll before its first element. - */ - END - -} diff --git a/shared/src/com/vaadin/shared/ui/grid/ScrollDestination.java b/shared/src/com/vaadin/shared/ui/grid/ScrollDestination.java new file mode 100644 index 0000000000..decc2fab5f --- /dev/null +++ b/shared/src/com/vaadin/shared/ui/grid/ScrollDestination.java @@ -0,0 +1,55 @@ +/* + * 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; + +/** + * Enumeration, specifying the destinations that are supported when scrolling + * rows or columns into view. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public enum ScrollDestination { + + /** + * Scroll as little as possible to show the target element. If the element + * fits into view, this works as START or END depending on the current + * scroll position. If the element does not fit into view, this works as + * START. + */ + ANY, + + /** + * Scrolls so that the element is shown at the start of the viewport. The + * viewport will, however, not scroll beyond its contents. + */ + START, + + /** + * Scrolls so that the element is shown in the middle of the viewport. The + * viewport will, however, not scroll beyond its contents, given more + * elements than what the viewport is able to show at once. Under no + * circumstances will the viewport scroll before its first element. + */ + MIDDLE, + + /** + * Scrolls so that the element is shown at the end of the viewport. The + * viewport will, however, not scroll before its first element. + */ + END + +} -- cgit v1.2.3