]> source.dussan.org Git - vaadin-framework.git/commitdiff
Add support for colspan in Escalator (#12645)
authorHenrik Paul <henrik@vaadin.com>
Mon, 25 Nov 2013 10:32:52 +0000 (12:32 +0200)
committerVaadin Code Review <review@vaadin.com>
Wed, 27 Nov 2013 11:57:58 +0000 (11:57 +0000)
Change-Id: I252fe949537154bce7e09d42f434bce20ec28928

client/src/com/vaadin/client/ui/grid/Cell.java
client/src/com/vaadin/client/ui/grid/Escalator.java
client/src/com/vaadin/client/ui/grid/EscalatorUpdater.java
client/src/com/vaadin/client/ui/grid/FlyweightCell.java
client/src/com/vaadin/client/ui/grid/FlyweightRow.java
client/src/com/vaadin/client/ui/grid/Grid.java
uitest/src/com/vaadin/tests/widgetset/client/grid/VTestGrid.java

index b06ad582b2dea8177603be3eddbc04be6c3150e5..09bc5da344dc284c937d1a68d5f66a33a51df337 100644 (file)
@@ -47,9 +47,27 @@ public interface Cell {
      * 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.
+     * Avoid modifying the dimensions, positioning or colspan of the cell
+     * element.
      * 
      * @return The root element for this cell. Never <code>null</code>.
      */
     public Element getElement();
+
+    /**
+     * Sets the column span of the cell.
+     * <p>
+     * This will overwrite any possible "colspan" attribute in the current
+     * element (i.e. the object returned by {@link #getElement()}). This will
+     * also handle internal bookkeeping, skip the rendering of any affected
+     * adjacent cells, and make sure that the current cell's dimensions are
+     * handled correctly.
+     * 
+     * @param numberOfCells
+     *            the number of cells to span to the right, or <code>1</code> to
+     *            unset any column spans
+     * @throws IllegalArgumentException
+     *             if <code>numberOfCells &lt; 1</code>
+     */
+    public void setColSpan(int numberOfCells) throws IllegalArgumentException;
 }
\ No newline at end of file
index 431b8715adab590ef0e33b0d5211ecbf13761421..2f5f28871b2ae6be556d41aae38164e0ba14743b 100644 (file)
@@ -520,7 +520,7 @@ public class Escalator extends Widget {
     }
 
     private static final int ROW_HEIGHT_PX = 20;
-    private static final int COLUMN_WIDTH_PX = 100;
+    static final int COLUMN_WIDTH_PX = 100;
 
     /** An inner class that handles all logic related to scrolling. */
     private class Scroller extends JsniWorkaround {
@@ -1257,12 +1257,21 @@ public class Escalator extends Widget {
             // this needs to be after the scroll position adjustment above.
             scroller.recalculateScrollbarsForVirtualViewport();
 
+            /*
+             * Because we might remove columns where affected by colspans, it's
+             * easiest to simply redraw everything when columns are modified.
+             * 
+             * Yes, this is a TODO [[optimize]].
+             */
+            if (getRowCount() > 0
+                    && getColumnConfiguration().getColumnCount() > 0) {
+                refreshRows(0, getRowCount());
+            }
         }
 
         protected void paintInsertColumns(final int offset,
                 final int numberOfColumns, boolean frozen) {
             final NodeList<Node> childNodes = root.getChildNodes();
-            final int topVisualRowLogicalIndex = getTopVisualRowLogicalIndex();
 
             for (int row = 0; row < childNodes.getLength(); row++) {
                 final Element tr = getTrByVisualIndex(row);
@@ -1280,8 +1289,6 @@ public class Escalator extends Widget {
                             cellElem, referenceCell);
                 }
 
-                refreshRow(tr, topVisualRowLogicalIndex + row);
-
                 /*
                  * TODO [[optimize]] [[colwidth]]: When this method is updated
                  * to measure things instead of using hardcoded values, it would
@@ -1308,6 +1315,17 @@ public class Escalator extends Widget {
                         .setScrollPos((int) (scroller.lastScrollLeft + numberOfColumns
                                 * COLUMN_WIDTH_PX));
             }
+
+            /*
+             * Because we might insert columns where affected by colspans, it's
+             * easiest to simply redraw everything when columns are modified.
+             * 
+             * Yes, this is a TODO [[optimize]].
+             */
+            if (getRowCount() > 0
+                    && getColumnConfiguration().getColumnCount() > 1) {
+                refreshRows(0, getRowCount());
+            }
         }
 
         public void setColumnFrozen(int column, boolean frozen) {
@@ -2216,6 +2234,9 @@ public class Escalator extends Widget {
         private Range convertToVisual(final Range logicalRange) {
             if (logicalRange.isEmpty()) {
                 return logicalRange;
+            } else if (visualRowOrder.isEmpty()) {
+                // empty range
+                return Range.withLength(0, 0);
             }
 
             /*
index 9a48dbffd806ae10f2305169400a3953374089f9..283517fcc4c97fd5fcac3d42c8493c0a0cfc8613 100644 (file)
@@ -16,7 +16,6 @@
 
 package com.vaadin.client.ui.grid;
 
-import java.util.List;
 
 /**
  * A functional interface that allows client code to define how a certain row in
@@ -37,7 +36,8 @@ public interface EscalatorUpdater {
     /** An {@link EscalatorUpdater} that doesn't render anything. */
     public static final EscalatorUpdater NULL = new EscalatorUpdater() {
         @Override
-        public void updateCells(final Row row, final List<Cell> cellsToUpdate) {
+        public void updateCells(final Row row,
+                final Iterable<Cell> cellsToUpdate) {
             // NOOP
         }
     };
@@ -62,5 +62,5 @@ public interface EscalatorUpdater {
      *            You should neither store nor reuse the reference to the list,
      *            nor to the individual cells
      */
-    public void updateCells(Row row, List<Cell> cellsToUpdate);
+    public void updateCells(Row row, Iterable<Cell> cellsToUpdate);
 }
index 88f05fafe5313b4833829abbaaab195fa13b042c..08c27fa859fcc0ec7f996317a6356909d4d0698b 100644 (file)
  */
 package com.vaadin.client.ui.grid;
 
+import java.util.List;
+
+import com.google.gwt.dom.client.Style.Display;
+import com.google.gwt.dom.client.Style.Unit;
 import com.google.gwt.user.client.Element;
+import com.vaadin.client.ui.grid.FlyweightRow.CellIterator;
 
 /**
  * An internal implementation of the {@link Cell} interface.
@@ -30,8 +35,11 @@ import com.google.gwt.user.client.Element;
  * @see FlyweightRow#removeCells(int, int)
  */
 class FlyweightCell implements Cell {
+    private static final String COLSPAN_ATTR = "colSpan";
+
     private final int column;
     private final FlyweightRow row;
+    private CellIterator currentIterator = null;
 
     public FlyweightCell(final FlyweightRow row, final int column) {
         this.row = row;
@@ -40,11 +48,13 @@ class FlyweightCell implements Cell {
 
     @Override
     public int getRow() {
+        assertSetup();
         return row.getRow();
     }
 
     @Override
     public int getColumn() {
+        assertSetup();
         return column;
     }
 
@@ -53,4 +63,87 @@ class FlyweightCell implements Cell {
         return (Element) row.getElement().getChild(column);
     }
 
+    void setup(final CellIterator cellIterator) {
+        currentIterator = cellIterator;
+
+        final Element e = getElement();
+        e.setPropertyInt(COLSPAN_ATTR, 1);
+        e.getStyle().setWidth(Escalator.COLUMN_WIDTH_PX, Unit.PX);
+        e.getStyle().clearDisplay();
+    }
+
+    /**
+     * Tear down the state of the Cell.
+     * <p>
+     * This is an internal check method, to prevent retrieving uninitialized
+     * data by calling {@link #getRow()}, {@link #getColumn()} or
+     * {@link #getElement()} at an improper time.
+     * <p>
+     * This should only be used with asserts ("
+     * <code>assert flyweightCell.teardown()</code> ") so that the code is never
+     * run when asserts aren't enabled.
+     * 
+     * @return always <code>true</code>
+     * @see FlyweightRow#teardown()
+     */
+    boolean teardown() {
+        currentIterator = null;
+        return true;
+    }
+
+    /**
+     * Asserts that the flyweight cell has properly been set up before trying to
+     * access any of its data.
+     */
+    private void assertSetup() {
+        assert currentIterator != null : "FlyweightCell was not properly "
+                + "initialized. This is either a bug in Grid/Escalator "
+                + "or a Cell reference has been stored and reused "
+                + "inappropriately.";
+    }
+
+    @Override
+    public void setColSpan(final int numberOfCells) {
+        /*-
+         * This will default to 1 if unset, as per DOM specifications:
+         * http://www.w3.org/TR/html5/tabular-data.html#attributes-common-to-td-and-th-elements
+         */
+        final int prevColSpan = getElement().getPropertyInt(COLSPAN_ATTR);
+        if (numberOfCells == 1 && prevColSpan == 1) {
+            return;
+        }
+
+        getElement().setPropertyInt(COLSPAN_ATTR, numberOfCells);
+        adjustCellWidthForSpan(numberOfCells);
+        hideOrRevealAdjacentCellElements(numberOfCells, prevColSpan);
+        currentIterator.setSkipNext(numberOfCells - 1);
+    }
+
+    private void adjustCellWidthForSpan(final int numberOfCells) {
+        final List<FlyweightCell> cellsToTheRight = currentIterator
+                .rawPeekNext(numberOfCells - 1);
+        final int widthsOfColumnsToTheRight = cellsToTheRight.size()
+                * Escalator.COLUMN_WIDTH_PX;
+        final int selfWidth = Escalator.COLUMN_WIDTH_PX;
+        getElement().getStyle().setWidth(selfWidth + widthsOfColumnsToTheRight,
+                Unit.PX);
+    }
+
+    private void hideOrRevealAdjacentCellElements(final int numberOfCells,
+            final int prevColSpan) {
+        final int affectedCellsNumber = Math.max(prevColSpan, numberOfCells);
+        final List<FlyweightCell> affectedCells = currentIterator
+                .rawPeekNext(affectedCellsNumber - 1);
+        if (prevColSpan < numberOfCells) {
+            for (int i = 0; i < affectedCells.size(); i++) {
+                affectedCells.get(prevColSpan + i - 1).getElement().getStyle()
+                        .setDisplay(Display.NONE);
+            }
+        } else if (prevColSpan > numberOfCells) {
+            for (int i = 0; i < affectedCells.size(); i++) {
+                affectedCells.get(numberOfCells + i - 1).getElement()
+                        .getStyle().clearDisplay();
+            }
+        }
+    }
 }
index dae27585274f20c743463a227b23fadcc7a96449..fc1c9770524df80adc799981479be63f61614d0e 100644 (file)
@@ -16,7 +16,8 @@
 package com.vaadin.client.ui.grid;
 
 import java.util.ArrayList;
-import java.util.Collections;
+import java.util.Collection;
+import java.util.Iterator;
 import java.util.List;
 
 import com.google.gwt.dom.client.Node;
@@ -33,12 +34,77 @@ import com.google.gwt.user.client.Element;
  * @see Escalator.AbstractRowContainer#refreshRow(Node, int)
  */
 class FlyweightRow implements Row {
+
+    static class CellIterator implements Iterator<Cell> {
+        /** A defensive copy of the cells in the current row. */
+        private final ArrayList<FlyweightCell> cells;
+        private int cursor = 0;
+        private int skipNext = 0;
+
+        public CellIterator(final Collection<FlyweightCell> cells) {
+            this.cells = new ArrayList<FlyweightCell>(cells);
+        }
+
+        @Override
+        public boolean hasNext() {
+            return cursor + skipNext < cells.size();
+        }
+
+        @Override
+        public FlyweightCell next() {
+            // if we needed to skip some cells since the last invocation.
+            for (int i = 0; i < skipNext; i++) {
+                cells.remove(cursor);
+            }
+            skipNext = 0;
+
+            final FlyweightCell cell = cells.get(cursor++);
+            cell.setup(this);
+            return cell;
+        }
+
+        @Override
+        public void remove() {
+            throw new UnsupportedOperationException(
+                    "Cannot remove cells via iterator");
+        }
+
+        /**
+         * Sets the number of cells to skip when {@link #next()} is called the
+         * next time. Cell hiding is also handled eagerly in this method.
+         * 
+         * @param colspan
+         *            the number of cells to skip on next invocation of
+         *            {@link #next()}
+         */
+        public void setSkipNext(final int colspan) {
+            assert colspan > 0 : "Number of cells didn't make sense: "
+                    + colspan;
+            skipNext = colspan;
+        }
+
+        /**
+         * Gets the next <code>n</code> cells in the iterator, ignoring any
+         * possibly spanned cells.
+         * 
+         * @param n
+         *            the number of next cells to retrieve
+         * @return A list of next <code>n</code> cells, or less if there aren't
+         *         enough cells to retrieve
+         */
+        public List<FlyweightCell> rawPeekNext(final int n) {
+            final int from = Math.min(cursor, cells.size());
+            final int to = Math.min(cursor + n, cells.size());
+            return cells.subList(from, to);
+        }
+    }
+
     private static final int BLANK = Integer.MIN_VALUE;
 
     private int row;
     private Element element;
     private final Escalator escalator;
-    private final List<Cell> cells = new ArrayList<Cell>();
+    private final List<FlyweightCell> cells = new ArrayList<FlyweightCell>();
 
     public FlyweightRow(final Escalator escalator) {
         this.escalator = escalator;
@@ -70,6 +136,9 @@ class FlyweightRow implements Row {
     boolean teardown() {
         element = null;
         row = BLANK;
+        for (final FlyweightCell cell : cells) {
+            assert cell.teardown();
+        }
         return true;
     }
 
@@ -116,9 +185,14 @@ class FlyweightRow implements Row {
      * @see #setup(Element, int)
      * @see #teardown()
      */
-    List<Cell> getCells() {
+    Iterable<Cell> getCells() {
         assertSetup();
-        return Collections.unmodifiableList(cells);
+        return new Iterable<Cell>() {
+            @Override
+            public Iterator<Cell> iterator() {
+                return new CellIterator(cells);
+            }
+        };
     }
 
     /**
index 90c8b60474b66fbe103386c6fd18f300ff14446a..66f37c762095d9d3b829fcc18732b8ce182faaac 100644 (file)
@@ -304,7 +304,7 @@ public class Grid<T> extends Composite {
         public abstract boolean firstRowIsVisible();
 
         @Override
-        public void updateCells(Row row, List<Cell> cellsToUpdate) {
+        public void updateCells(Row row, Iterable<Cell> cellsToUpdate) {
 
             int rowIndex;
             if (inverted) {
@@ -423,7 +423,7 @@ public class Grid<T> extends Composite {
         return new EscalatorUpdater() {
 
             @Override
-            public void updateCells(Row row, List<Cell> cellsToUpdate) {
+            public void updateCells(Row row, Iterable<Cell> cellsToUpdate) {
                 int rowIndex = row.getRow();
                 if (dataSource == null) {
                     setCellsLoading(cellsToUpdate);
@@ -443,7 +443,7 @@ public class Grid<T> extends Composite {
                 }
             }
 
-            private void setCellsLoading(List<Cell> cellsToUpdate) {
+            private void setCellsLoading(Iterable<Cell> cellsToUpdate) {
                 for (Cell cell : cellsToUpdate) {
                     cell.getElement().setInnerText("...");
                 }
index e9ee461fb950e742a7b736828a45052c2e51c9a3..18732549c0d3598aeb1342c22e9cf4fd081bd409 100644 (file)
@@ -42,8 +42,12 @@ public class VTestGrid extends Composite {
             return new EscalatorUpdater() {
                 @Override
                 public void updateCells(final Row row,
-                        final List<Cell> cellsToUpdate) {
+                        final Iterable<Cell> cellsToUpdate) {
                     for (final Cell cell : cellsToUpdate) {
+                        if (cell.getColumn() % 3 == 0) {
+                            cell.setColSpan(2);
+                        }
+
                         final Integer columnName = columns
                                 .get(cell.getColumn());
                         cell.getElement().setInnerText("Header " + columnName);
@@ -56,8 +60,12 @@ public class VTestGrid extends Composite {
             return new EscalatorUpdater() {
                 @Override
                 public void updateCells(final Row row,
-                        final List<Cell> cellsToUpdate) {
+                        final Iterable<Cell> cellsToUpdate) {
                     for (final Cell cell : cellsToUpdate) {
+                        if (cell.getColumn() % 3 == 1) {
+                            cell.setColSpan(2);
+                        }
+
                         final Integer columnName = columns
                                 .get(cell.getColumn());
                         cell.getElement().setInnerText("Footer " + columnName);
@@ -83,6 +91,10 @@ public class VTestGrid extends Composite {
                                 "Row " + cell.getRow() + ": " + cellInfo);
                     }
 
+                    if (cell.getColumn() % 3 == cell.getRow() % 3) {
+                        cell.setColSpan(3);
+                    }
+
                     final double c = i * .1;
                     final int r = (int) ((Math.cos(c) + 1) * 128);
                     final int g = (int) ((Math.cos(c / Math.PI) + 1) * 128);
@@ -102,7 +114,7 @@ public class VTestGrid extends Composite {
 
                 @Override
                 public void updateCells(final Row row,
-                        final List<Cell> cellsToUpdate) {
+                        final Iterable<Cell> cellsToUpdate) {
                     for (final Cell cell : cellsToUpdate) {
                         renderCell(cell);
                     }