]> source.dussan.org Git - vaadin-framework.git/commitdiff
Multiple headers and footer rows #3153
authorJohn Ahlroos <john@vaadin.com>
Wed, 6 Nov 2013 08:35:03 +0000 (10:35 +0200)
committerVaadin Code Review <review@vaadin.com>
Fri, 22 Nov 2013 12:59:10 +0000 (12:59 +0000)
Change-Id: Iadb0d8b051d0f0ef1303e0d7d740cf476cd81971

15 files changed:
client/src/com/vaadin/client/ui/grid/ColumnGroup.java [new file with mode: 0644]
client/src/com/vaadin/client/ui/grid/ColumnGroupRow.java [new file with mode: 0644]
client/src/com/vaadin/client/ui/grid/Grid.java
client/src/com/vaadin/client/ui/grid/GridConnector.java
server/src/com/vaadin/ui/components/grid/ColumnGroup.java [new file with mode: 0644]
server/src/com/vaadin/ui/components/grid/ColumnGroupRow.java [new file with mode: 0644]
server/src/com/vaadin/ui/components/grid/Grid.java
server/src/com/vaadin/ui/components/grid/GridColumn.java
server/tests/src/com/vaadin/tests/server/component/grid/GridColumns.java
shared/src/com/vaadin/shared/ui/grid/ColumnGroupRowState.java [new file with mode: 0644]
shared/src/com/vaadin/shared/ui/grid/ColumnGroupState.java [new file with mode: 0644]
shared/src/com/vaadin/shared/ui/grid/GridColumnState.java
shared/src/com/vaadin/shared/ui/grid/GridState.java
uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java
uitest/src/com/vaadin/tests/components/grid/GridColumnGroups.java [new file with mode: 0644]

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 (file)
index 0000000..c37068d
--- /dev/null
@@ -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<GridColumn> columns;
+
+    /**
+     * The grid associated with the column group
+     */
+    private final Grid grid;
+
+    /**
+     * Constructs a new column group
+     */
+    ColumnGroup(Grid grid, Collection<GridColumn> 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<GridColumn>(
+                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<GridColumn> 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 (file)
index 0000000..6bbc9bc
--- /dev/null
@@ -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<ColumnGroup> groups = new ArrayList<ColumnGroup>();
+
+    /**
+     * 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<GridColumn> columns = new HashSet<GridColumn>();
+        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<ColumnGroup> getGroups() {
+        return Collections.unmodifiableList(groups);
+    }
+
+    /**
+     * Is the header visible for the row.
+     * 
+     * @return <code>true</code> 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 <code>true</code> 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;
+    }
+}
index 3c4e2d6e1328d6acfd2b7f12f553c2827b2b0b14..67f14301f01c11fe54b56d089034a9694eda4a73 100644 (file)
@@ -55,7 +55,7 @@ import com.vaadin.shared.util.SharedUtil;
 public class Grid<T> 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<T> extends Composite {
     private final List<GridColumn<T>> columns = new ArrayList<GridColumn<T>>();
 
     /**
-     * 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<ColumnGroupRow> columnGroupRows = new ArrayList<ColumnGroupRow>();
+
+    /**
+     * 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 <T>
      *            the row type
@@ -74,24 +89,24 @@ public class Grid<T> extends Composite {
     public static abstract class AbstractGridColumn<T> {
 
         /**
-         * Grid associated with the column
+         * The grid the column is associated with
          */
         private Grid<T> 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<T> 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<T> 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<T> extends Composite {
          * Sets a column as visible in the grid.
          * 
          * @param visible
-         *            Set to <code>true</code> to show the column in the grid
+         *            <code>true</code> 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<T> 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<T> 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 <code>true</code> if the row should be visible
+         */
+        public abstract boolean isRowVisible(ColumnGroupRow row);
+
+        /**
+         * Should the first row be visible
+         * 
+         * @return <code>true</code> if the first row should be visible
+         */
+        public abstract boolean firstRowIsVisible();
+
+        @Override
+        public void updateCells(Row row, List<Cell> 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<T> extends Composite {
         escalator.getHeader().setEscalatorUpdater(createHeaderUpdater());
         escalator.getBody().setEscalatorUpdater(createBodyUpdater());
         escalator.getFooter().setEscalatorUpdater(createFooterUpdater());
+
+        refreshHeader();
+        refreshFooter();
     }
 
     /**
@@ -238,18 +375,26 @@ public class Grid<T> 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<Cell> cellsToUpdate) {
-                if (isHeaderVisible()) {
-                    for (Cell cell : cellsToUpdate) {
-                        AbstractGridColumn<T> 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<T> 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<Cell> cellsToUpdate) {
-                if (isFooterVisible()) {
-                    for (Cell cell : cellsToUpdate) {
-                        AbstractGridColumn<T> 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
+     *            <code>true</code> 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<T> extends Composite {
      *             if the column index does not exist in the grid
      */
     public GridColumn<T> 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.
+     * 
+     * <p>
+     * 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.
+     * </p>
+     * 
+     * <p>
+     * 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 <code>false</code>.
+     * </p>
+     * 
+     * <p>
+     * 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.
+     * </p>
+     * 
+     * <p>
+     * The header row is by default visible.
+     * </p>
      * 
      * @param visible
-     *            true if header rows should be visible
+     *            <code>true</code> 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 <code>true</code> if the header is visible
+     * @return <code>true</code> 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.
+     * 
+     * <p>
+     * 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.
+     * </p>
+     * 
+     * <p>
+     * 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 <code>false</code>.
+     * </p>
+     * 
+     * <p>
+     * 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.
+     * </p>
+     * 
+     * <p>
+     * The footer row is by default hidden.
+     * </p>
      * 
      * @param visible
-     *            true if header rows should be visible
+     *            <code>true</code> 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 <code>true</code> 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.
+     * 
+     * <p>
+     * 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.
+     * </p>
+     * 
+     * Example usage:
+     * 
+     * <pre>
+     * // Add a new column group row to the grid
+     * ColumnGroupRow row = grid.addColumnGroupRow();
+     * 
+     * // Group &quot;Column1&quot; and &quot;Column2&quot; together to form a header in the row
+     * ColumnGroup column12 = row.addGroup(&quot;Column1&quot;, &quot;Column2&quot;);
+     * 
+     * // Set a common header for &quot;Column1&quot; and &quot;Column2&quot;
+     * column12.setHeader(&quot;Column 1&amp;2&quot;);
+     * 
+     * // Set a common footer for &quot;Column1&quot; and &quot;Column2&quot;
+     * column12.setFooter(&quot;Column 1&amp;2&quot;);
+     * </pre>
+     * 
+     * @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 <code>true</code> 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<ColumnGroupRow> getColumnGroupRows() {
+        return Collections.unmodifiableList(new ArrayList<ColumnGroupRow>(
+                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 <code>null</code> if not
+     *         found.
+     */
+    private static ColumnGroup getGroupForColumn(ColumnGroupRow row,
+            GridColumn column) {
+        for (ColumnGroup group : row.getGroups()) {
+            List<GridColumn> columns = group.getColumns();
+            if (columns.contains(column)) {
+                return group;
+            }
+        }
+        return null;
     }
 
     @Override
index c48c9936bc313694d3430076adbb1ab2338981e0..32907e1e29059c9b7da89518c0737d4912574b59 100644 (file)
 
 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<String[]> {
 
         @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<String, CustomGridColumn> columnIdToColumn = new HashMap<String, CustomGridColumn>();
 
     @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<String[]> 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<String[]> 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<GridColumn> columns = new ArrayList<GridColumn>();
+                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 (file)
index 0000000..0ab1f61
--- /dev/null
@@ -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<Object> 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<Object> propertyIds) {
+        if (propertyIds == null) {
+            throw new IllegalArgumentException(
+                    "propertyIds cannot be null. Use empty list instead.");
+        }
+
+        this.state = state;
+        columns = Collections.unmodifiableList(new ArrayList<Object>(
+                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 <code>true</code> 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<Object> 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 (file)
index 0000000..326d282
--- /dev/null
@@ -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<ColumnGroup> groups = new ArrayList<ColumnGroup>();
+
+    /**
+     * 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<Object> 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<Object> 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<Object> propertyIds = new ArrayList<Object>();
+        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<Object> propertyIds = new ArrayList<Object>();
+        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<ColumnGroup> 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 <code>true</code> 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 <code>true</code> 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 <code>true</code> 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();
+    }
+
+}
index 25ac796d47886a232edbf86ddecc99508b939ffb..2b19043d93d9c65663b3b97e7737ccfc0eda623e 100644 (file)
@@ -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<Object, GridColumn> columns = new HashMap<Object, GridColumn>();
 
     /**
-     * Key generator for column server->client communication
+     * Key generator for column server-to-client communication
      */
     private final KeyMapper<Object> columnKeys = new KeyMapper<Object>();
 
+    /**
+     * The column groups added to the grid
+     */
+    private final List<ColumnGroupRow> columnGroupRows = new ArrayList<ColumnGroupRow>();
+
     /**
      * 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
      *            <code>true</code> 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 <code>true</code> if the header is visible
+     * @return <code>true</code> 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
-     *            <code>true</code> if the header rows should be visible
+     *            <code>true</code> 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 <code>true</code> if the footer rows should be visible
      */
-    public boolean isFooterVisible() {
-        return getState(false).footerVisible;
+    public boolean isColumnFootersVisible() {
+        return getState(false).columnFootersVisible;
+    }
+
+    /**
+     * <p>
+     * Adds a new column group to the grid.
+     * 
+     * <p>
+     * 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.
+     * </p>
+     * </p>
+     * 
+     * <p>
+     * Example usage:
+     * 
+     * <pre>
+     * // Add a new column group row to the grid
+     * ColumnGroupRow row = grid.addColumnGroupRow();
+     * 
+     * // Group &quot;Column1&quot; and &quot;Column2&quot; together to form a header in the row
+     * ColumnGroup column12 = row.addGroup(&quot;Column1&quot;, &quot;Column2&quot;);
+     * 
+     * // Set a common header for &quot;Column1&quot; and &quot;Column2&quot;
+     * column12.setHeader(&quot;Column 1&amp;2&quot;);
+     * </pre>
+     * 
+     * </p>
+     * 
+     * @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<ColumnGroupRow> getColumnGroupRows() {
+        return Collections.unmodifiableList(new ArrayList<ColumnGroupRow>(
+                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 <code>null</code> if not found
+     * @return the column with the id or <code>null</code> 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");
         }
index 505919b3cfb9013578837034ebe7c85e6aa66522..dde066923898b455522f49ef2ddfc18610015e62 100644 (file)
@@ -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();
     }
index 5989d537b4f66ac0ebdbe2ae89a196cd0e99c641..85864160a806b4dd09fe2b509408b78830ddd2ba 100644 (file)
@@ -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 (file)
index 0000000..a8e0f87
--- /dev/null
@@ -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<ColumnGroupState> groups = new ArrayList<ColumnGroupState>();
+
+    /**
+     * 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 (file)
index 0000000..3992b66
--- /dev/null
@@ -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<String> columns = new ArrayList<String>();
+
+    /**
+     * The header text of the group
+     */
+    public String header;
+
+    /**
+     * The footer text of the group
+     */
+    public String footer;
+}
index 391eb2a65ce63946dfb9538359f4be40eeca4b46..0301c5ead2ea8b20e4b49acf5723307109289e56 100644 (file)
@@ -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;
 
index e1e0fff35418f8720fba9bde6a39f55125d99321..d1167f3d4fbed83b27992792853d0d3a7dcb290f 100644 (file)
@@ -40,13 +40,17 @@ public class GridState extends AbstractComponentState {
     public List<GridColumnState> columns = new ArrayList<GridColumnState>();
 
     /**
-     * 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<ColumnGroupRowState> columnGroupRows = new ArrayList<ColumnGroupRowState>();
 }
index 5b3d742f19aa6b80e7a64b24e3b0f6166374065c..bd3e96f84a1c8704e6d4301e87bc7cae26f8f94f 100644 (file)
@@ -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<Grid> {
 
     private final int COLUMNS = 10;
 
+    private int columnGroupRows = 0;
+
     @Override
     protected Grid constructComponent() {
 
@@ -42,20 +46,51 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> {
             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<Grid, Boolean>() {
+
+                    @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<Grid, Boolean>() {
+
+                    @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<Grid> {
                         }
                     }, c);
 
-            createBooleanAction("Footer", "Column" + c, true,
-                    new Command<Grid, Boolean>() {
-
-                        @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<Grid, Boolean>() {
-
-                        @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<Grid, String>() {
 
@@ -131,6 +126,72 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> {
 
     }
 
+    protected void createColumnGroupActions() {
+        createCategory("Column groups", null);
+
+        createClickAction("Add group row", "Column groups",
+                new Command<Grid, String>() {
+
+                    @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<Grid, Boolean>() {
+
+                                    @Override
+                                    public void execute(Grid grid,
+                                            Boolean value, Object columnIndex) {
+                                        row.setHeaderVisible(value);
+                                    }
+                                }, row);
+
+                        createBooleanAction("Footer Visible",
+                                "Column group row " + columnGroupRows, false,
+                                new Command<Grid, Boolean>() {
+
+                                    @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<Grid, Integer>() {
+
+                                        @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 (file)
index 0000000..66e7651
--- /dev/null
@@ -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;
+    }
+
+}