]> source.dussan.org Git - vaadin-framework.git/commitdiff
Initial escalator commit (#12645)
authorHenrik Paul <henrik@vaadin.com>
Tue, 24 Sep 2013 12:03:56 +0000 (15:03 +0300)
committerHenrik Paul <henrik@vaadin.com>
Wed, 9 Oct 2013 11:26:05 +0000 (14:26 +0300)
Change-Id: Ibd0ac2896e12b99ddebdc26674a2dfced486c49a

13 files changed:
WebContent/VAADIN/themes/base/base.scss
WebContent/VAADIN/themes/base/escalator/escalator.scss [new file with mode: 0644]
client/src/com/vaadin/client/ui/grid/Cell.java [new file with mode: 0644]
client/src/com/vaadin/client/ui/grid/CellRenderer.java [new file with mode: 0644]
client/src/com/vaadin/client/ui/grid/ColumnConfiguration.java [new file with mode: 0644]
client/src/com/vaadin/client/ui/grid/Escalator.java [new file with mode: 0644]
client/src/com/vaadin/client/ui/grid/PositionFunction.java [new file with mode: 0644]
client/src/com/vaadin/client/ui/grid/RowContainer.java [new file with mode: 0644]
uitest/src/com/vaadin/tests/components/grid/GridTest.java [new file with mode: 0644]
uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridConnector.java [new file with mode: 0644]
uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridState.java [new file with mode: 0644]
uitest/src/com/vaadin/tests/widgetset/client/grid/VTestGrid.java [new file with mode: 0644]
uitest/src/com/vaadin/tests/widgetset/server/grid/TestGrid.java [new file with mode: 0644]

index f22af7670e29a4774ce15d76fcc7c1c66244e61f..2ea70036c47c014d4233f6533e52bf19aa8a7bec 100644 (file)
@@ -15,6 +15,7 @@
 @import "inlinedatefield/inlinedatefield.scss";
 @import "dragwrapper/dragwrapper.scss";
 @import "embedded/embedded.scss";
+@import "escalator/escalator.scss";
 @import "formlayout/formlayout.scss";
 @import "gridlayout/gridlayout.scss";
 @import "label/label.scss";
@@ -82,6 +83,7 @@ $line-height: normal;
        @include base-inline-datefield;
        @include base-dragwrapper;
        @include base-embedded;
+       @include base-escalator;
        @include base-formlayout;
        @include base-gridlayout;
        @include base-label;
diff --git a/WebContent/VAADIN/themes/base/escalator/escalator.scss b/WebContent/VAADIN/themes/base/escalator/escalator.scss
new file mode 100644 (file)
index 0000000..9dad07d
--- /dev/null
@@ -0,0 +1,72 @@
+@mixin base-escalator($primaryStyleName : v-escalator) {
+
+$background-color: white;
+$border-color: #aaa;
+
+.#{$primaryStyleName} {
+       position: relative;
+       background-color: $background-color;
+}
+
+.#{$primaryStyleName}-cell {
+       -moz-box-sizing: border-box;
+       box-sizing: border-box;
+}
+
+.#{$primaryStyleName}-scroller {
+       position: absolute;
+       overflow: auto;
+       height: inherit;
+       width: inherit; /* width will be overridden if we have frozen columns */
+}
+
+.#{$primaryStyleName}-tablewrapper {
+       position: absolute;
+       overflow: hidden;
+}
+
+.#{$primaryStyleName}-tablewrapper > table {
+       border-spacing: 0;
+       table-layout: fixed;
+       width: inherit; /* a decent default fallback */
+}
+
+.#{$primaryStyleName}-header {
+       position: absolute;
+       top: 0;
+       left: 0;
+       width: inherit;
+       z-index: 10;
+}
+
+.#{$primaryStyleName}-body {
+       position: absolute;
+       top: 0;
+       left: 0;
+       width: inherit;
+}
+
+.#{$primaryStyleName}-footer {
+       position: absolute;
+       bottom: 0;
+       left: 0;
+       width: inherit;
+}
+
+.#{$primaryStyleName}-row {
+       position: absolute;
+       width: inherit;
+       top: 0;
+       left: 0;
+       background-color: $background-color;
+}
+
+.#{$primaryStyleName}-cell {
+       display: block;
+       float: left;
+       border: 1px solid $border-color;
+       padding: 2px;
+       white-space: nowrap;
+}
+
+}
\ No newline at end of file
diff --git a/client/src/com/vaadin/client/ui/grid/Cell.java b/client/src/com/vaadin/client/ui/grid/Cell.java
new file mode 100644 (file)
index 0000000..08f3eeb
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * 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.user.client.Element;
+
+/**
+ * A representation of a single cell.
+ * <p>
+ * A Cell instance will be provided to the {@link CellRenderer} responsible for
+ * rendering the cells in a certain {@link RowContainer}.
+ * 
+ * @since 7.2
+ * @author Vaadin Ltd
+ * @see CellRenderer#renderCell(Cell)
+ */
+public interface Cell {
+    /**
+     * Returns the index of the row this cell is in.
+     * 
+     * @return the index of the row this cell is in
+     */
+    public int getRow();
+
+    /**
+     * Returns the index of the column this cell is in.
+     * 
+     * @return the index of the column this cell is in
+     */
+    public int getColumn();
+
+    /**
+     * Returns the root element for this cell. The {@link CellRenderer} may
+     * update the class names of the element, add inline styles and freely
+     * modify the contents.
+     * <p>
+     * Avoid modifying the dimensions or positioning of the cell element.
+     * 
+     * @return The root element for this cell. Never <code>null</code>.
+     */
+    public Element getElement();
+}
\ No newline at end of file
diff --git a/client/src/com/vaadin/client/ui/grid/CellRenderer.java b/client/src/com/vaadin/client/ui/grid/CellRenderer.java
new file mode 100644 (file)
index 0000000..636a512
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * 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 interface that defines how the cells in a {@link RowContainer} should look
+ * like.
+ * 
+ * @since 7.2
+ * @author Vaadin Ltd
+ * @see RowContainer#setCellRenderer(CellRenderer)
+ */
+public interface CellRenderer {
+    /** A {@link CellRenderer} that doesn't render anything. */
+    public static final CellRenderer NULL_RENDERER = new CellRenderer() {
+        @Override
+        public void renderCell(final Cell cell) {
+        }
+    };
+
+    /**
+     * Renders a cell contained in a row container.
+     * 
+     * @param cell
+     *            the cell that can be manipulated to modify the contents of the
+     *            cell being rendered. Never <code>null</code>.
+     */
+    public void renderCell(Cell cell);
+}
diff --git a/client/src/com/vaadin/client/ui/grid/ColumnConfiguration.java b/client/src/com/vaadin/client/ui/grid/ColumnConfiguration.java
new file mode 100644 (file)
index 0000000..be60b1a
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ * 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;
+
+/**
+ * A representation of the columns in an instance of {@link Escalator}.
+ * 
+ * @since 7.2
+ * @author Vaadin Ltd
+ * @see Escalator#getColumnConfiguration()
+ */
+public interface ColumnConfiguration {
+
+    /**
+     * Removes columns at a certain offset.
+     * 
+     * @param offset
+     *            the index of the first column to be removed
+     * @param numberOfColumns
+     *            the number of rows to remove, starting from the offset
+     * @throws IndexOutOfBoundsException
+     *             if any integer in the range
+     *             <code>[offset..(offset+numberOfColumns)]</code> is not an
+     *             existing column index.
+     * @throws IllegalArgumentException
+     *             if <code>numberOfColumns</code> is less than 1.
+     */
+    public void removeColumns(int offset, int numberOfColumns)
+            throws IndexOutOfBoundsException, IllegalArgumentException;
+
+    /**
+     * Adds columns at a certain offset.
+     * <p>
+     * The new columns will be inserted between the column at the offset, and
+     * the column before (an offset of 0 means that the columns are inserted at
+     * the beginning). Therefore, the columns at the offset and afterwards will
+     * be moved to the right.
+     * <p>
+     * The contents of the inserted columns will be queried from the respective
+     * cell renderers in the header, body and footer.
+     * <p>
+     * <em>Note:</em> Only the contents of the inserted columns will be
+     * rendered. If inserting new columns affects the contents of existing
+     * columns, {@link RowContainer#refreshRows(int, int)} needs to be called as
+     * appropriate.
+     * 
+     * @param offset
+     *            the index of the column before which new columns are inserted,
+     *            or {@link #getColumnCount()} to add new columns at the end
+     * @param numberOfColumns
+     *            the number of columns to insert after the <code>offset</code>
+     * @throws IndexOutOfBoundsException
+     *             if <code>offset</code> is not an integer in the range
+     *             <code>[0..{@link #getColumnCount()}]</code>
+     * @throws IllegalArgumentException
+     *             if {@code numberOfColumns} is less than 1.
+     */
+    public void insertColumns(int offset, int numberOfColumns)
+            throws IndexOutOfBoundsException, IllegalArgumentException;
+
+    /**
+     * Returns the number of columns in the escalator.
+     * 
+     * @return the number of columns in the escalator
+     */
+    public int getColumnCount();
+}
\ No newline at end of file
diff --git a/client/src/com/vaadin/client/ui/grid/Escalator.java b/client/src/com/vaadin/client/ui/grid/Escalator.java
new file mode 100644 (file)
index 0000000..7398650
--- /dev/null
@@ -0,0 +1,751 @@
+/*
+ * 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.logging.Logger;
+
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.Node;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.event.logical.shared.AttachEvent;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.client.ui.grid.PositionFunction.AbsolutePosition;
+import com.vaadin.client.ui.grid.PositionFunction.Translate3DPosition;
+import com.vaadin.client.ui.grid.PositionFunction.TranslatePosition;
+import com.vaadin.client.ui.grid.PositionFunction.WebkitTranslate3DPosition;
+
+/**
+ * A low-level table-like widget that features a scrolling virtual viewport and
+ * lazily generated rows.
+ * 
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public class Escalator extends Widget {
+
+    // todo comments legend
+    /*
+     * [[optimize]]: There's an opportunity to rewrite the code in such a way
+     * that it _might_ perform better (rememeber to measure, implement,
+     * re-measure)
+     */
+    /*
+     * [[escalator]]: This needs to be re-inspected once the escalator pattern
+     * is actually implemented.
+     */
+    /*
+     * [[rowwidth]] [[colwidth]]: This needs to be re-inspected once hard-coded
+     * values are removed, and cell dimensions are actually being calculated.
+     * NOTE: these bits can most often also be identified by searching for code
+     * reading the ROW_HEIGHT_PX and COL_WIDTH_PX constans.
+     */
+    /*
+     * [[API]]: Implementing this suggestion would require a change in the
+     * public API. These suggestions usually don't come lightly.
+     */
+
+    private static final int ROW_HEIGHT_PX = 20;
+    private static final int COLUMN_WIDTH_PX = 100;
+
+    private static class CellImpl implements Cell {
+        private final Element cellElem;
+        private final int row;
+        private final int column;
+
+        public CellImpl(final Element cellElem, final int row, final int column) {
+            this.cellElem = cellElem;
+            this.row = row;
+            this.column = column;
+        }
+
+        @Override
+        public int getRow() {
+            return row;
+        }
+
+        @Override
+        public int getColumn() {
+            return column;
+        }
+
+        @Override
+        public Element getElement() {
+            return cellElem;
+        }
+
+    }
+
+    private static final String CLASS_NAME = "v-escalator";
+
+    private class RowContainerImpl implements RowContainer {
+        private CellRenderer renderer = CellRenderer.NULL_RENDERER;
+
+        private int rows;
+
+        /**
+         * The table section element ({@code <thead>}, {@code <tbody>} or
+         * {@code <tfoot>}) the rows (i.e. {@code <tr>} tags) are contained in.
+         */
+        private final Element root;
+
+        /**
+         * What cell type to contain in this {@link RowContainer}. Usually
+         * either a {@code <th>} or {@code <td>}.
+         */
+        private final String cellElementTag;
+
+        public RowContainerImpl(final Element rowContainerElement,
+                final String cellElementTag) {
+            root = rowContainerElement;
+            this.cellElementTag = cellElementTag;
+        }
+
+        /**
+         * Informs the row container that the height of its respective table
+         * section has changed.
+         * <p>
+         * These calculations might affect some layouting logic, such as the
+         * body is being offset by the footer, the footer needs to be readjusted
+         * according to its height, and so on.
+         * <p>
+         * A table section is either header, body or footer.
+         * 
+         * @param newPxHeight
+         *            The new pixel height
+         */
+        protected void sectionHeightCalculated(final double newPxHeight) {
+            // override if implementation is needed
+        };
+
+        private Element createCellElement() {
+            return DOM.createElement(cellElementTag);
+        }
+
+        @Override
+        public CellRenderer getCellRenderer() {
+            return renderer;
+        }
+
+        /**
+         * {@inheritDoc}
+         * <p>
+         * <em>Implementation detail:</em> This method does no DOM modifications
+         * (i.e. is very cheap to call) if there is no data for rows or columns
+         * when this method is called.
+         * 
+         * @see #hasColumnAndRowData()
+         */
+        @Override
+        public void setCellRenderer(final CellRenderer cellRenderer) {
+            if (cellRenderer == null) {
+                throw new IllegalArgumentException(
+                        "cell renderer cannot be null");
+            }
+
+            renderer = cellRenderer;
+
+            if (hasColumnAndRowData() && getRowCount() > 0) {
+                refreshRows(0, getRowCount());
+            }
+        }
+
+        /**
+         * {@inheritDoc}
+         * <p>
+         * <em>Implementation detail:</em> This method does no DOM modifications
+         * (i.e. is very cheap to call) if there are no rows in the DOM when
+         * this method is called.
+         * 
+         * @see #hasSomethingInDom()
+         */
+        @Override
+        public void removeRows(final int offset, final int numberOfRows) {
+            assertArgumentsAreValidAndWithinRange(offset, numberOfRows);
+
+            rows -= numberOfRows;
+
+            if (hasSomethingInDom()) {
+                for (int i = 0; i < numberOfRows; i++) {
+                    root.getChild(offset).removeFromParent();
+                }
+            }
+            refreshRowPositions(offset, getRowCount());
+            recalculateSectionHeight();
+        }
+
+        private void assertArgumentsAreValidAndWithinRange(final int offset,
+                final int numberOfRows) throws IllegalArgumentException,
+                IndexOutOfBoundsException {
+            if (numberOfRows < 1) {
+                throw new IllegalArgumentException(
+                        "Number of rows must be 1 or greater (was "
+                                + numberOfRows + ")");
+            }
+
+            if (offset < 0 || offset + numberOfRows > getRowCount()) {
+                throw new IndexOutOfBoundsException("The given "
+                        + "row range (" + offset + ".."
+                        + (offset + numberOfRows)
+                        + ") was outside of the current number of rows ("
+                        + getRowCount() + ")");
+            }
+        }
+
+        @Override
+        public int getRowCount() {
+            return rows;
+        }
+
+        /**
+         * {@inheritDoc}
+         * <p>
+         * <em>Implementation detail:</em> This method does no DOM modifications
+         * (i.e. is very cheap to call) if there is no data for columns when
+         * this method is called.
+         * 
+         * @see #hasColumnAndRowData()
+         */
+        @Override
+        public void insertRows(final int offset, final int numberOfRows) {
+            if (offset < 0 || offset > getRowCount()) {
+                throw new IndexOutOfBoundsException("The given offset ("
+                        + offset
+                        + ") was outside of the current number of rows (0.."
+                        + getRowCount() + ")");
+            }
+
+            if (numberOfRows < 1) {
+                throw new IllegalArgumentException(
+                        "Number of rows must be 1 or greater (was "
+                                + numberOfRows + ")");
+            }
+
+            rows += numberOfRows;
+
+            /*
+             * TODO [[escalator]]: modify offset and numberOfRows so that they
+             * suit the current viewport. If a partial dataset is shown,update
+             * only the part that is visible. If the viewport doesn't show any
+             * of the modifications, this method does nothing.
+             */
+
+            /*
+             * TODO [[escalator]]: assert that escalatorChildIndex is a number
+             * equal or less than the number of escalator rows
+             */
+
+            Node referenceNode;
+            if (root.getChildCount() != 0 && offset != 0) {
+                // get the row node we're inserting stuff after
+                referenceNode = root.getChild(offset - 1);
+            } else {
+                // there are now rows, so just append.
+                referenceNode = null;
+            }
+
+            for (int row = offset; row < offset + numberOfRows; row++) {
+                final Element tr = DOM.createTR();
+
+                for (int col = 0; col < columnConfiguration.getColumnCount(); col++) {
+                    final Element cellElem = createCellElement();
+                    paintCell(cellElem, row, col);
+                    tr.appendChild(cellElem);
+                }
+
+                /*
+                 * TODO [[optimize]] [[rowwidth]]: When this method is updated
+                 * to measure things instead of using hardcoded values, it would
+                 * be better to do everything at once after all rows have been
+                 * updated to reduce the number of reflows.
+                 */
+                recalculateRowWidth(tr);
+                tr.addClassName(CLASS_NAME + "-row");
+
+                position.set(tr, 0, row * ROW_HEIGHT_PX);
+
+                if (referenceNode != null) {
+                    root.insertAfter(tr, referenceNode);
+                } else {
+                    /*
+                     * referencenode being null means we have offset 0, i.e.
+                     * make it the first row
+                     */
+                    /*
+                     * TODO [[optimize]]: Is insertFirst or append faster for an
+                     * empty root?
+                     */
+                    root.insertFirst(tr);
+                }
+
+                /*
+                 * to get the rows to appear one after another in a logical
+                 * order, update the reference
+                 */
+                referenceNode = tr;
+            }
+
+            /*
+             * we need to update the positions of all rows beneath the ones
+             * added right now.
+             */
+            refreshRowPositions(offset + numberOfRows, getRowCount());
+
+            /*
+             * TODO [[optimize]]: maybe the height doesn't always change?
+             */
+            recalculateSectionHeight();
+        }
+
+        /**
+         * Re-evaluates the positional coordinates for the rows in the given
+         * range. The given range is truncated to suit the given viewport.
+         * 
+         * @param offset
+         *            starting row index
+         * @param numberOfRows
+         *            the number of rows after {@code offset} to refresh the
+         *            positions of
+         */
+        private void refreshRowPositions(final int offset,
+                final int numberOfRows) {
+            final int startRow = Math.max(0, offset);
+            final int endRow = Math.min(getRowCount(), offset + numberOfRows);
+
+            for (int row = startRow; row < endRow; row++) {
+                Element tr = (Element) root.getChild(row);
+                position.set(tr, 0, row * ROW_HEIGHT_PX);
+            }
+        }
+
+        private void recalculateSectionHeight() {
+            /* TODO [[optimize]]: only do this if the height has changed */
+            sectionHeightCalculated(root.getChildCount() * ROW_HEIGHT_PX);
+        }
+
+        /**
+         * {@inheritDoc}
+         * <p>
+         * <em>Implementation detail:</em> This method does no DOM modifications
+         * (i.e. is very cheap to call) if there is no data for columns when
+         * this method is called.
+         * 
+         * @see #hasColumnAndRowData()
+         */
+        @Override
+        public void refreshRows(final int offset, final int numberOfRows) {
+            assertArgumentsAreValidAndWithinRange(offset, numberOfRows);
+
+            /*
+             * TODO [[escalator]]: modify offset and numberOfRows to fit in the
+             * current viewport. If they don't fall into the current viewport,
+             * NOOP
+             */
+
+            if (hasColumnAndRowData()) {
+                /*
+                 * TODO [[rowheight]]: nudge rows down with
+                 * refreshRowPositions() as needed
+                 */
+                /*
+                 * TODO [[colwidth]]: reapply column and colspan widths as
+                 * needed
+                 */
+
+                for (int row = offset; row < offset + numberOfRows; row++) {
+                    Node tr = root.getChild(row);
+                    for (int col = 0; col < tr.getChildCount(); col++) {
+                        paintCell((Element) tr.getChild(col), row, col);
+                    }
+                }
+            }
+        }
+
+        private void paintCell(final Element cellElem, final int row,
+                final int col) {
+            /*
+             * TODO [[optimize]]: Only do this for new cells or when a row
+             * height or column width actually changes. Or is it a NOOP when
+             * re-setting a property to its current value?
+             */
+            cellElem.getStyle().setHeight(ROW_HEIGHT_PX, Unit.PX);
+            cellElem.getStyle().setWidth(COLUMN_WIDTH_PX, Unit.PX);
+
+            /*
+             * TODO [[optimize]]: Don't create a new instance every time a cell
+             * is rendered
+             */
+            final CellImpl cell = new CellImpl(cellElem, row, col);
+            /*
+             * TODO [[optimize]] [[API]]: Let the renderer know whether the cell
+             * is new so that it can use a quicker route if it can deduct that
+             * the elements that it has put there in a previous rendering is
+             * still there and the contents only need to be updated.
+             */
+            renderer.renderCell(cell);
+
+            /*
+             * TODO [[optimize]]: Only do this for cells that have not already
+             * been rendered.
+             */
+            cellElem.addClassName(CLASS_NAME + "-cell");
+        }
+    }
+
+    private class ColumnConfigurationImpl implements ColumnConfiguration {
+        private int columns = 0;
+
+        /**
+         * {@inheritDoc}
+         * <p>
+         * <em>Implementation detail:</em> This method does no DOM modifications
+         * (i.e. is very cheap to call) if there are no rows in the DOM when
+         * this method is called.
+         * 
+         * @see #hasSomethingInDom()
+         */
+        @Override
+        public void removeColumns(final int offset, final int numberOfColumns) {
+            assertArgumentsAreValidAndWithinRange(offset, numberOfColumns);
+
+            columns--;
+
+            if (hasSomethingInDom()) {
+                for (RowContainerImpl rowContainer : rowContainers) {
+                    for (int row = 0; row < rowContainer.getRowCount(); row++) {
+                        Node tr = rowContainer.root.getChild(row);
+                        for (int col = 0; col < numberOfColumns; col++) {
+                            tr.getChild(offset).removeFromParent();
+                        }
+                    }
+                }
+            }
+        }
+
+        private void assertArgumentsAreValidAndWithinRange(final int offset,
+                final int numberOfColumns) {
+            if (numberOfColumns < 1) {
+                throw new IllegalArgumentException(
+                        "Number of columns can't be less than 1 (was "
+                                + numberOfColumns + ")");
+            }
+
+            if (offset < 0 || offset + numberOfColumns > getColumnCount()) {
+                throw new IndexOutOfBoundsException("The given "
+                        + "column range (" + offset + ".."
+                        + (offset + numberOfColumns)
+                        + ") was outside of the current "
+                        + "number of columns (" + getColumnCount() + ")");
+            }
+        }
+
+        /**
+         * {@inheritDoc}
+         * <p>
+         * <em>Implementation detail:</em> This method does no DOM modifications
+         * (i.e. is very cheap to call) if there is no data for rows when this
+         * method is called.
+         * 
+         * @see #hasColumnAndRowData()
+         */
+        @Override
+        public void insertColumns(final int offset, final int numberOfColumns) {
+            if (offset < 0 || offset > getColumnCount()) {
+                throw new IndexOutOfBoundsException("The given offset("
+                        + offset
+                        + ") was outside of the current number of columns (0.."
+                        + getColumnCount() + ")");
+            }
+
+            if (numberOfColumns < 1) {
+                throw new IllegalArgumentException(
+                        "Number of columns must be 1 or greater (was "
+                                + numberOfColumns);
+            }
+
+            columns += numberOfColumns;
+            if (!hasColumnAndRowData()) {
+                return;
+            }
+
+            for (final RowContainerImpl rowContainer : rowContainers) {
+                final Element element = rowContainer.root;
+
+                for (int row = 0; row < element.getChildCount(); row++) {
+                    final Element tr = (Element) element.getChild(row);
+
+                    Node referenceElement;
+                    if (offset != 0) {
+                        referenceElement = tr.getChild(offset - 1);
+                    } else {
+                        referenceElement = null;
+                    }
+
+                    for (int col = offset; col < offset + numberOfColumns; col++) {
+                        final Element cellElem = rowContainer
+                                .createCellElement();
+                        rowContainer.paintCell(cellElem, row, col);
+
+                        if (referenceElement != null) {
+                            tr.insertAfter(cellElem, referenceElement);
+                        } else {
+                            /*
+                             * referenceElement being null means we have offset
+                             * 0, make it the first cell.
+                             */
+                            /*
+                             * TODO [[optimize]]: Is insertFirst or append
+                             * faster for an empty tr?
+                             */
+                            tr.insertFirst(cellElem);
+                        }
+
+                        /*
+                         * update reference to insert cells in logical order,
+                         * the latter after the former
+                         */
+                        referenceElement = cellElem;
+                    }
+
+                    /*
+                     * TODO [[optimize]] [[colwidth]]: When this method is
+                     * updated to measure things instead of using hardcoded
+                     * values, it would be better to do everything at once after
+                     * all rows have been updated to reduce the number of
+                     * reflows.
+                     */
+                    recalculateRowWidth(tr);
+                }
+            }
+        }
+
+        @Override
+        public int getColumnCount() {
+            return columns;
+        }
+    }
+
+    private final Element headElem = DOM.createTHead();
+    private final Element bodyElem = DOM.createTBody();
+    private final Element footElem = DOM.createTFoot();
+    private final Element scroller;
+    private final Element innerScroller;
+
+    private final RowContainerImpl header = new RowContainerImpl(headElem, "th") {
+        @Override
+        protected void sectionHeightCalculated(final double newPxHeight) {
+            bodyElem.getStyle().setTop(newPxHeight, Unit.PX);
+        };
+    };
+
+    private final RowContainerImpl body = new RowContainerImpl(bodyElem, "td");
+
+    private final RowContainerImpl footer = new RowContainerImpl(footElem, "td") {
+        @Override
+        protected void sectionHeightCalculated(final double newPxHeight) {
+            footElem.getStyle().setBottom(newPxHeight, Unit.PX);
+        }
+    };
+
+    private final RowContainerImpl[] rowContainers = new RowContainerImpl[] {
+            header, body, footer };
+
+    private final ColumnConfigurationImpl columnConfiguration = new ColumnConfigurationImpl();
+    private final Element tableWrapper;
+
+    private PositionFunction position;
+
+    /**
+     * Creates a new Escalator widget instance.
+     */
+    public Escalator() {
+
+        detectAndApplyPositionFunction();
+
+        final Element root = DOM.createDiv();
+        setElement(root);
+        setStyleName(CLASS_NAME);
+
+        scroller = DOM.createDiv();
+        scroller.setClassName(CLASS_NAME + "-scroller");
+        root.appendChild(scroller);
+
+        innerScroller = DOM.createDiv();
+        scroller.appendChild(innerScroller);
+
+        tableWrapper = DOM.createDiv();
+        tableWrapper.setClassName(CLASS_NAME + "-tablewrapper");
+        root.appendChild(tableWrapper);
+
+        final Element table = DOM.createTable();
+        tableWrapper.appendChild(table);
+
+        headElem.setClassName(CLASS_NAME + "-header");
+        table.appendChild(headElem);
+
+        bodyElem.setClassName(CLASS_NAME + "-body");
+        table.appendChild(bodyElem);
+
+        footElem.setClassName(CLASS_NAME + "-footer");
+        table.appendChild(footElem);
+
+        /*
+         * Size calculations work only after the Escalator has been attached to
+         * the DOM. It doesn't matter if the table is populated or not by this
+         * point, there's a lot of other stuff to calculate also. All sizes
+         * start working once the first sizes have been initialized.
+         */
+        addAttachHandler(new AttachEvent.Handler() {
+            @Override
+            public void onAttachOrDetach(final AttachEvent event) {
+                if (event.isAttached()) {
+                    recalculateElementSizes();
+                }
+            }
+        });
+    }
+
+    private void detectAndApplyPositionFunction() {
+        final Style docStyle = Document.get().getBody().getStyle();
+        if (hasProperty(docStyle, "transform")) {
+            if (hasProperty(docStyle, "transformStyle")) {
+                position = new Translate3DPosition();
+            } else {
+                position = new TranslatePosition();
+            }
+        } else if (hasProperty(docStyle, "webkitTransform")) {
+            position = new WebkitTranslate3DPosition();
+        } else {
+            position = new AbsolutePosition();
+        }
+
+        getLogger().info(
+                "Using " + position.getClass().getSimpleName()
+                        + " for position");
+    }
+
+    private Logger getLogger() {
+        return Logger.getLogger(getClass().getName());
+    }
+
+    private static native boolean hasProperty(Style style, String name)
+    /*-{
+        return style[name] !== undefined;
+    }-*/;
+
+    /**
+     * Check whether there are both columns and any row data (for either
+     * headers, body or footer).
+     * 
+     * @return <code>true</code> iff header, body or footer has rows && there
+     *         are columns
+     */
+    private boolean hasColumnAndRowData() {
+        return (header.getRowCount() > 0 || body.getRowCount() > 0 || footer
+                .getRowCount() > 0) && columnConfiguration.getColumnCount() > 0;
+    }
+
+    /**
+     * Check whether there are any cells in the DOM.
+     * 
+     * @return <code>true</code> iff header, body or footer has any child
+     *         elements
+     */
+    private boolean hasSomethingInDom() {
+        return headElem.hasChildNodes() || bodyElem.hasChildNodes()
+                || footElem.hasChildNodes();
+    }
+
+    /**
+     * Returns the representation of this Escalator header.
+     * 
+     * @return the header. Never <code>null</code>
+     */
+    public RowContainer getHeader() {
+        return header;
+    }
+
+    /**
+     * Returns the representation of this Escalator body.
+     * 
+     * @return the body. Never <code>null</code>
+     */
+    public RowContainer getBody() {
+        return body;
+    }
+
+    /**
+     * Returns the representation of this Escalator footer.
+     * 
+     * @return the footer. Never <code>null</code>
+     */
+    public RowContainer getFooter() {
+        return footer;
+    }
+
+    /**
+     * Returns the configuration object for the columns in this Escalator.
+     * 
+     * @return the configuration object for the columns in this Escalator. Never
+     *         <code>null</code>
+     */
+    public ColumnConfiguration getColumnConfiguration() {
+        return columnConfiguration;
+    }
+
+    /*
+     * TODO remove method once RequiresResize and the Vaadin layoutmanager
+     * listening mechanisms are implemented (https://trello.com/c/r3Kh0Kfy)
+     */
+    @Override
+    public void setWidth(final String width) {
+        super.setWidth(width);
+        recalculateElementSizes();
+    }
+
+    /*
+     * TODO remove method once RequiresResize and the Vaadin layoutmanager
+     * listening mechanisms are implemented (https://trello.com/c/r3Kh0Kfy)
+     */
+    @Override
+    public void setHeight(final String height) {
+        super.setHeight(height);
+        recalculateElementSizes();
+    }
+
+    private void recalculateElementSizes() {
+        for (final RowContainerImpl rowContainer : rowContainers) {
+            rowContainer.recalculateSectionHeight();
+        }
+
+        /*
+         * TODO [[escalator]]: take scrollbar size into account only if there is
+         * something to scroll, and only for the dimension it applies to.
+         */
+        // recalculate required space for scroll underlay
+        tableWrapper.getStyle().setHeight(getElement().getOffsetHeight(),
+                Unit.PX);
+        tableWrapper.getStyle()
+                .setWidth(getElement().getOffsetWidth(), Unit.PX);
+    }
+
+    private static void recalculateRowWidth(Element tr) {
+        // TODO [[colwidth]]: adjust for variable column widths
+        tr.getStyle().setWidth(tr.getChildCount() * COLUMN_WIDTH_PX, Unit.PX);
+    }
+}
diff --git a/client/src/com/vaadin/client/ui/grid/PositionFunction.java b/client/src/com/vaadin/client/ui/grid/PositionFunction.java
new file mode 100644 (file)
index 0000000..24e119b
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+ * 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.dom.client.Style.Unit;
+import com.google.gwt.user.client.Element;
+
+/**
+ * A functional interface that can be used for positioning elements in the DOM.
+ * 
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+interface PositionFunction {
+    /**
+     * A position function using "transform: translate3d(x,y,z)" to position
+     * elements in the DOM.
+     */
+    public static class Translate3DPosition implements PositionFunction {
+        @Override
+        public void set(Element e, double x, double y) {
+            e.getStyle().setProperty("transform",
+                    "translate3d(" + x + "px, " + y + "px, 0)");
+        }
+    }
+
+    /**
+     * A position function using "transform: translate(x,y)" to position
+     * elements in the DOM.
+     */
+    public static class TranslatePosition implements PositionFunction {
+        @Override
+        public void set(Element e, double x, double y) {
+            e.getStyle().setProperty("transform",
+                    "translate(" + x + "px," + y + "px)");
+        }
+    }
+
+    /**
+     * A position function using "-webkit-transform: translate3d(x,y,z)" to
+     * position elements in the DOM.
+     */
+    public static class WebkitTranslate3DPosition implements PositionFunction {
+        @Override
+        public void set(Element e, double x, double y) {
+            e.getStyle().setProperty("webkitTransform",
+                    "translate3d(" + x + "px," + y + "px,0");
+        }
+    }
+
+    /**
+     * A position function using "left: x" and "top: y" to position elements in
+     * the DOM.
+     */
+    public static class AbsolutePosition implements PositionFunction {
+        @Override
+        public void set(Element e, double x, double y) {
+            e.getStyle().setLeft(x, Unit.PX);
+            e.getStyle().setTop(y, Unit.PX);
+        }
+    }
+
+    /**
+     * Position an element in an (x,y) coordinate system in the DOM.
+     * 
+     * @param e
+     *            the element to position. Never <code>null</code>.
+     * @param x
+     *            the x coordinate, in pixels
+     * @param y
+     *            the y coordinate, in pixels
+     */
+    void set(Element e, double x, double y);
+}
\ No newline at end of file
diff --git a/client/src/com/vaadin/client/ui/grid/RowContainer.java b/client/src/com/vaadin/client/ui/grid/RowContainer.java
new file mode 100644 (file)
index 0000000..1a1a200
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+ * 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;
+
+/**
+ * A representation of the rows in each of the sections (header, body and
+ * footer) in an {@link Escalator}.
+ * 
+ * @since 7.2
+ * @author Vaadin Ltd
+ * @see Escalator#getHeader()
+ * @see Escalator#getBody()
+ * @see Escalator#getFooter()
+ */
+public interface RowContainer {
+    /**
+     * Returns the current {@link CellRenderer} used to render cells.
+     * 
+     * @return the current cell renderer
+     */
+    public CellRenderer getCellRenderer();
+
+    /**
+     * Sets the {@link CellRenderer} to use when displaying data in the
+     * escalator.
+     * 
+     * @param cellRenderer
+     *            the cell renderer to use to render cells. May not be
+     *            <code>null</code>
+     * @throws IllegalArgumentException
+     *             if {@code cellRenderer} is <code>null</code>
+     * @see CellRenderer#NULL_RENDERER
+     */
+    public void setCellRenderer(CellRenderer cellRenderer)
+            throws IllegalArgumentException;
+
+    /**
+     * Removes rows at a certain offset in the current row container.
+     * 
+     * @param offset
+     *            the index of the first row to be removed
+     * @param numberOfRows
+     *            the number of rows to remove, starting from the offset
+     * @throws IndexOutOfBoundsException
+     *             if any integer number in the range
+     *             <code>[offset..(offset+numberOfRows)]</code> is not an
+     *             existing row index
+     * @throws IllegalArgumentException
+     *             if {@code numberOfRows} is less than 1.
+     */
+    public void removeRows(int offset, int numberOfRows)
+            throws IndexOutOfBoundsException, IllegalArgumentException;
+
+    /**
+     * Adds rows at a certain offset in this row container.
+     * <p>
+     * The new rows will be inserted between the row at the offset, and the row
+     * before (an offset of 0 means that the rows are inserted at the
+     * beginning). Therefore, the rows currently at the offset and afterwards
+     * will be moved downwards.
+     * <p>
+     * The contents of the inserted rows will subsequently be queried from the
+     * cell renderer.
+     * <p>
+     * <em>Note:</em> Only the contents of the inserted rows will be rendered.
+     * If inserting new rows affects the contents of existing rows,
+     * {@link #refreshRows(int, int)} needs to be called for those rows
+     * separately.
+     * 
+     * @param offset
+     *            the index of the row before which new rows are inserted, or
+     *            {@link #getRowCount()} to add rows at the end
+     * @param numberOfRows
+     *            the number of rows to insert after the <code>offset</code>
+     * @see #setCellRenderer(CellRenderer)
+     * @throws IndexOutOfBoundsException
+     *             if <code>offset</code> is not an integer in the range
+     *             <code>[0..{@link #getRowCount()}]</code>
+     * @throws IllegalArgumentException
+     *             if {@code numberOfRows} is less than 1.
+     */
+    public void insertRows(int offset, int numberOfRows)
+            throws IndexOutOfBoundsException, IllegalArgumentException;
+
+    /**
+     * Refreshes a range of rows in the current row container.
+     * <p>
+     * The data for the refreshed rows are queried from the current cell
+     * renderer.
+     * 
+     * @param offset
+     *            the index of the first row that will be updated
+     * @param numberOfRows
+     *            the number of rows to update, starting from the offset
+     * @see #setCellRenderer(CellRenderer)
+     * @throws IndexOutOfBoundsException
+     *             if any integer number in the range
+     *             <code>[offset..(offset+numberOfColumns)]</code> is not an
+     *             existing column index.
+     * @throws IllegalArgumentException
+     *             if {@code numberOfRows} is less than 1.
+     */
+    public void refreshRows(int offset, int numberOfRows)
+            throws IndexOutOfBoundsException, IllegalArgumentException;
+
+    /**
+     * Gets the number of rows in the current row container.
+     * 
+     * @return the number of rows in the current row container
+     */
+    public int getRowCount();
+}
\ No newline at end of file
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridTest.java b/uitest/src/com/vaadin/tests/components/grid/GridTest.java
new file mode 100644 (file)
index 0000000..cd8a134
--- /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.tests.components.grid;
+
+import com.vaadin.annotations.Widgetset;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.components.AbstractTestUI;
+import com.vaadin.tests.widgetset.TestingWidgetSet;
+import com.vaadin.tests.widgetset.server.grid.TestGrid;
+
+/**
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+@Widgetset(TestingWidgetSet.NAME)
+public class GridTest extends AbstractTestUI {
+    @Override
+    protected void setup(VaadinRequest request) {
+        addComponent(new TestGrid());
+    }
+
+    @Override
+    protected String getTestDescription() {
+        return null;
+    }
+
+    @Override
+    protected Integer getTicketNumber() {
+        return null;
+    }
+
+}
diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridConnector.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridConnector.java
new file mode 100644 (file)
index 0000000..ef624d6
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * 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.widgetset.client.grid;
+
+import com.vaadin.client.ui.AbstractComponentConnector;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.tests.widgetset.server.grid.TestGrid;
+
+/**
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+@Connect(TestGrid.class)
+public class TestGridConnector extends AbstractComponentConnector {
+    @Override
+    protected void init() {
+        super.init();
+    }
+
+    @Override
+    public VTestGrid getWidget() {
+        return (VTestGrid) super.getWidget();
+    }
+
+    @Override
+    public TestGridState getState() {
+        return (TestGridState) super.getState();
+    }
+}
diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridState.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridState.java
new file mode 100644 (file)
index 0000000..9aeca0b
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ * 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.widgetset.client.grid;
+
+import com.vaadin.shared.AbstractComponentState;
+
+/**
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public class TestGridState extends AbstractComponentState {
+    public static final String DEFAULT_HEIGHT = "400px";
+
+    /* TODO: this should be "100%" before setting final. */
+    public static final String DEFAULT_WIDTH = "800px";
+}
diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/VTestGrid.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/VTestGrid.java
new file mode 100644 (file)
index 0000000..274b01b
--- /dev/null
@@ -0,0 +1,113 @@
+package com.vaadin.tests.widgetset.client.grid;
+
+import com.google.gwt.user.client.ui.Composite;
+import com.vaadin.client.ui.grid.Cell;
+import com.vaadin.client.ui.grid.CellRenderer;
+import com.vaadin.client.ui.grid.ColumnConfiguration;
+import com.vaadin.client.ui.grid.Escalator;
+import com.vaadin.client.ui.grid.RowContainer;
+
+public class VTestGrid extends Composite {
+    public static class HeaderRenderer implements CellRenderer {
+        private int i = 0;
+
+        @Override
+        public void renderCell(final Cell cell) {
+            cell.getElement().setInnerText("Header " + (i++));
+        }
+    }
+
+    public static class BodyRenderer implements CellRenderer {
+        private int i = 0;
+
+        @Override
+        public void renderCell(final Cell cell) {
+            cell.getElement().setInnerText("Cell #" + (i++));
+
+            double c = i * .1;
+            int r = (int) ((Math.cos(c) + 1) * 128);
+            int g = (int) ((Math.cos(c / Math.PI) + 1) * 128);
+            int b = (int) ((Math.cos(c / (Math.PI * 2)) + 1) * 128);
+            cell.getElement().getStyle()
+                    .setBackgroundColor("rgb(" + r + "," + g + "," + b + ")");
+            if ((r + g + b) / 3 < 127) {
+                cell.getElement().getStyle().setColor("white");
+            }
+        }
+    }
+
+    public static class FooterRenderer implements CellRenderer {
+        private int i = 0;
+
+        @Override
+        public void renderCell(final Cell cell) {
+            cell.getElement().setInnerText("Footer " + (i++));
+        }
+    }
+
+    private Escalator escalator = new Escalator();
+
+    public VTestGrid() {
+        initWidget(escalator);
+
+        final ColumnConfiguration cConf = escalator.getColumnConfiguration();
+        cConf.insertColumns(0, 1);
+        cConf.insertColumns(0, 1); // prepend one column
+        cConf.insertColumns(cConf.getColumnCount(), 1); // append one column
+        // cConf.insertColumns(cConf.getColumnCount(), 10); // append 10 columns
+
+        final RowContainer h = escalator.getHeader();
+        h.setCellRenderer(new HeaderRenderer());
+        h.insertRows(0, 1);
+
+        final RowContainer b = escalator.getBody();
+        b.setCellRenderer(new BodyRenderer());
+        b.insertRows(0, 5);
+
+        final RowContainer f = escalator.getFooter();
+        f.setCellRenderer(new FooterRenderer());
+        f.insertRows(0, 1);
+
+        b.removeRows(3, 2);
+        // iterative transformations for testing.
+        // step2();
+        // step3();
+        // step4();
+        // step5();
+        // step6();
+
+        setWidth(TestGridState.DEFAULT_WIDTH);
+        setHeight(TestGridState.DEFAULT_HEIGHT);
+    }
+
+    private void step2() {
+        RowContainer b = escalator.getBody();
+        b.insertRows(0, 5); // prepend five rows
+        b.insertRows(b.getRowCount(), 5); // append five rows
+    }
+
+    private void step3() {
+        ColumnConfiguration cConf = escalator.getColumnConfiguration();
+        cConf.insertColumns(0, 1); // prepend one column
+        cConf.insertColumns(cConf.getColumnCount(), 1); // append one column
+    }
+
+    private void step4() {
+        final ColumnConfiguration cConf = escalator.getColumnConfiguration();
+        cConf.removeColumns(0, 1);
+        cConf.removeColumns(1, 1);
+        cConf.removeColumns(cConf.getColumnCount() - 1, 1);
+    }
+
+    private void step5() {
+        final RowContainer b = escalator.getBody();
+        b.removeRows(0, 1);
+        b.removeRows(b.getRowCount() - 1, 1);
+    }
+
+    private void step6() {
+        RowContainer b = escalator.getBody();
+        b.refreshRows(0, b.getRowCount());
+    }
+
+}
diff --git a/uitest/src/com/vaadin/tests/widgetset/server/grid/TestGrid.java b/uitest/src/com/vaadin/tests/widgetset/server/grid/TestGrid.java
new file mode 100644 (file)
index 0000000..7ae7e03
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * 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.widgetset.server.grid;
+
+import com.vaadin.tests.widgetset.client.grid.TestGridState;
+import com.vaadin.ui.AbstractComponent;
+
+/**
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public class TestGrid extends AbstractComponent {
+    public TestGrid() {
+        setWidth(TestGridState.DEFAULT_WIDTH);
+        setHeight(TestGridState.DEFAULT_HEIGHT);
+    }
+
+    @Override
+    protected TestGridState getState() {
+        return (TestGridState) super.getState();
+    }
+}