summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--WebContent/VAADIN/themes/base/escalator/escalator.scss48
-rw-r--r--client/src/com/vaadin/client/ui/grid/ColumnConfiguration.java28
-rw-r--r--client/src/com/vaadin/client/ui/grid/Escalator.java1773
-rw-r--r--client/src/com/vaadin/client/ui/grid/PositionFunction.java2
-rw-r--r--client/src/com/vaadin/client/ui/grid/Range.java362
-rw-r--r--client/src/com/vaadin/client/ui/grid/RowContainer.java38
-rw-r--r--client/src/com/vaadin/client/ui/grid/ScrollDestination.java45
-rw-r--r--client/tests/src/com/vaadin/client/ui/grid/PartitioningTest.java102
-rw-r--r--client/tests/src/com/vaadin/client/ui/grid/RangeTest.java212
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridTest.html151
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridTest.java86
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridClientRpc.java28
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridConnector.java23
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridState.java3
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/grid/VTestGrid.java58
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/server/grid/TestGrid.java21
16 files changed, 2737 insertions, 243 deletions
diff --git a/WebContent/VAADIN/themes/base/escalator/escalator.scss b/WebContent/VAADIN/themes/base/escalator/escalator.scss
index 9dad07d3e0..461a3050e9 100644
--- a/WebContent/VAADIN/themes/base/escalator/escalator.scss
+++ b/WebContent/VAADIN/themes/base/escalator/escalator.scss
@@ -31,34 +31,54 @@ $border-color: #aaa;
width: inherit; /* a decent default fallback */
}
-.#{$primaryStyleName}-header {
+.#{$primaryStyleName}-header,
+.#{$primaryStyleName}-body,
+.#{$primaryStyleName}-footer {
position: absolute;
- top: 0;
left: 0;
width: inherit;
z-index: 10;
}
+.#{$primaryStyleName}-header { top: 0; }
+.#{$primaryStyleName}-footer { bottom: 0; }
+
.#{$primaryStyleName}-body {
- position: absolute;
+ z-index: 0;
top: 0;
- left: 0;
- width: inherit;
+
+ .#{$primaryStyleName}-row {
+ position: absolute;
+ top: 0;
+ left: 0;
+ }
}
-.#{$primaryStyleName}-footer {
- position: absolute;
- bottom: 0;
- left: 0;
- width: inherit;
+.#{$primaryStyleName}-row {
+ display: block;
+
+ .v-ie8 & {
+ /* IE8 doesn't let table rows be longer than body only with display block. Moar hax. */
+ float: left;
+ clear: left;
+
+ /*
+ * The inline style of margin-top from the <tbody> to offset the header's dimension is,
+ * for some strange reason, inherited into each contained <tr>.
+ * We need to cancel it:
+ */
+ margin-top: 0;
+ }
+
+ > td, > th {
+ /* IE8 likes the bgcolor here instead of on the row */
+ background-color: $background-color;
+ }
}
+
.#{$primaryStyleName}-row {
- position: absolute;
width: inherit;
- top: 0;
- left: 0;
- background-color: $background-color;
}
.#{$primaryStyleName}-cell {
diff --git a/client/src/com/vaadin/client/ui/grid/ColumnConfiguration.java b/client/src/com/vaadin/client/ui/grid/ColumnConfiguration.java
index be60b1a49b..fdd6a25382 100644
--- a/client/src/com/vaadin/client/ui/grid/ColumnConfiguration.java
+++ b/client/src/com/vaadin/client/ui/grid/ColumnConfiguration.java
@@ -26,29 +26,29 @@ package com.vaadin.client.ui.grid;
public interface ColumnConfiguration {
/**
- * Removes columns at a certain offset.
+ * Removes columns at a certain index.
*
- * @param offset
+ * @param index
* the index of the first column to be removed
* @param numberOfColumns
- * the number of rows to remove, starting from the offset
+ * the number of rows to remove, starting from the index
* @throws IndexOutOfBoundsException
* if any integer in the range
- * <code>[offset..(offset+numberOfColumns)]</code> is not an
+ * <code>[index..(index+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)
+ public void removeColumns(int index, int numberOfColumns)
throws IndexOutOfBoundsException, IllegalArgumentException;
/**
- * Adds columns at a certain offset.
+ * Adds columns at a certain index.
* <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.
+ * The new columns will be inserted between the column at the index, and the
+ * column before (an index of 0 means that the columns are inserted at the
+ * beginning). Therefore, the columns at the index 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.
@@ -58,18 +58,18 @@ public interface ColumnConfiguration {
* columns, {@link RowContainer#refreshRows(int, int)} needs to be called as
* appropriate.
*
- * @param offset
+ * @param index
* 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>
+ * the number of columns to insert after the <code>index</code>
* @throws IndexOutOfBoundsException
- * if <code>offset</code> is not an integer in the range
+ * if <code>index</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)
+ public void insertColumns(int index, int numberOfColumns)
throws IndexOutOfBoundsException, IllegalArgumentException;
/**
diff --git a/client/src/com/vaadin/client/ui/grid/Escalator.java b/client/src/com/vaadin/client/ui/grid/Escalator.java
index 73986503d6..a67b701683 100644
--- a/client/src/com/vaadin/client/ui/grid/Escalator.java
+++ b/client/src/com/vaadin/client/ui/grid/Escalator.java
@@ -15,8 +15,16 @@
*/
package com.vaadin.client.ui.grid;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
import java.util.logging.Logger;
+import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Node;
import com.google.gwt.dom.client.Style;
@@ -24,12 +32,149 @@ 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.Window;
import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.client.Util;
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;
+/*-
+
+ Maintenance Notes! Reading these might save your day.
+
+
+ == Row Container Structure
+
+ AbstractRowContainer
+ |-- AbstractStaticRowContainer
+ | |-- HeaderRowContainer
+ | `-- FooterContainer
+ `-- BodyRowContainer
+
+ AbstractRowContainer is intended to contain all common logic
+ between RowContainers. It manages the bookkeeping of row
+ count, makes sure that all individual cells are rendered
+ the same way, and so on.
+
+ AbstractStaticRowContainer has some special logic that is
+ required by all RowContainers that don't scroll (hence the
+ word "static"). HeaderRowContainer and FooterRowContainer
+ are pretty thin special cases of a StaticRowContainer
+ (mostly relating to positioning of the root element).
+
+ BodyRowContainer could also be split into an additional
+ "AbstractScrollingRowContainer", but I felt that no more
+ inner classes were needed. So it contains both logic
+ required for making things scroll about, and equivalent
+ special cases for layouting, as are found in
+ Header/FooterRowContainers.
+
+
+ == The Three Indices
+
+ Each RowContainer can be thought to have three levels of
+ indices for any given displayed row (but the distinction
+ matters primarily for the BodyRowContainer, because of the
+ way it scrolls through data):
+
+ - Logical index
+ - Physical (or DOM) index
+ - Visual index
+
+ LOGICAL INDEX is the index that is linked to the data
+ source. If you want your data source to represent a SQL
+ database with 10 000 rows, the 7 000:th row in the SQL has a
+ logical index of 6 999, since the index is 0-based (unless
+ that data source does some funky logic).
+
+ PHYSICAL INDEX is the index for a row that you see in a
+ browser's DOM inspector. If your row is the second <tr>
+ element within a <tbody> tag, it has a physical index of 1
+ (because of 0-based indices). In Header and
+ FooterRowContainers, you are safe to assume that the logical
+ index is the same as the physical index. But because the
+ BodyRowContainer never displays large data sources entirely
+ in the DOM, a physical index usually has no apparent direct
+ relationship with its logical index.
+
+ VISUAL INDEX is the index relating to the order that you
+ see a row in, in the browser, as it is rendered. The
+ topmost row is 0, the second is 1, and so on. The visual
+ index is similar to the physical index in the sense that
+ Header and FooterRowContainers can assume a 1:1
+ relationship between visual index and logical index. And
+ again, BodyRowContainer has no such relationship. The
+ body's visual index has additionally no apparent
+ relationship with its physical index. Because the <tr> tags
+ are reused in the body and visually repositioned with CSS
+ as the user scrolls, the relationship between physical
+ index and visual index is quickly broken. You can get an
+ element's visual index via the field
+ BodyRowContainer.visualRowOrder.
+
+ */
+
+/**
+ * A workaround-class for GWT and JSNI.
+ * <p>
+ * GWT is unable to handle some method calls to Java methods in inner-classes
+ * from within JSNI blocks. Having that inner class implement a non-inner-class
+ * (or interface), makes it possible for JSNI to indirectly refer to the inner
+ * class, by invoking methods and fields in the non-inner-class.
+ *
+ * @see Escalator.Scroller
+ */
+abstract class JsniWorkaround {
+ /**
+ * A JavaScript function that handles the scroll DOM event, and passes it on
+ * to Java code.
+ *
+ * @see #createScrollListenerFunction(Escalator)
+ * @see Escalator#onScroll(double,double)
+ * @see Escalator.Scroller#onScroll(double, double)
+ */
+ protected final JavaScriptObject scrollListenerFunction;
+
+ /**
+ * A JavaScript function that handles the mousewheel DOM event, and passes
+ * it on to Java code.
+ *
+ * @see #createMousewheelListenerFunction(Escalator)
+ * @see Escalator#onScroll(double,double)
+ * @see Escalator.Scroller#onScroll(double, double)
+ */
+ protected final JavaScriptObject mousewheelListenerFunction;
+
+ protected JsniWorkaround(final Escalator escalator) {
+ scrollListenerFunction = createScrollListenerFunction(escalator);
+ mousewheelListenerFunction = createMousewheelListenerFunction(escalator);
+ }
+
+ /**
+ * A method that constructs the JavaScript function that will be stored into
+ * {@link #scrollListenerFunction}.
+ *
+ * @param esc
+ * a reference to the current instance of {@link Escalator}
+ * @see Escalator#onScroll(double,double)
+ */
+ protected abstract JavaScriptObject createScrollListenerFunction(
+ Escalator esc);
+
+ /**
+ * A method that constructs the JavaScript function that will be stored into
+ * {@link #mousewheelListenerFunction}.
+ *
+ * @param esc
+ * a reference to the current instance of {@link Escalator}
+ * @see Escalator#onScroll(double,double)
+ */
+ protected abstract JavaScriptObject createMousewheelListenerFunction(
+ Escalator esc);
+}
+
/**
* A low-level table-like widget that features a scrolling virtual viewport and
* lazily generated rows.
@@ -46,12 +191,12 @@ public class Escalator extends Widget {
* re-measure)
*/
/*
- * [[escalator]]: This needs to be re-inspected once the escalator pattern
- * is actually implemented.
+ * [[escalator]]: This code will require alterations that are relevant for
+ * the escalator functionality.
*/
/*
- * [[rowwidth]] [[colwidth]]: This needs to be re-inspected once hard-coded
- * values are removed, and cell dimensions are actually being calculated.
+ * [[rowwidth]] [[colwidth]]: This code will require alterations that are
+ * relevant for being able to support variable row heights or column widths.
* NOTE: these bits can most often also be identified by searching for code
* reading the ROW_HEIGHT_PX and COL_WIDTH_PX constans.
*/
@@ -59,10 +204,236 @@ public class Escalator extends Widget {
* [[API]]: Implementing this suggestion would require a change in the
* public API. These suggestions usually don't come lightly.
*/
+ /*
+ * [[mpixscroll]]: This code will require alterations that are relevant for
+ * supporting the scrolling through more pixels than some browsers normally
+ * would support. (i.e. when we support more than "a million" pixels in the
+ * escalator DOM). NOTE: these bits can most often also be identified by
+ * searching for code that call scrollElem.getScrollTop();.
+ */
private static final int ROW_HEIGHT_PX = 20;
private static final int COLUMN_WIDTH_PX = 100;
+ /** An inner class that handles all logic related to scrolling. */
+ private class Scroller extends JsniWorkaround {
+ private double lastScrollTop = -1;
+ private double lastScrollLeft = -1;
+
+ public Scroller() {
+ super(Escalator.this);
+ }
+
+ @Override
+ protected native JavaScriptObject createScrollListenerFunction(
+ Escalator esc)
+ /*-{
+ return $entry(function(e) {
+ esc.@com.vaadin.client.ui.grid.Escalator::onScroll()();
+ });
+ }-*/;
+
+ @Override
+ protected native JavaScriptObject createMousewheelListenerFunction(
+ Escalator esc)
+ /*-{
+ return $entry(function(e) {
+ var deltaX = e.deltaX ? e.deltaX : -0.5*e.wheelDeltaX;
+ var deltaY = e.deltaY ? e.deltaY : -0.5*e.wheelDeltaY;
+
+ // IE8 has only delta y
+ if (isNaN(deltaY)) {
+ deltaY = -0.5*e.wheelDelta;
+ }
+
+ // TODO [[optimize]]: instead of using "+=" that potentially
+ // causes a reflow, update a new scroll absolute position.
+ if (!isNaN(deltaY)) {
+ // the scroll event handler will make sure the content is moved around appropriately
+ esc.@com.vaadin.client.ui.grid.Escalator::scrollerElem.scrollTop += deltaY;
+ }
+
+ if (!isNaN(deltaX)) {
+ // the scroll event handler will make sure the content is moved around appropriately
+ esc.@com.vaadin.client.ui.grid.Escalator::scrollerElem.scrollLeft += deltaX;
+ }
+
+ // TODO: only prevent if not scrolled to end/bottom. Or no? UX team needs to decide.
+ if (e.preventDefault) {
+ e.preventDefault();
+ } else {
+ e.returnValue = false;
+ }
+ });
+ }-*/;
+
+ /**
+ * Recalculates the virtual viewport represented by the scrollbars, so
+ * that the sizes of the scroll handles appear correct in the browser
+ */
+ public void recalculateScrollbarsForVirtualViewport() {
+ double scrollerHeight = height - header.height;
+
+ /*
+ * It looks better this way: if we only have a vertical scrollbar,
+ * keep it between the header and footer. But if we have a
+ * horizontal scrollbar, envelop the footer also.
+ */
+ if (!needsHorizontalScrollbars()) {
+ scrollerHeight -= footer.height;
+ }
+
+ scrollerElem.getStyle().setHeight(scrollerHeight, Unit.PX);
+
+ final double scrollerTop = header.height;
+ scrollerElem.getStyle().setTop(scrollerTop, Unit.PX);
+
+ /*
+ * TODO [[freezecol]]: cut scrollerElem from the left to take freeze
+ * columns into account. innerScroller probably needs some
+ * adjustments too.
+ */
+
+ // TODO [[rowheight]]: adjust for variable row heights.
+ double innerScrollerHeight = ROW_HEIGHT_PX * body.getRowCount();
+ if (needsHorizontalScrollbars()) {
+ innerScrollerHeight += footer.height;
+ }
+ innerScrollerElem.getStyle()
+ .setHeight(innerScrollerHeight, Unit.PX);
+
+ // TODO [[colwidth]]: adjust for variable column widths.
+ innerScrollerElem.getStyle().setWidth(
+ COLUMN_WIDTH_PX * columnConfiguration.getColumnCount(),
+ Unit.PX);
+
+ // we might've got new or got rid of old scrollbars.
+ recalculateTableWrapperSize();
+ }
+
+ /**
+ * Makes sure that the viewport of the table is the correct size, and
+ * that it takes any possible scrollbars into account
+ */
+ public void recalculateTableWrapperSize() {
+ double wrapperHeight = height;
+ if (scrollerElem.getOffsetWidth() < innerScrollerElem
+ .getOffsetWidth()) {
+ wrapperHeight -= Util.getNativeScrollbarSize();
+ }
+
+ double wrapperWidth = width;
+ if (scrollerElem.getOffsetHeight() - footer.height < innerScrollerElem
+ .getOffsetHeight()) {
+ wrapperWidth -= Util.getNativeScrollbarSize();
+ }
+
+ tableWrapper.getStyle().setHeight(wrapperHeight, Unit.PX);
+ tableWrapper.getStyle().setWidth(wrapperWidth, Unit.PX);
+ }
+
+ /**
+ * Logical scrolling event handler for the entire widget.
+ *
+ * @param scrollLeft
+ * the current number of pixels that the user has scrolled
+ * from left
+ * @param scrollTop
+ * the current number of pixels that the user has scrolled
+ * from the top
+ */
+ public void onScroll() {
+ if (internalScrollEventCalls > 0) {
+ internalScrollEventCalls--;
+ return;
+ }
+
+ final int scrollLeft = scrollerElem.getScrollLeft();
+ final int scrollTop = scrollerElem.getScrollTop();
+
+ if (lastScrollLeft != scrollLeft) {
+ // TODO [[frozen]]: frozen columns should be offset here.
+
+ position.set(headElem, -scrollLeft, 0);
+
+ /*
+ * TODO [[optimize]]: cache this value in case the instanceof
+ * check has undesirable overhead. This could also be a
+ * candidate for some deferred binding magic so that e.g.
+ * AbsolutePosition is not even considered in permutations that
+ * we know support something better. That would let the compiler
+ * completely remove the entire condition since it knows that
+ * the if will never be true.
+ */
+ if (position instanceof AbsolutePosition) {
+ /*
+ * we don't want to put "top: 0" on the footer, since it'll
+ * render wrong, as we already have
+ * "bottom: $footer-height".
+ */
+ footElem.getStyle().setLeft(-scrollLeft, Unit.PX);
+ } else {
+ position.set(footElem, -scrollLeft, 0);
+ }
+
+ lastScrollLeft = scrollLeft;
+ }
+
+ body.setBodyScrollPosition(scrollLeft, scrollTop);
+
+ lastScrollTop = scrollTop;
+ body.updateEscalatorRowsOnScroll();
+ /*
+ * TODO [[optimize]]: Might avoid a reflow by first calculating new
+ * scrolltop and scrolleft, then doing the escalator magic based on
+ * those numbers and only updating the positions after that.
+ */
+ }
+
+ public native void attachScrollListener(Element element)
+ /*-{
+ if (element.addEventListener) {
+ element.addEventListener("scroll", this.@com.vaadin.client.ui.grid.JsniWorkaround::scrollListenerFunction);
+ } else {
+ element.attachEvent("onscroll", this.@com.vaadin.client.ui.grid.JsniWorkaround::scrollListenerFunction);
+ }
+ }-*/;
+
+ public native void detachScrollListener(Element element)
+ /*-{
+ if (element.addEventListener) {
+ element.removeEventListener("scroll", this.@com.vaadin.client.ui.grid.JsniWorkaround::scrollListenerFunction);
+ } else {
+ element.detachEvent("onscroll", this.@com.vaadin.client.ui.grid.JsniWorkaround::scrollListenerFunction);
+ }
+ }-*/;
+
+ public native void attachMousewheelListener(Element element)
+ /*-{
+
+ if (element.addEventListener) {
+ // firefox likes "wheel", while others use "mousewheel"
+ var eventName = element.onwheel===undefined?"mousewheel":"wheel";
+ element.addEventListener(eventName, this.@com.vaadin.client.ui.grid.JsniWorkaround::mousewheelListenerFunction);
+ } else {
+ // IE8
+ element.attachEvent("onmousewheel", this.@com.vaadin.client.ui.grid.JsniWorkaround::mousewheelListenerFunction);
+ }
+ }-*/;
+
+ public native void detachMousewheelListener(Element element)
+ /*-{
+ if (element.addEventListener) {
+ // firefox likes "wheel", while others use "mousewheel"
+ var eventName = element.onwheel===undefined?"mousewheel":"wheel";
+ element.removeEventListener(eventName, this.@com.vaadin.client.ui.grid.JsniWorkaround::mousewheelListenerFunction);
+ } else {
+ // IE8
+ element.detachEvent("onmousewheel", this.@com.vaadin.client.ui.grid.JsniWorkaround::mousewheelListenerFunction);
+ }
+ }-*/;
+ }
+
private static class CellImpl implements Cell {
private final Element cellElem;
private final int row;
@@ -93,7 +464,7 @@ public class Escalator extends Widget {
private static final String CLASS_NAME = "v-escalator";
- private class RowContainerImpl implements RowContainer {
+ private abstract class AbstractRowContainer implements RowContainer {
private CellRenderer renderer = CellRenderer.NULL_RENDERER;
private int rows;
@@ -102,18 +473,13 @@ public class Escalator extends Widget {
* 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;
+ protected final Element root;
- /**
- * What cell type to contain in this {@link RowContainer}. Usually
- * either a {@code <th>} or {@code <td>}.
- */
- private final String cellElementTag;
+ /** The height of the combined rows in the DOM. */
+ protected double height = -1;
- public RowContainerImpl(final Element rowContainerElement,
- final String cellElementTag) {
+ public AbstractRowContainer(final Element rowContainerElement) {
root = rowContainerElement;
- this.cellElementTag = cellElementTag;
}
/**
@@ -126,17 +492,20 @@ public class Escalator extends Widget {
* <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);
+ protected void sectionHeightCalculated() {
+ // Does nothing by default. Override to catch the "event".
}
+ /**
+ * Returns a new element to be used as a cell in a row.
+ * <p>
+ * Usually a {@code <th>} or {@code <td>}.
+ *
+ * @return a new element to be used as a cell in a row
+ */
+ protected abstract Element createCellElement();
+
@Override
public CellRenderer getCellRenderer() {
return renderer;
@@ -175,21 +544,24 @@ public class Escalator extends Widget {
* @see #hasSomethingInDom()
*/
@Override
- public void removeRows(final int offset, final int numberOfRows) {
- assertArgumentsAreValidAndWithinRange(offset, numberOfRows);
+ public void removeRows(final int index, final int numberOfRows) {
+ assertArgumentsAreValidAndWithinRange(index, numberOfRows);
rows -= numberOfRows;
+ if (!isAttached()) {
+ return;
+ }
+
if (hasSomethingInDom()) {
- for (int i = 0; i < numberOfRows; i++) {
- root.getChild(offset).removeFromParent();
- }
+ paintRemoveRows(index, numberOfRows);
}
- refreshRowPositions(offset, getRowCount());
- recalculateSectionHeight();
}
- private void assertArgumentsAreValidAndWithinRange(final int offset,
+ protected abstract void paintRemoveRows(final int index,
+ final int numberOfRows);
+
+ private void assertArgumentsAreValidAndWithinRange(final int index,
final int numberOfRows) throws IllegalArgumentException,
IndexOutOfBoundsException {
if (numberOfRows < 1) {
@@ -198,10 +570,9 @@ public class Escalator extends Widget {
+ numberOfRows + ")");
}
- if (offset < 0 || offset + numberOfRows > getRowCount()) {
+ if (index < 0 || index + numberOfRows > getRowCount()) {
throw new IndexOutOfBoundsException("The given "
- + "row range (" + offset + ".."
- + (offset + numberOfRows)
+ + "row range (" + index + ".." + (index + numberOfRows)
+ ") was outside of the current number of rows ("
+ getRowCount() + ")");
}
@@ -222,10 +593,9 @@ public class Escalator extends Widget {
* @see #hasColumnAndRowData()
*/
@Override
- public void insertRows(final int offset, final int numberOfRows) {
- if (offset < 0 || offset > getRowCount()) {
- throw new IndexOutOfBoundsException("The given offset ("
- + offset
+ public void insertRows(final int index, final int numberOfRows) {
+ if (index < 0 || index > getRowCount()) {
+ throw new IndexOutOfBoundsException("The given index (" + index
+ ") was outside of the current number of rows (0.."
+ getRowCount() + ")");
}
@@ -239,28 +609,46 @@ public class Escalator extends Widget {
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.
+ * only add items in the DOM if the widget itself is attached to the
+ * DOM. We can't calculate sizes otherwise.
*/
+ if (isAttached()) {
+ paintInsertRows(index, numberOfRows);
+ }
+ }
- /*
- * TODO [[escalator]]: assert that escalatorChildIndex is a number
- * equal or less than the number of escalator rows
- */
+ /**
+ * Actually add rows into the DOM, now that everything can be
+ * calculated.
+ *
+ * @param visualIndex
+ * the DOM index to add rows into
+ * @param numberOfRows
+ * the number of rows to insert
+ * @return a list of the added row elements
+ */
+ protected List<Element> paintInsertRows(final int visualIndex,
+ final int numberOfRows) {
+ assert isAttached() : "Can't paint rows if Escalator is not attached";
+
+ final List<Element> addedRows = new ArrayList<Element>();
+
+ if (numberOfRows < 1) {
+ return addedRows;
+ }
Node referenceNode;
- if (root.getChildCount() != 0 && offset != 0) {
+ if (root.getChildCount() != 0 && visualIndex != 0) {
// get the row node we're inserting stuff after
- referenceNode = root.getChild(offset - 1);
+ referenceNode = root.getChild(visualIndex - 1);
} else {
- // there are now rows, so just append.
+ // index is 0, so just prepend.
referenceNode = null;
}
- for (int row = offset; row < offset + numberOfRows; row++) {
+ for (int row = visualIndex; row < visualIndex + numberOfRows; row++) {
final Element tr = DOM.createTR();
+ addedRows.add(tr);
for (int col = 0; col < columnConfiguration.getColumnCount(); col++) {
final Element cellElem = createCellElement();
@@ -277,14 +665,12 @@ public class Escalator extends Widget {
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
+ * referencenode being null means we have index 0, i.e. make
+ * it the first row
*/
/*
* TODO [[optimize]]: Is insertFirst or append faster for an
@@ -300,42 +686,17 @@ public class Escalator extends Widget {
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);
- }
+ return addedRows;
}
- private void recalculateSectionHeight() {
- /* TODO [[optimize]]: only do this if the height has changed */
- sectionHeightCalculated(root.getChildCount() * ROW_HEIGHT_PX);
+ protected void recalculateSectionHeight() {
+ final double newHeight = root.getChildCount() * ROW_HEIGHT_PX;
+ if (newHeight != height) {
+ height = newHeight;
+ sectionHeightCalculated();
+ }
}
/**
@@ -348,15 +709,20 @@ public class Escalator extends Widget {
* @see #hasColumnAndRowData()
*/
@Override
- public void refreshRows(final int offset, final int numberOfRows) {
- assertArgumentsAreValidAndWithinRange(offset, numberOfRows);
+ public void refreshRows(final int index, final int numberOfRows) {
+ assertArgumentsAreValidAndWithinRange(index, numberOfRows);
+
+ if (!isAttached()) {
+ return;
+ }
/*
- * TODO [[escalator]]: modify offset and numberOfRows to fit in the
- * current viewport. If they don't fall into the current viewport,
- * NOOP
+ * TODO [[escalator]][[rowheight]]: even if no rows are evaluated in
+ * the current viewport, the heights of some unrendered rows might
+ * change in a refresh. This would cause the scrollbar to be
+ * adjusted (in scrollHeight and/or scrollTop). Do we want to take
+ * this into account?
*/
-
if (hasColumnAndRowData()) {
/*
* TODO [[rowheight]]: nudge rows down with
@@ -367,15 +733,24 @@ public class Escalator extends Widget {
* 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);
- }
+ for (int row = index; row < index + numberOfRows; row++) {
+ final Node tr = getTrByVisualIndex(row);
+ refreshRow(tr, row);
}
}
}
+ void refreshRow(final Node tr, final int logicalRowIndex) {
+ /*
+ * TODO [[API]]: update this to use the row-based updater API in
+ * Artur's "design" project in github.
+ */
+
+ for (int col = 0; col < tr.getChildCount(); col++) {
+ paintCell((Element) tr.getChild(col), logicalRowIndex, col);
+ }
+ }
+
private void paintCell(final Element cellElem, final int row,
final int col) {
/*
@@ -405,6 +780,907 @@ public class Escalator extends Widget {
*/
cellElem.addClassName(CLASS_NAME + "-cell");
}
+
+ /**
+ * Gets the child element that is visually at a certain index
+ *
+ * @param index
+ * the index of the element to retrieve
+ * @return the element at position {@code index}
+ * @throws IndexOutOfBoundsException
+ * if {@code index} is not valid within {@link #root}
+ */
+ abstract protected Element getTrByVisualIndex(int index)
+ throws IndexOutOfBoundsException;
+ }
+
+ private abstract class AbstractStaticRowContainer extends
+ AbstractRowContainer {
+ public AbstractStaticRowContainer(final Element headElement) {
+ super(headElement);
+ }
+
+ @Override
+ protected void paintRemoveRows(final int index, final int numberOfRows) {
+ for (int i = index; i < index + numberOfRows; i++) {
+ final Element tr = (Element) root.getChild(i);
+ tr.removeFromParent();
+ }
+ recalculateSectionHeight();
+ }
+
+ @Override
+ protected Element getTrByVisualIndex(final int index)
+ throws IndexOutOfBoundsException {
+ if (index >= 0 && index < root.getChildCount()) {
+ return (Element) root.getChild(index);
+ } else {
+ throw new IndexOutOfBoundsException("No such visual index: "
+ + index);
+ }
+ }
+ }
+
+ private class HeaderRowContainer extends AbstractStaticRowContainer {
+ public HeaderRowContainer(final Element headElement) {
+ super(headElement);
+ }
+
+ @Override
+ protected void sectionHeightCalculated() {
+ bodyElem.getStyle().setMarginTop(height, Unit.PX);
+ }
+
+ @Override
+ protected Element createCellElement() {
+ return DOM.createTH();
+ }
+ }
+
+ private class FooterRowContainer extends AbstractStaticRowContainer {
+ public FooterRowContainer(final Element footElement) {
+ super(footElement);
+ }
+
+ @Override
+ protected Element createCellElement() {
+ return DOM.createTD();
+ }
+ }
+
+ private class BodyRowContainer extends AbstractRowContainer {
+ /*
+ * TODO [[optimize]]: check whether a native JsArray might be faster
+ * than LinkedList
+ */
+ /**
+ * The order in which row elements are rendered visually in the browser,
+ * with the help of CSS tricks. Usually has nothing to do with the DOM
+ * order.
+ */
+ private final LinkedList<Element> visualRowOrder = new LinkedList<Element>();
+
+ /**
+ * Don't use this field directly, because it will not take proper care
+ * of all the bookkeeping required.
+ *
+ * @deprecated Use {@link #setRowPosition(Element, int, int)} and
+ * {@link #getRowTop(Element)} instead.
+ */
+ @Deprecated
+ private final Map<Element, Integer> rowTopPosMap = new HashMap<Element, Integer>();
+
+ private int tBodyScrollTop = 0;
+ private int tBodyScrollLeft = 0;
+
+ public BodyRowContainer(final Element bodyElement) {
+ super(bodyElement);
+ }
+
+ public void updateEscalatorRowsOnScroll() {
+ final int topRowPos = getRowTop(visualRowOrder.getFirst());
+ // TODO [[mpixscroll]]
+ final int scrollTop = tBodyScrollTop;
+ final int viewportOffset = topRowPos - scrollTop;
+
+ /*
+ * TODO [[optimize]] this if-else can most probably be refactored
+ * into a neater block of code
+ */
+
+ if (viewportOffset > 0) {
+ // there's empty room on top
+
+ int rowsToMove = (int) Math.ceil((double) viewportOffset
+ / (double) ROW_HEIGHT_PX);
+ rowsToMove = Math.min(rowsToMove, root.getChildCount());
+
+ final int end = root.getChildCount();
+ final int start = end - rowsToMove;
+ final int logicalRowIndex = scrollTop / ROW_HEIGHT_PX;
+ moveAndUpdateEscalatorRows(Range.between(start, end), 0,
+ logicalRowIndex);
+ }
+
+ else if (viewportOffset + ROW_HEIGHT_PX <= 0) {
+ /*
+ * the viewport has been scrolled more than the topmost visual
+ * row.
+ */
+
+ /*
+ * Using the fact that integer division has implicit
+ * floor-function to our advantage here.
+ */
+ int rowsToMove = Math.abs(viewportOffset / ROW_HEIGHT_PX);
+ rowsToMove = Math.min(rowsToMove, root.getChildCount());
+
+ int logicalRowIndex;
+ if (rowsToMove < root.getChildCount()) {
+ /*
+ * We scroll so little that we can just keep adding the rows
+ * below the current escalator
+ */
+ logicalRowIndex = getLogicalRowIndex(visualRowOrder
+ .getLast()) + 1;
+ } else {
+ /*
+ * Since we're moving all escalator rows, we need to
+ * calculate the first logical row index from the scroll
+ * position.
+ */
+ logicalRowIndex = scrollTop / ROW_HEIGHT_PX;
+ }
+
+ /*
+ * Since we're moving the viewport downwards, the visual index
+ * is always at the bottom. Note: Due to how
+ * moveAndUpdateEscalatorRows works, this will work out even if
+ * we move all the rows, and try to place them "at the end".
+ */
+ final int targetVisualIndex = root.getChildCount();
+
+ // make sure that we don't move rows over the data boundary
+ boolean aRowWasLeftBehind = false;
+ if (logicalRowIndex + rowsToMove > getRowCount()) {
+ /*
+ * TODO [[rowheight]]: with constant row heights, there's
+ * always exactly one row that will be moved beyond the data
+ * source, when viewport is scrolled to the end. This,
+ * however, isn't guaranteed anymore once row heights start
+ * varying.
+ */
+ rowsToMove--;
+ aRowWasLeftBehind = true;
+ }
+
+ moveAndUpdateEscalatorRows(Range.between(0, rowsToMove),
+ targetVisualIndex, logicalRowIndex);
+
+ if (aRowWasLeftBehind) {
+ /*
+ * To keep visualRowOrder as a spatially contiguous block of
+ * rows, let's make sure that the one row we didn't move
+ * visually still stays with the pack.
+ */
+ final Range strayRow = Range.withOnly(0);
+ final int topLogicalIndex = getLogicalRowIndex(visualRowOrder
+ .get(1)) - 1;
+ moveAndUpdateEscalatorRows(strayRow, 0, topLogicalIndex);
+ }
+ }
+ }
+
+ @Override
+ protected List<Element> paintInsertRows(final int index,
+ final int numberOfRows) {
+ if (numberOfRows == 0) {
+ return Collections.emptyList();
+ }
+
+ /*
+ * TODO: this method should probably only add physical rows, and not
+ * populate them - let everything be populated as appropriate by the
+ * logic that follows.
+ *
+ * This also would lead to the fact that paintInsertRows wouldn't
+ * need to return anything.
+ */
+ final List<Element> addedRows = fillAndPopulateEscalatorRowsIfNeeded(
+ index, numberOfRows);
+
+ /*
+ * insertRows will always change the number of rows - update the
+ * scrollbar sizes.
+ */
+ scroller.recalculateScrollbarsForVirtualViewport();
+
+ final boolean addedRowsAboveCurrentViewport = index * ROW_HEIGHT_PX < scrollerElem
+ .getScrollTop();
+ final boolean addedRowsBelowCurrentViewport = index * ROW_HEIGHT_PX > scrollerElem
+ .getScrollTop() + calculateHeight();
+
+ if (addedRowsAboveCurrentViewport) {
+ /*
+ * We need to tweak the virtual viewport (scroll handle
+ * positions, table "scroll position" and row locations), but
+ * without re-evaluating any rows.
+ */
+
+ final int yDelta = numberOfRows * ROW_HEIGHT_PX;
+ adjustScrollPosIgnoreEvents(yDelta);
+ }
+
+ else if (addedRowsBelowCurrentViewport) {
+ // NOOP, we already recalculated scrollbars.
+ }
+
+ else { // some rows were added inside the current viewport
+
+ final int unupdatedLogicalStart = index + addedRows.size();
+ final int visualOffset = getLogicalRowIndex(visualRowOrder
+ .getFirst());
+
+ /*
+ * At this point, we have added new escalator rows, if so
+ * needed.
+ *
+ * If more rows were added than the new escalator rows can
+ * account for, we need to start to spin the escalator to update
+ * the remaining rows aswell.
+ */
+ final int rowsStillNeeded = numberOfRows - addedRows.size();
+ final Range unupdatedVisual = convertToVisual(Range.withLength(
+ unupdatedLogicalStart, rowsStillNeeded));
+ final int end = root.getChildCount();
+ final int start = end - unupdatedVisual.length();
+ final int visualTargetIndex = unupdatedLogicalStart
+ - visualOffset;
+ moveAndUpdateEscalatorRows(Range.between(start, end),
+ visualTargetIndex, unupdatedLogicalStart);
+
+ // move the surrounding rows to their correct places.
+ int rowTop = (unupdatedLogicalStart + (end - start))
+ * ROW_HEIGHT_PX;
+ final ListIterator<Element> i = visualRowOrder
+ .listIterator(visualTargetIndex + (end - start));
+ while (i.hasNext()) {
+ final Element tr = i.next();
+ setRowPosition(tr, 0, rowTop);
+ rowTop += ROW_HEIGHT_PX;
+ }
+ }
+ return addedRows;
+ }
+
+ /**
+ * Move escalator rows around, and make sure everything gets
+ * appropriately repositioned and repainted.
+ *
+ * @param visualSourceRange
+ * the range of rows to move to a new place
+ * @param visualTargetIndex
+ * the visual index where the rows will be placed to
+ * @param logicalTargetIndex
+ * the logical index to be assigned to the first moved row
+ * @throws IllegalArgumentException
+ * if any of <code>visualSourceRange.getStart()</code>,
+ * <code>visualTargetIndex</code> or
+ * <code>logicalTargetIndex</code> is a negative number; or
+ * if <code>visualTargetInfo</code> is greater than the
+ * number of escalator rows.
+ */
+ private void moveAndUpdateEscalatorRows(final Range visualSourceRange,
+ final int visualTargetIndex, final int logicalTargetIndex)
+ throws IllegalArgumentException {
+
+ if (visualSourceRange.isEmpty()) {
+ return;
+ }
+
+ if (visualSourceRange.getStart() < 0) {
+ throw new IllegalArgumentException(
+ "Logical source start must be 0 or greater (was "
+ + visualSourceRange.getStart() + ")");
+ } else if (logicalTargetIndex < 0) {
+ throw new IllegalArgumentException(
+ "Logical target must be 0 or greater");
+ } else if (visualTargetIndex < 0) {
+ throw new IllegalArgumentException(
+ "Visual target must be 0 or greater");
+ } else if (visualTargetIndex > root.getChildCount()) {
+ throw new IllegalArgumentException(
+ "Visual target must not be greater than the number of escalator rows");
+ } else if (logicalTargetIndex + visualSourceRange.length() > getRowCount()) {
+ final int logicalEndIndex = logicalTargetIndex
+ + visualSourceRange.length() - 1;
+ throw new IllegalArgumentException(
+ "Logical target leads to rows outside of the data range ("
+ + logicalTargetIndex + ".." + logicalEndIndex
+ + ")");
+ }
+
+ /*
+ * Since we move a range into another range, the indices might move
+ * about. Having 10 rows, if we move 0..1 to index 10 (to the end of
+ * the collection), the target range will end up being 8..9, instead
+ * of 10..11.
+ *
+ * This applies only if we move elements forward in the collection,
+ * not backward.
+ */
+ final int adjustedVisualTargetIndex;
+ if (visualSourceRange.getStart() < visualTargetIndex) {
+ adjustedVisualTargetIndex = visualTargetIndex
+ - visualSourceRange.length();
+ } else {
+ adjustedVisualTargetIndex = visualTargetIndex;
+ }
+
+ if (visualSourceRange.getStart() != adjustedVisualTargetIndex) {
+
+ /*
+ * Reorder the rows to their correct places within
+ * visualRowOrder (unless rows are moved back to their original
+ * places)
+ */
+
+ /*
+ * TODO [[optimize]]: move whichever set is smaller: the ones
+ * explicitly moved, or the others. So, with 10 escalator rows,
+ * if we are asked to move idx[0..8] to the end of the list,
+ * it's faster to just move idx[9] to the beginning.
+ */
+
+ final List<Element> removedRows = new ArrayList<Element>(
+ visualSourceRange.length());
+ for (int i = 0; i < visualSourceRange.length(); i++) {
+ final Element tr = visualRowOrder.remove(visualSourceRange
+ .getStart());
+ removedRows.add(tr);
+ }
+ visualRowOrder.addAll(adjustedVisualTargetIndex, removedRows);
+ }
+
+ { // Refresh the contents of the affected rows
+ final ListIterator<Element> iter = visualRowOrder
+ .listIterator(adjustedVisualTargetIndex);
+ for (int logicalIndex = logicalTargetIndex; logicalIndex < logicalTargetIndex
+ + visualSourceRange.length(); logicalIndex++) {
+ final Element tr = iter.next();
+ refreshRow(tr, logicalIndex);
+ }
+ }
+
+ { // Reposition the rows that were moved
+ int newRowTop = logicalTargetIndex * ROW_HEIGHT_PX;
+
+ final ListIterator<Element> iter = visualRowOrder
+ .listIterator(adjustedVisualTargetIndex);
+ for (int i = 0; i < visualSourceRange.length(); i++) {
+ final Element tr = iter.next();
+ setRowPosition(tr, 0, newRowTop);
+ newRowTop += ROW_HEIGHT_PX;
+ }
+ }
+ }
+
+ /**
+ * Adjust the scroll position without having the scroll handler have any
+ * side-effects.
+ * <p>
+ * <em>Note:</em> {@link Scroller#onScroll(double, double)}
+ * <em>will</em> be triggered, but it not do anything, with the help of
+ * {@link Escalator#internalScrollEventCalls}.
+ *
+ * @param yDelta
+ * the delta of pixels to scrolls. A positive value moves the
+ * viewport downwards, while a negative value moves the
+ * viewport upwards
+ */
+ public void adjustScrollPosIgnoreEvents(final int yDelta) {
+ if (yDelta == 0) {
+ return;
+ }
+
+ internalScrollEventCalls++;
+ scrollerElem.setScrollTop(scrollerElem.getScrollTop() + yDelta);
+
+ final int snappedYDelta = yDelta - yDelta % ROW_HEIGHT_PX;
+ for (final Element tr : visualRowOrder) {
+ setRowPosition(tr, 0, getRowTop(tr) + snappedYDelta);
+ }
+ setBodyScrollPosition(tBodyScrollLeft, tBodyScrollTop + yDelta);
+ }
+
+ private void setRowPosition(final Element tr, final int x, final int y) {
+ position.set(tr, x, y);
+ rowTopPosMap.put(tr, y);
+ }
+
+ private int getRowTop(final Element tr) {
+ return rowTopPosMap.get(tr);
+ }
+
+ private List<Element> fillAndPopulateEscalatorRowsIfNeeded(
+ final int index, final int numberOfRows) {
+
+ final int maxEscalatorRowCapacity = (int) Math
+ .ceil(calculateHeight() / ROW_HEIGHT_PX) + 1;
+ final int escalatorRowsStillFit = maxEscalatorRowCapacity
+ - root.getChildCount();
+ final int escalatorRowsNeeded = Math.min(numberOfRows,
+ escalatorRowsStillFit);
+
+ if (escalatorRowsNeeded > 0) {
+
+ final List<Element> addedRows = super.paintInsertRows(index,
+ escalatorRowsNeeded);
+ visualRowOrder.addAll(index, addedRows);
+
+ /*
+ * We need to figure out the top positions for the rows we just
+ * added.
+ */
+ for (int i = 0; i < addedRows.size(); i++) {
+ setRowPosition(addedRows.get(i), 0, (index + i)
+ * ROW_HEIGHT_PX);
+ }
+
+ /* Move the other rows away from above the added escalator rows */
+ for (int i = index + addedRows.size(); i < visualRowOrder
+ .size(); i++) {
+ final Element tr = visualRowOrder.get(i);
+ setRowPosition(tr, 0, i * ROW_HEIGHT_PX);
+ }
+
+ return addedRows;
+ } else {
+ return new ArrayList<Element>();
+ }
+ }
+
+ @Override
+ protected void paintRemoveRows(final int index, final int numberOfRows) {
+
+ final Range viewportRange = Range.withLength(
+ getLogicalRowIndex(visualRowOrder.getFirst()),
+ visualRowOrder.size());
+
+ final Range removedRowsRange = Range
+ .withLength(index, numberOfRows);
+
+ final Range[] partitions = removedRowsRange
+ .partitionWith(viewportRange);
+ final Range removedAbove = partitions[0];
+ final Range removedLogicalInside = partitions[1];
+ final Range removedVisualInside = convertToVisual(removedLogicalInside);
+
+ /*
+ * TODO: extract the following if-block to a separate method. I'll
+ * leave this be inlined for now, to make linediff-based code
+ * reviewing easier. Probably will be moved in the following patch
+ * set.
+ */
+
+ /*
+ * Adjust scroll position in one of two scenarios:
+ *
+ * 1) Rows were removed above. Then we just need to adjust the
+ * scrollbar by the height of the removed rows.
+ *
+ * 2) There are no logical rows above, and at least the first (if
+ * not more) visual row is removed. Then we need to snap the scroll
+ * position to the first visible row (i.e. reset scroll position to
+ * absolute 0)
+ *
+ * The logic is optimized in such a way that the
+ * adjustScrollPosIgnoreEvents is called only once, to avoid extra
+ * reflows, and thus the code might seem a bit obscure.
+ */
+ final boolean firstVisualRowIsRemoved = !removedVisualInside
+ .isEmpty() && removedVisualInside.getStart() == 0;
+
+ if (!removedAbove.isEmpty() || firstVisualRowIsRemoved) {
+ // TODO [[rowheight]]
+ final int yDelta = removedAbove.length() * ROW_HEIGHT_PX;
+ final int firstLogicalRowHeight = ROW_HEIGHT_PX;
+ final boolean removalScrollsToShowFirstLogicalRow = scrollerElem
+ .getScrollTop() - yDelta < firstLogicalRowHeight;
+
+ if (removedVisualInside.isEmpty()
+ && (!removalScrollsToShowFirstLogicalRow || !firstVisualRowIsRemoved)) {
+ /*
+ * rows were removed from above the viewport, so all we need
+ * to do is to adjust the scroll position to account for the
+ * removed rows
+ */
+ adjustScrollPosIgnoreEvents(-yDelta);
+ } else if (removalScrollsToShowFirstLogicalRow) {
+ /*
+ * It seems like we've removed all rows from above, and also
+ * into the current viewport. This means we'll need to even
+ * out the scroll position to exactly 0 (i.e. adjust by the
+ * current negative scrolltop, presto!), so that it isn't
+ * aligned funnily
+ */
+ adjustScrollPosIgnoreEvents(-scrollerElem.getScrollTop());
+ }
+ }
+
+ // ranges evaluated, let's do things.
+ if (!removedVisualInside.isEmpty()) {
+ int escalatorRowCount = bodyElem.getChildCount();
+
+ /*
+ * If we're left with less rows than the number of escalators,
+ * remove the unused ones.
+ */
+ final int escalatorRowsToRemove = escalatorRowCount
+ - getRowCount();
+ if (escalatorRowsToRemove > 0) {
+ for (int i = 0; i < escalatorRowsToRemove; i++) {
+ final Element tr = visualRowOrder
+ .remove(removedVisualInside.getStart());
+ tr.removeFromParent();
+ rowTopPosMap.remove(tr);
+ }
+ escalatorRowCount -= escalatorRowsToRemove;
+
+ /*
+ * Because we're removing escalator rows, we don't have
+ * anything to scroll by. Let's make sure the viewport is
+ * scrolled to top, to render any rows possibly left above.
+ */
+ body.setBodyScrollPosition(body.tBodyScrollLeft, 0);
+
+ /*
+ * We might have removed some rows from the middle, so let's
+ * make sure we're not left with any holes. Also remember:
+ * visualIndex == logicalIndex applies now.
+ */
+ final int dirtyRowsStart = removedLogicalInside.getStart();
+ for (int i = dirtyRowsStart; i < escalatorRowCount; i++) {
+ final Element tr = visualRowOrder.get(i);
+ setRowPosition(tr, 0, i * ROW_HEIGHT_PX);
+ }
+
+ /*
+ * this is how many rows appeared into the viewport from
+ * below
+ */
+ final int rowsToUpdateDataOn = numberOfRows
+ - escalatorRowsToRemove;
+ final int start = Math.max(0, escalatorRowCount
+ - rowsToUpdateDataOn);
+ final int end = escalatorRowCount;
+ for (int i = start; i < end; i++) {
+ final Element tr = visualRowOrder.get(i);
+ refreshRow(tr, i);
+ }
+ }
+
+ else {
+ // No escalator rows need to be removed.
+
+ /*
+ * Two things (or a combination thereof) can happen:
+ *
+ * 1) We're scrolled to the bottom, the last rows are
+ * removed. SOLUTION: moveAndUpdateEscalatorRows the
+ * bottommost rows, and place them at the top to be
+ * refreshed.
+ *
+ * 2) We're scrolled somewhere in the middle, arbitrary rows
+ * are removed. SOLUTION: moveAndUpdateEscalatorRows the
+ * removed rows, and place them at the bottom to be
+ * refreshed.
+ *
+ * Since a combination can also happen, we need to handle
+ * this in a smart way, all while avoiding
+ * double-refreshing.
+ */
+
+ final int contentBottom = getRowCount() * ROW_HEIGHT_PX;
+ final int viewportBottom = (int) (tBodyScrollTop + calculateHeight());
+ if (viewportBottom <= contentBottom) {
+ /*
+ * We're in the middle of the row container, everything
+ * is added to the bottom
+ */
+ paintRemoveRowsAtMiddle(removedLogicalInside,
+ removedVisualInside, 0);
+ }
+
+ else if (contentBottom + (numberOfRows * ROW_HEIGHT_PX)
+ - viewportBottom < ROW_HEIGHT_PX) {
+ /*
+ * We're at the end of the row container, everything is
+ * added to the top.
+ */
+ paintRemoveRowsAtBottom(removedLogicalInside,
+ removedVisualInside);
+ }
+
+ else {
+ /*
+ * We're in a combination, where we need to both scroll
+ * up AND show new rows at the bottom.
+ *
+ * Example: Scrolled down to show the second to last
+ * row. Remove two. Viewport scrolls up, revealing the
+ * row above row. The last element collapses up and into
+ * view.
+ *
+ * Reminder: this use case handles only the case when
+ * there are enough escalator rows to still render a
+ * full view. I.e. all escalator rows will _always_ be
+ * populated
+ */
+ /*-
+ * 1 1 |1| <- newly rendered
+ * |2| |2| |2|
+ * |3| ==> |*| ==> |5| <- newly rendered
+ * |4| |*|
+ * 5 5
+ *
+ * 1 1 |1| <- newly rendered
+ * |2| |*| |4|
+ * |3| ==> |*| ==> |5| <- newly rendered
+ * |4| |4|
+ * 5 5
+ */
+
+ /*
+ * STEP 1:
+ *
+ * reorganize deprecated escalator rows to bottom, but
+ * don't re-render anything yet
+ */
+ /*-
+ * 1 1 1
+ * |2| |*| |4|
+ * |3| ==> |*| ==> |*|
+ * |4| |4| |*|
+ * 5 5 5
+ */
+ int newTop = getRowTop(visualRowOrder
+ .get(removedVisualInside.getStart()));
+ for (int i = 0; i < removedVisualInside.length(); i++) {
+ final Element tr = visualRowOrder
+ .remove(removedVisualInside.getStart());
+ visualRowOrder.addLast(tr);
+ }
+
+ for (int i = removedVisualInside.getStart(); i < escalatorRowCount; i++) {
+ final Element tr = visualRowOrder.get(i);
+ setRowPosition(tr, 0, newTop);
+ newTop += ROW_HEIGHT_PX;
+ }
+
+ /*
+ * STEP 2:
+ *
+ * manually scroll
+ */
+ /*-
+ * 1 |1| <-- newly rendered (by scrolling)
+ * |4| |4|
+ * |*| ==> |*|
+ * |*|
+ * 5 5
+ */
+ final double newScrollTop = contentBottom
+ - calculateHeight();
+ setScrollTop(newScrollTop);
+ /*
+ * Manually call the scroll handler, so we get immediate
+ * effects in the escalator.
+ */
+ scroller.onScroll();
+ internalScrollEventCalls++;
+
+ /*
+ * Move the bottommost (n+1:th) escalator row to top,
+ * because scrolling up doesn't handle that for us
+ * automatically
+ */
+ moveAndUpdateEscalatorRows(
+ Range.withOnly(escalatorRowCount - 1),
+ 0,
+ getLogicalRowIndex(visualRowOrder.getFirst()) - 1);
+
+ /*
+ * STEP 3:
+ *
+ * update remaining escalator rows
+ */
+ /*-
+ * |1| |1|
+ * |4| ==> |4|
+ * |*| |5| <-- newly rendered
+ *
+ * 5
+ */
+ final int rowsScrolled = (int) (Math
+ .ceil((viewportBottom - (double) contentBottom)
+ / ROW_HEIGHT_PX));
+ final int start = escalatorRowCount
+ - (removedVisualInside.length() - rowsScrolled);
+ final Range visualRefreshRange = Range.between(start,
+ escalatorRowCount);
+ final int logicalTargetIndex = getLogicalRowIndex(visualRowOrder
+ .getFirst()) + start;
+ // in-place move simply re-renders the rows.
+ moveAndUpdateEscalatorRows(visualRefreshRange, start,
+ logicalTargetIndex);
+ }
+ }
+ }
+
+ /*
+ * this needs to be done after the escalator has been shrunk down,
+ * or it won't work correctly (due to setScrollTop invocation)
+ */
+ scroller.recalculateScrollbarsForVirtualViewport();
+ }
+
+ private void paintRemoveRowsAtMiddle(final Range removedLogicalInside,
+ final Range removedVisualInside, final int logicalOffset) {
+ /*-
+ * : : :
+ * |2| |2| |2|
+ * |3| ==> |*| ==> |4|
+ * |4| |4| |6| <- newly rendered
+ * : : :
+ */
+
+ final int escalatorRowCount = visualRowOrder.size();
+
+ final int logicalTargetIndex = getLogicalRowIndex(visualRowOrder
+ .getLast())
+ - (removedVisualInside.length() - 1)
+ + logicalOffset;
+ moveAndUpdateEscalatorRows(removedVisualInside, escalatorRowCount,
+ logicalTargetIndex);
+
+ // move the surrounding rows to their correct places.
+ final ListIterator<Element> iterator = visualRowOrder
+ .listIterator(removedVisualInside.getStart());
+ int rowTop = (removedLogicalInside.getStart() + logicalOffset)
+ * ROW_HEIGHT_PX;
+ for (int i = removedVisualInside.getStart(); i < escalatorRowCount
+ - removedVisualInside.length(); i++) {
+ final Element tr = iterator.next();
+ setRowPosition(tr, 0, rowTop);
+ rowTop += ROW_HEIGHT_PX;
+ }
+ }
+
+ private void paintRemoveRowsAtBottom(final Range removedLogicalInside,
+ final Range removedVisualInside) {
+ /*-
+ * :
+ * : : |4| <- newly rendered
+ * |5| |5| |5|
+ * |6| ==> |*| ==> |7|
+ * |7| |7|
+ */
+
+ final int logicalTargetIndex = getLogicalRowIndex(visualRowOrder
+ .getFirst()) - removedVisualInside.length();
+ moveAndUpdateEscalatorRows(removedVisualInside, 0,
+ logicalTargetIndex);
+
+ // move the surrounding rows to their correct places.
+ final ListIterator<Element> iterator = visualRowOrder
+ .listIterator(removedVisualInside.getEnd());
+ int rowTop = removedLogicalInside.getStart() * ROW_HEIGHT_PX;
+ while (iterator.hasNext()) {
+ final Element tr = iterator.next();
+ setRowPosition(tr, 0, rowTop);
+ rowTop += ROW_HEIGHT_PX;
+ }
+ }
+
+ private int getLogicalRowIndex(final Element element) {
+ // TODO [[rowheight]]
+ return getRowTop(element) / ROW_HEIGHT_PX;
+ }
+
+ @Override
+ protected void recalculateSectionHeight() {
+ // disable for body, since it doesn't make any sense.
+ }
+
+ /**
+ * Adjusts the row index and number to be relevant for the current
+ * virtual viewport.
+ * <p>
+ * It converts a logical range of rows index to the matching visual
+ * range, truncating the resulting range with the viewport.
+ * <p>
+ * <ul>
+ * <li>Escalator contains logical rows 0..100
+ * <li>Current viewport showing logical rows 20..29
+ * <li>convertToVisual([20..29]) &rarr; [0..9]
+ * <li>convertToVisual([15..24]) &rarr; [0..4]
+ * <li>convertToVisual([25..29]) &rarr; [5..9]
+ * <li>convertToVisual([26..39]) &rarr; [6..9]
+ * <li>convertToVisual([0..5]) &rarr; [0..-1] <em>(empty)</em>
+ * <li>convertToVisual([35..1]) &rarr; [0..-1] <em>(empty)</em>
+ * <li>convertToVisual([0..100]) &rarr; [0..9]
+ * </ul>
+ *
+ * @return a logical range converted to a visual range, truncated to the
+ * current viewport. The first visual row has the index 0.
+ */
+ private Range convertToVisual(final Range logicalRange) {
+ if (logicalRange.isEmpty()) {
+ return logicalRange;
+ }
+
+ /*
+ * TODO [[rowheight]]: these assumptions will be totally broken with
+ * variable row heights.
+ */
+ final int topRowHeight = ROW_HEIGHT_PX;
+ final int maxEscalatorRows = (int) Math
+ .ceil((calculateHeight() + topRowHeight) / ROW_HEIGHT_PX);
+ final int currentTopRowIndex = getLogicalRowIndex(visualRowOrder
+ .getFirst());
+
+ final Range[] partitions = logicalRange.partitionWith(Range
+ .withLength(currentTopRowIndex, maxEscalatorRows));
+ final Range insideRange = partitions[1];
+ return insideRange.offsetBy(-currentTopRowIndex);
+ }
+
+ @Override
+ protected Element createCellElement() {
+ return DOM.createTD();
+ }
+
+ private double calculateHeight() {
+ final int tableHeight = tableWrapper.getOffsetHeight();
+ final double footerHeight = footer.height;
+ final double headerHeight = header.height;
+ return tableHeight - footerHeight - headerHeight;
+ }
+
+ @Override
+ public void refreshRows(final int index, final int numberOfRows) {
+ final Range visualRange = convertToVisual(Range.withLength(index,
+ numberOfRows));
+
+ if (!visualRange.isEmpty()) {
+ final int firstLogicalRowIndex = getLogicalRowIndex(visualRowOrder
+ .getFirst());
+ for (int rowNumber = visualRange.getStart(); rowNumber < visualRange
+ .getEnd(); rowNumber++) {
+ refreshRow(visualRowOrder.get(rowNumber),
+ firstLogicalRowIndex + rowNumber);
+ }
+ }
+ }
+
+ @Override
+ protected Element getTrByVisualIndex(final int index)
+ throws IndexOutOfBoundsException {
+ if (index >= 0 && index < visualRowOrder.size()) {
+ return visualRowOrder.get(index);
+ } else {
+ throw new IndexOutOfBoundsException("No such visual index: "
+ + index);
+ }
+ }
+
+ private void setBodyScrollPosition(final int scrollLeft,
+ final int scrollTop) {
+ tBodyScrollLeft = scrollLeft;
+ tBodyScrollTop = scrollTop;
+ position.set(bodyElem, -tBodyScrollLeft, -tBodyScrollTop);
+ }
}
private class ColumnConfigurationImpl implements ColumnConfiguration {
@@ -420,24 +1696,25 @@ public class Escalator extends Widget {
* @see #hasSomethingInDom()
*/
@Override
- public void removeColumns(final int offset, final int numberOfColumns) {
- assertArgumentsAreValidAndWithinRange(offset, numberOfColumns);
+ public void removeColumns(final int index, final int numberOfColumns) {
+ assertArgumentsAreValidAndWithinRange(index, numberOfColumns);
columns--;
+ // FIXME [[escalator]]: broken on escalator
if (hasSomethingInDom()) {
- for (RowContainerImpl rowContainer : rowContainers) {
+ for (final AbstractRowContainer rowContainer : rowContainers) {
for (int row = 0; row < rowContainer.getRowCount(); row++) {
- Node tr = rowContainer.root.getChild(row);
+ final Node tr = rowContainer.root.getChild(row);
for (int col = 0; col < numberOfColumns; col++) {
- tr.getChild(offset).removeFromParent();
+ tr.getChild(index).removeFromParent();
}
}
}
}
}
- private void assertArgumentsAreValidAndWithinRange(final int offset,
+ private void assertArgumentsAreValidAndWithinRange(final int index,
final int numberOfColumns) {
if (numberOfColumns < 1) {
throw new IllegalArgumentException(
@@ -445,10 +1722,10 @@ public class Escalator extends Widget {
+ numberOfColumns + ")");
}
- if (offset < 0 || offset + numberOfColumns > getColumnCount()) {
+ if (index < 0 || index + numberOfColumns > getColumnCount()) {
throw new IndexOutOfBoundsException("The given "
- + "column range (" + offset + ".."
- + (offset + numberOfColumns)
+ + "column range (" + index + ".."
+ + (index + numberOfColumns)
+ ") was outside of the current "
+ "number of columns (" + getColumnCount() + ")");
}
@@ -464,10 +1741,9 @@ public class Escalator extends Widget {
* @see #hasColumnAndRowData()
*/
@Override
- public void insertColumns(final int offset, final int numberOfColumns) {
- if (offset < 0 || offset > getColumnCount()) {
- throw new IndexOutOfBoundsException("The given offset("
- + offset
+ public void insertColumns(final int index, final int numberOfColumns) {
+ if (index < 0 || index > getColumnCount()) {
+ throw new IndexOutOfBoundsException("The given index(" + index
+ ") was outside of the current number of columns (0.."
+ getColumnCount() + ")");
}
@@ -483,20 +1759,21 @@ public class Escalator extends Widget {
return;
}
- for (final RowContainerImpl rowContainer : rowContainers) {
+ for (final AbstractRowContainer rowContainer : rowContainers) {
+ // FIXME: broken on escalator
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);
+ if (index != 0) {
+ referenceElement = tr.getChild(index - 1);
} else {
referenceElement = null;
}
- for (int col = offset; col < offset + numberOfColumns; col++) {
+ for (int col = index; col < index + numberOfColumns; col++) {
final Element cellElem = rowContainer
.createCellElement();
rowContainer.paintCell(cellElem, row, col);
@@ -505,7 +1782,7 @@ public class Escalator extends Widget {
tr.insertAfter(cellElem, referenceElement);
} else {
/*
- * referenceElement being null means we have offset
+ * referenceElement being null means we have index
* 0, make it the first cell.
*/
/*
@@ -540,29 +1817,23 @@ public class Escalator extends Widget {
}
}
+ /** The {@code <thead/>} tag. */
private final Element headElem = DOM.createTHead();
+ /** The {@code <tbody/>} tag. */
private final Element bodyElem = DOM.createTBody();
+ /** The {@code <tfoot/>} tag. */
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 Element scrollerElem = DOM.createDiv();
+ private final Element innerScrollerElem = DOM.createDiv();
- private final RowContainerImpl body = new RowContainerImpl(bodyElem, "td");
+ private final HeaderRowContainer header = new HeaderRowContainer(headElem);
+ private final BodyRowContainer body = new BodyRowContainer(bodyElem);
+ private final FooterRowContainer footer = new FooterRowContainer(footElem);
- private final RowContainerImpl footer = new RowContainerImpl(footElem, "td") {
- @Override
- protected void sectionHeightCalculated(final double newPxHeight) {
- footElem.getStyle().setBottom(newPxHeight, Unit.PX);
- }
- };
+ private final Scroller scroller = new Scroller();
- private final RowContainerImpl[] rowContainers = new RowContainerImpl[] {
+ private final AbstractRowContainer[] rowContainers = new AbstractRowContainer[] {
header, body, footer };
private final ColumnConfigurationImpl columnConfiguration = new ColumnConfigurationImpl();
@@ -570,23 +1841,51 @@ public class Escalator extends Widget {
private PositionFunction position;
+ private int internalScrollEventCalls = 0;
+
+ /** The cached width of the escalator, in pixels. */
+ private double width;
+ /** The cached height of the escalator, in pixels. */
+ private double height;
+
+ private static native double getPreciseWidth(Element element)
+ /*-{
+ if (element.getBoundingClientRect) {
+ var rect = element.getBoundingClientRect();
+ return rect.right - rect.left;
+ } else {
+ return element.offsetWidth;
+ }
+ }-*/;
+
+ private static native double getPreciseHeight(Element element)
+ /*-{
+ if (element.getBoundingClientRect) {
+ var rect = element.getBoundingClientRect();
+ return rect.bottom - rect.top;
+ } else {
+ return element.offsetHeight;
+ }
+ }-*/;
+
/**
* Creates a new Escalator widget instance.
*/
public Escalator() {
detectAndApplyPositionFunction();
+ getLogger().info(
+ "Using " + position.getClass().getSimpleName()
+ + " for position");
final Element root = DOM.createDiv();
setElement(root);
setStyleName(CLASS_NAME);
- scroller = DOM.createDiv();
- scroller.setClassName(CLASS_NAME + "-scroller");
- root.appendChild(scroller);
+ scrollerElem.setClassName(CLASS_NAME + "-scroller");
+ root.appendChild(scrollerElem);
- innerScroller = DOM.createDiv();
- scroller.appendChild(innerScroller);
+ scrollerElem.appendChild(innerScrollerElem);
tableWrapper = DOM.createDiv();
tableWrapper.setClassName(CLASS_NAME + "-tablewrapper");
@@ -614,13 +1913,37 @@ public class Escalator extends Widget {
@Override
public void onAttachOrDetach(final AttachEvent event) {
if (event.isAttached()) {
+
+ /*
+ * this specific order of method calls matters: header and
+ * footer get defined heights, the body assumes to get the
+ * rest.
+ */
+ header.paintInsertRows(0, header.getRowCount());
+ footer.paintInsertRows(0, footer.getRowCount());
recalculateElementSizes();
+ body.paintInsertRows(0, body.getRowCount());
+
+ scroller.attachScrollListener(scrollerElem);
+ scroller.attachMousewheelListener(getElement());
+ } else {
+ scroller.detachScrollListener(scrollerElem);
+ scroller.detachMousewheelListener(getElement());
}
}
});
}
private void detectAndApplyPositionFunction() {
+ /*
+ * firefox has a bug in its translate operation, showing white space
+ * when adjusting the scrollbar in BodyRowContainer.paintInsertRows
+ */
+ if (Window.Navigator.getUserAgent().contains("Firefox")) {
+ position = new AbsolutePosition();
+ return;
+ }
+
final Style docStyle = Document.get().getBody().getStyle();
if (hasProperty(docStyle, "transform")) {
if (hasProperty(docStyle, "transformStyle")) {
@@ -633,10 +1956,6 @@ public class Escalator extends Widget {
} else {
position = new AbsolutePosition();
}
-
- getLogger().info(
- "Using " + position.getClass().getSimpleName()
- + " for position");
}
private Logger getLogger() {
@@ -728,24 +2047,178 @@ public class Escalator extends Widget {
recalculateElementSizes();
}
+ /**
+ * Returns the vertical scroll offset. Note that this is not necessarily the
+ * same as the scroll top in the DOM
+ *
+ * @return the logical vertical scroll offset
+ */
+ public double getScrollTop() {
+ return scrollerElem.getScrollTop();
+ }
+
+ /**
+ * Sets the vertical scroll offset. Note that this is not necessarily the
+ * same as the scroll top in the DOM
+ *
+ * @param scrollTop
+ * the number of pixels to scroll vertically
+ */
+ public void setScrollTop(final double scrollTop) {
+ scrollerElem.setScrollTop((int) scrollTop);
+ }
+
+ /**
+ * Returns the logical horizontal scroll offset. Note that this is not
+ * necessarily the same as the scroll left in the DOM.
+ *
+ * @return the logical horizontal scroll offset
+ */
+ public int getScrollLeft() {
+ return scrollerElem.getScrollLeft();
+ }
+
+ /**
+ * Sets the logical horizontal scroll offset. Note that this is not
+ * necessarily the same as the scroll left in the DOM.
+ *
+ * @param scrollLeft
+ * the number of pixels to scroll horizontally
+ */
+ public void setScrollLeft(final int scrollLeft) {
+ scrollerElem.setScrollLeft(scrollLeft);
+ }
+
+ /**
+ * Scrolls the body horizontally so that the column at the given index is
+ * visible.
+ *
+ * @param columnIndex
+ * the index of the column to scroll to
+ * @param destination
+ * where the column should be aligned visually after scrolling
+ * @throws IndexOutOfBoundsException
+ * if {@code columnIndex} is not a valid index for an existing
+ * column
+ */
+ public void scrollToColumn(final int columnIndex,
+ final ScrollDestination destination)
+ throws IndexOutOfBoundsException {
+ // FIXME [[escalator]]
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ /**
+ * Scrolls the body horizontally so that the column at the given index is
+ * visible and there is at least {@code padding} pixels to the given scroll
+ * destination.
+ *
+ * @param columnIndex
+ * the index of the column to scroll to
+ * @param destination
+ * where the column should be aligned visually after scrolling
+ * @param padding
+ * the number pixels to place between the scrolled-to column and
+ * the viewport edge.
+ * @throws IndexOutOfBoundsException
+ * if {@code columnIndex} is not a valid index for an existing
+ * column
+ * @throws IllegalArgumentException
+ * if {@code destination} is {@link ScrollDestination#MIDDLE},
+ * because having a padding on a centered column is undefined
+ * behavior.
+ */
+ public void scrollToColumn(final int columnIndex,
+ final ScrollDestination destination, final int padding)
+ throws IndexOutOfBoundsException, IllegalArgumentException {
+ // FIXME [[escalator]]
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ /**
+ * Scrolls the body vertically so that the row at the given index is
+ * visible.
+ *
+ * @param rowIndex
+ * the index of the row to scroll to
+ * @param destination
+ * where the row should be aligned visually after scrolling
+ * @throws IndexOutOfBoundsException
+ * if {@code rowIndex} is not a valid index for an existing
+ * logical row
+ */
+ public void scrollToRow(final int rowIndex,
+ final ScrollDestination destination)
+ throws IndexOutOfBoundsException {
+ // FIXME [[escalator]]
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ /**
+ * Scrolls the body vertically so that the row at the given index is visible
+ * and there is at least {@literal padding} pixels to the given scroll
+ * destination.
+ *
+ * @param rowIndex
+ * the index of the logical row to scroll to
+ *
+ * @param destination
+ * where the row should be aligned visually after scrolling
+ * @param padding
+ * the number pixels to place between the scrolled-to row and the
+ * viewport edge.
+ * @throws IndexOutOfBoundsException
+ * if {@code rowIndex} is not a valid index for an existing row
+ * @throws IllegalArgumentException
+ * if {@code destination} is {@link ScrollDestination#MIDDLE},
+ * because having a padding on a centered row is undefined
+ * behavior.
+ */
+ public void scrollToRow(final int rowIndex,
+ final ScrollDestination destination, final int padding)
+ throws IndexOutOfBoundsException, IllegalArgumentException {
+ // FIXME [[escalator]]
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
private void recalculateElementSizes() {
- for (final RowContainerImpl rowContainer : rowContainers) {
+ width = getPreciseWidth(getElement());
+ height = getPreciseHeight(getElement());
+ for (final AbstractRowContainer 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);
+ scroller.recalculateTableWrapperSize();
+ scroller.recalculateScrollbarsForVirtualViewport();
}
- private static void recalculateRowWidth(Element tr) {
+ private boolean needsVerticalScrollbars() {
+ // TODO [[rowheight]]: take variable row heights into account
+ final double bodyHeight = ROW_HEIGHT_PX * body.getRowCount();
+ return height < header.height + bodyHeight + footer.height;
+ }
+
+ private boolean needsHorizontalScrollbars() {
+ // TODO [[colwidth]]: take variable column widths into account
+ return width < COLUMN_WIDTH_PX * columnConfiguration.getColumnCount();
+ }
+
+ private static void recalculateRowWidth(final Element tr) {
// TODO [[colwidth]]: adjust for variable column widths
tr.getStyle().setWidth(tr.getChildCount() * COLUMN_WIDTH_PX, Unit.PX);
}
+
+ /**
+ * A routing method for {@link Scroller#onScroll(double, double)}
+ * <p>
+ * This is a workaround for GWT and JSNI unable to properly handle inner
+ * classes, so instead we call the outer class' method, which calls the
+ * inner class' respective method.
+ * <p>
+ * Ideally, this method would not exist, and
+ * {@link Scroller#onScroll(double, double)} would be called directly.
+ */
+ private void onScroll() {
+ scroller.onScroll();
+ }
}
diff --git a/client/src/com/vaadin/client/ui/grid/PositionFunction.java b/client/src/com/vaadin/client/ui/grid/PositionFunction.java
index 24e119b6a4..d89d73ccd1 100644
--- a/client/src/com/vaadin/client/ui/grid/PositionFunction.java
+++ b/client/src/com/vaadin/client/ui/grid/PositionFunction.java
@@ -58,7 +58,7 @@ interface PositionFunction {
@Override
public void set(Element e, double x, double y) {
e.getStyle().setProperty("webkitTransform",
- "translate3d(" + x + "px," + y + "px,0");
+ "translate3d(" + x + "px," + y + "px,0)");
}
}
diff --git a/client/src/com/vaadin/client/ui/grid/Range.java b/client/src/com/vaadin/client/ui/grid/Range.java
new file mode 100644
index 0000000000..6dbb287e57
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/Range.java
@@ -0,0 +1,362 @@
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.vaadin.client.ui.grid;
+
+/**
+ * An immutable representation of a range, marked by start and end points.
+ * <p>
+ * The range is treated as inclusive at the start, and exclusive at the end.
+ * I.e. the range [0..1[ has the length 1, and represents one integer: 0.
+ * <p>
+ * The range is considered {@link #isEmpty() empty} if the start is the same as
+ * the end.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public final class Range {
+ private final int start;
+ private final int end;
+
+ /**
+ * Creates a range object representing a single integer.
+ *
+ * @param integer
+ * the number to represent as a range
+ * @return the range represented by <code>integer</code>
+ */
+ public static Range withOnly(final int integer) {
+ return new Range(integer, integer + 1);
+ }
+
+ /**
+ * Creates a range of all integers between two integers.
+ * <p>
+ * The range start is <em>inclusive</em> and the end is <em>exclusive</em>.
+ * So, a range "between" 0 and 5 represents the numbers 0, 1, 2, 3 and 4,
+ * but not 5.
+ *
+ * @param start
+ * the start of the the range, inclusive
+ * @param end
+ * the end of the range, exclusive
+ * @return a range representing <code>[start..end[</code>
+ * @throws IllegalArgumentException
+ * if <code>start &gt; end</code>
+ */
+ public static Range between(final int start, final int end)
+ throws IllegalArgumentException {
+ return new Range(start, end);
+ }
+
+ /**
+ * Creates a range from a start point, with a given length.
+ *
+ * @param start
+ * the first integer to include in the range
+ * @param length
+ * the length of the resulting range
+ * @return a range of all the integers from <code>start</code>, with
+ * <code>length</code> number of integers following
+ * @throws IllegalArgumentException
+ * if length &lt; 0
+ */
+ public static Range withLength(final int start, final int length)
+ throws IllegalArgumentException {
+ if (length < 0) {
+ /*
+ * The constructor of Range will throw an exception if start >
+ * start+length (i.e. if length is negative). We're throwing the
+ * same exception type, just with a more descriptive message.
+ */
+ throw new IllegalArgumentException("length must not be negative");
+ }
+ return new Range(start, start + length);
+ }
+
+ /**
+ * Creates a new range between two numbers: <code>[start..end[</code>.
+ *
+ * @param start
+ * the start integer, inclusive
+ * @param end
+ * the end integer, exclusive
+ * @throws IllegalArgumentException
+ * if <code>start &gt; end</code>
+ */
+ private Range(final int start, final int end)
+ throws IllegalArgumentException {
+ if (start > end) {
+ throw new IllegalArgumentException(
+ "start must not be greater than end");
+ }
+
+ this.start = start;
+ this.end = end;
+ }
+
+ /**
+ * Returns the <em>inclusive</em>start point of this range.
+ *
+ * @return the start point of this range
+ */
+ public int getStart() {
+ return start;
+ }
+
+ /**
+ * Returns the <em>exclusive</em> end point of this range.
+ *
+ * @return the end point of this range
+ */
+ public int getEnd() {
+ return end;
+ }
+
+ /**
+ * The number of integers contained in the range.
+ *
+ * @return the number of integers contained in the range
+ */
+ public int length() {
+ return getEnd() - getStart();
+ }
+
+ /**
+ * Checks whether the range has no elements between the start and end.
+ *
+ * @return <code>true</code> iff the range contains no elements.
+ */
+ public boolean isEmpty() {
+ return getStart() >= getEnd();
+ }
+
+ /**
+ * Checks whether this range and another range shares integers.
+ * <p>
+ * An empty range never intersects with any other range.
+ *
+ * @param other
+ * the other range to check against
+ * @return <code>true</code> if this and <code>other</code> have any
+ * integers in common
+ */
+ public boolean intersects(final Range other) {
+ if (!isEmpty() && !other.isEmpty()) {
+ return getStart() < other.getEnd() && other.getStart() < getEnd();
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Checks whether an integer is found within this range.
+ *
+ * @param integer
+ * an integer to test for presence in this range
+ * @return <code>true</code> iff <code>integer</code> is in this range
+ */
+ public boolean contains(final int integer) {
+ return getStart() <= integer && integer < getEnd();
+ }
+
+ /**
+ * Checks whether this range is a subset of another range.
+ *
+ * @return <code>true</code> iff <code>other</code> completely wraps this
+ * range
+ */
+ public boolean isSubsetOf(final Range other) {
+ return other.getStart() <= getStart() && getEnd() <= other.getEnd();
+ }
+
+ /**
+ * Overlay this range with another one, and partition the ranges according
+ * to how they position relative to each other.
+ * <p>
+ * The three partitions are returned as a three-element Range array:
+ * <ul>
+ * <li>Elements in this range that occur before elements in
+ * <code>other</code>.
+ * <li>Elements that are shared between the two ranges.
+ * <li>Elements in this range that occur after elements in
+ * <code>other</code>.
+ * </ul>
+ *
+ * @param other
+ * the other range to act as delimiters.
+ * @return a three-element Range array of partitions depicting the elements
+ * before (index 0), shared/inside (index 1) and after (index 2).
+ */
+ public Range[] partitionWith(final Range other) {
+ final Range[] splitBefore = splitAt(other.getStart());
+ final Range rangeBefore = splitBefore[0];
+ final Range[] splitAfter = splitBefore[1].splitAt(other.getEnd());
+ final Range rangeInside = splitAfter[0];
+ final Range rangeAfter = splitAfter[1];
+ return new Range[] { rangeBefore, rangeInside, rangeAfter };
+ }
+
+ /**
+ * Get a range that is based on this one, but offset by a number.
+ *
+ * @param offset
+ * the number to offset by
+ * @return a copy of this range, offset by <code>offset</code>
+ */
+ public Range offsetBy(final int offset) {
+ if (offset == 0) {
+ return this;
+ } else {
+ return new Range(start + offset, end + offset);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + " [" + getStart() + ".." + getEnd()
+ + "[" + (isEmpty() ? " (empty)" : "");
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + end;
+ result = prime * result + start;
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final Range other = (Range) obj;
+ if (end != other.end) {
+ return false;
+ }
+ if (start != other.start) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Checks whether this range starts before the start of another range.
+ *
+ * @param other
+ * the other range to compare against
+ * @return <code>true</code> iff this range starts before the
+ * <code>other</code>
+ */
+ public boolean startsBefore(final Range other) {
+ return getStart() < other.getStart();
+ }
+
+ /**
+ * Checks whether this range ends before the start of another range.
+ *
+ * @param other
+ * the other range to compare against
+ * @return <code>true</code> iff this range ends before the
+ * <code>other</code>
+ */
+ public boolean endsBefore(final Range other) {
+ return getEnd() <= other.getStart();
+ }
+
+ /**
+ * Checks whether this range ends after the end of another range.
+ *
+ * @param other
+ * the other range to compare against
+ * @return <code>true</code> iff this range ends after the
+ * <code>other</code>
+ */
+ public boolean endsAfter(final Range other) {
+ return getEnd() > other.getEnd();
+ }
+
+ /**
+ * Checks whether this range starts after the end of another range.
+ *
+ * @param other
+ * the other range to compare against
+ * @return <code>true</code> iff this range starts after the
+ * <code>other</code>
+ */
+ public boolean startsAfter(final Range other) {
+ return getStart() >= other.getEnd();
+ }
+
+ /**
+ * Split the range into two at a certain integer.
+ * <p>
+ * <em>Example:</em> <code>[5..10[.splitAt(7) == [5..7[, [7..10[</code>
+ *
+ * @param integer
+ * the integer at which to split the range into two
+ * @return an array of two ranges, with <code>[start..integer[</code> in the
+ * first element, and <code>[integer..end[</code> in the second
+ * element.
+ * <p>
+ * If {@code integer} is less than {@code start}, [empty,
+ * {@code this} ] is returned. if <code>integer</code> is equal to
+ * or greater than {@code end}, [{@code this}, empty] is returned
+ * instead.
+ */
+ public Range[] splitAt(final int integer) {
+ if (integer < start) {
+ return new Range[] { Range.withLength(integer, 0), this };
+ } else if (integer >= end) {
+ return new Range[] { this, Range.withLength(integer, 0) };
+ } else {
+ return new Range[] { new Range(start, integer),
+ new Range(integer, end) };
+ }
+ }
+
+ /**
+ * Split the range into two after a certain number of integers into the
+ * range.
+ * <p>
+ * Calling this method is equivalent to calling
+ * <code>{@link #splitAt(int) splitAt}({@link #getStart()}+length);</code>
+ * <p>
+ * <em>Example:</em>
+ * <code>[5..10[.splitAtFromStart(2) == [5..7[, [7..10[</code>
+ *
+ * @param length
+ * the length at which to split this range into two
+ * @return an array of two ranges, having the <code>length</code>-first
+ * elements of this range, and the second range having the rest. If
+ * <code>length</code> &leq; 0, the first element will be empty, and
+ * the second element will be this range. If <code>length</code>
+ * &geq; {@link #length()}, the first element will be this range,
+ * and the second element will be empty.
+ */
+ public Range[] splitAtFromStart(final int length) {
+ return splitAt(getStart() + length);
+ }
+}
diff --git a/client/src/com/vaadin/client/ui/grid/RowContainer.java b/client/src/com/vaadin/client/ui/grid/RowContainer.java
index 1a1a20073d..5e826fe743 100644
--- a/client/src/com/vaadin/client/ui/grid/RowContainer.java
+++ b/client/src/com/vaadin/client/ui/grid/RowContainer.java
@@ -49,29 +49,29 @@ public interface RowContainer {
throws IllegalArgumentException;
/**
- * Removes rows at a certain offset in the current row container.
+ * Removes rows at a certain index in the current row container.
*
- * @param offset
+ * @param index
* the index of the first row to be removed
* @param numberOfRows
- * the number of rows to remove, starting from the offset
+ * the number of rows to remove, starting from the index
* @throws IndexOutOfBoundsException
* if any integer number in the range
- * <code>[offset..(offset+numberOfRows)]</code> is not an
- * existing row index
+ * <code>[index..(index+numberOfRows)]</code> is not an existing
+ * row index
* @throws IllegalArgumentException
* if {@code numberOfRows} is less than 1.
*/
- public void removeRows(int offset, int numberOfRows)
+ public void removeRows(int index, int numberOfRows)
throws IndexOutOfBoundsException, IllegalArgumentException;
/**
- * Adds rows at a certain offset in this row container.
+ * Adds rows at a certain index 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.
+ * The new rows will be inserted between the row at the index, and the row
+ * before (an index of 0 means that the rows are inserted at the beginning).
+ * Therefore, the rows currently at the index and afterwards will be moved
+ * downwards.
* <p>
* The contents of the inserted rows will subsequently be queried from the
* cell renderer.
@@ -81,19 +81,19 @@ public interface RowContainer {
* {@link #refreshRows(int, int)} needs to be called for those rows
* separately.
*
- * @param offset
+ * @param index
* 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>
+ * the number of rows to insert after the <code>index</code>
* @see #setCellRenderer(CellRenderer)
* @throws IndexOutOfBoundsException
- * if <code>offset</code> is not an integer in the range
+ * if <code>index</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)
+ public void insertRows(int index, int numberOfRows)
throws IndexOutOfBoundsException, IllegalArgumentException;
/**
@@ -102,19 +102,19 @@ public interface RowContainer {
* The data for the refreshed rows are queried from the current cell
* renderer.
*
- * @param offset
+ * @param index
* the index of the first row that will be updated
* @param numberOfRows
- * the number of rows to update, starting from the offset
+ * the number of rows to update, starting from the index
* @see #setCellRenderer(CellRenderer)
* @throws IndexOutOfBoundsException
* if any integer number in the range
- * <code>[offset..(offset+numberOfColumns)]</code> is not an
+ * <code>[index..(index+numberOfColumns)]</code> is not an
* existing column index.
* @throws IllegalArgumentException
* if {@code numberOfRows} is less than 1.
*/
- public void refreshRows(int offset, int numberOfRows)
+ public void refreshRows(int index, int numberOfRows)
throws IndexOutOfBoundsException, IllegalArgumentException;
/**
diff --git a/client/src/com/vaadin/client/ui/grid/ScrollDestination.java b/client/src/com/vaadin/client/ui/grid/ScrollDestination.java
new file mode 100644
index 0000000000..8910216ac9
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/ScrollDestination.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.ui.grid;
+
+/**
+ * The destinations that are supported in an Escalator when scrolling rows or
+ * columns into view.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public enum ScrollDestination {
+ /**
+ * "scrollIntoView" i.e. scroll as little as possible to show the target
+ * element. If the element fits into view, this works as START or END
+ * depending on the current scroll position. If the element does not fit
+ * into view, this works as START.
+ */
+ ANY,
+ /**
+ * Scrolls so that the element is shown at the start of the view port.
+ */
+ START,
+ /**
+ * Scrolls so that the element is shown in the middle of the view port.
+ */
+ MIDDLE,
+ /**
+ * Scrolls so that the element is shown at the end of the view port.
+ */
+ END
+} \ No newline at end of file
diff --git a/client/tests/src/com/vaadin/client/ui/grid/PartitioningTest.java b/client/tests/src/com/vaadin/client/ui/grid/PartitioningTest.java
new file mode 100644
index 0000000000..3cbc6351b1
--- /dev/null
+++ b/client/tests/src/com/vaadin/client/ui/grid/PartitioningTest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.vaadin.client.ui.grid;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+@SuppressWarnings("static-method")
+public class PartitioningTest {
+
+ @Test
+ public void selfRangeTest() {
+ final Range range = Range.between(0, 10);
+ final Range[] partitioning = range.partitionWith(range);
+
+ assertTrue("before is empty", partitioning[0].isEmpty());
+ assertTrue("inside is self", partitioning[1].equals(range));
+ assertTrue("after is empty", partitioning[2].isEmpty());
+ }
+
+ @Test
+ public void beforeRangeTest() {
+ final Range beforeRange = Range.between(0, 10);
+ final Range afterRange = Range.between(10, 20);
+ final Range[] partitioning = beforeRange.partitionWith(afterRange);
+
+ assertTrue("before is self", partitioning[0].equals(beforeRange));
+ assertTrue("inside is empty", partitioning[1].isEmpty());
+ assertTrue("after is empty", partitioning[2].isEmpty());
+ }
+
+ @Test
+ public void afterRangeTest() {
+ final Range beforeRange = Range.between(0, 10);
+ final Range afterRange = Range.between(10, 20);
+ final Range[] partitioning = afterRange.partitionWith(beforeRange);
+
+ assertTrue("before is empty", partitioning[0].isEmpty());
+ assertTrue("inside is empty", partitioning[1].isEmpty());
+ assertTrue("after is self", partitioning[2].equals(afterRange));
+ }
+
+ @Test
+ public void beforeAndInsideRangeTest() {
+ final Range beforeRange = Range.between(0, 10);
+ final Range afterRange = Range.between(5, 15);
+ final Range[] partitioning = beforeRange.partitionWith(afterRange);
+
+ assertEquals("before", Range.between(0, 5), partitioning[0]);
+ assertEquals("inside", Range.between(5, 10), partitioning[1]);
+ assertTrue("after is empty", partitioning[2].isEmpty());
+ }
+
+ @Test
+ public void insideRangeTest() {
+ final Range fullRange = Range.between(0, 20);
+ final Range insideRange = Range.between(5, 15);
+ final Range[] partitioning = insideRange.partitionWith(fullRange);
+
+ assertTrue("before is empty", partitioning[0].isEmpty());
+ assertEquals("inside", Range.between(5, 15), partitioning[1]);
+ assertTrue("after is empty", partitioning[2].isEmpty());
+ }
+
+ @Test
+ public void insideAndBelowTest() {
+ final Range beforeRange = Range.between(0, 10);
+ final Range afterRange = Range.between(5, 15);
+ final Range[] partitioning = afterRange.partitionWith(beforeRange);
+
+ assertTrue("before is empty", partitioning[0].isEmpty());
+ assertEquals("inside", Range.between(5, 10), partitioning[1]);
+ assertEquals("after", Range.between(10, 15), partitioning[2]);
+ }
+
+ @Test
+ public void aboveAndBelowTest() {
+ final Range fullRange = Range.between(0, 20);
+ final Range insideRange = Range.between(5, 15);
+ final Range[] partitioning = fullRange.partitionWith(insideRange);
+
+ assertEquals("before", Range.between(0, 5), partitioning[0]);
+ assertEquals("inside", Range.between(5, 15), partitioning[1]);
+ assertEquals("after", Range.between(15, 20), partitioning[2]);
+ }
+}
diff --git a/client/tests/src/com/vaadin/client/ui/grid/RangeTest.java b/client/tests/src/com/vaadin/client/ui/grid/RangeTest.java
new file mode 100644
index 0000000000..a4715924b4
--- /dev/null
+++ b/client/tests/src/com/vaadin/client/ui/grid/RangeTest.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.ui.grid;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+@SuppressWarnings("static-method")
+public class RangeTest {
+
+ @Test(expected = IllegalArgumentException.class)
+ public void startAfterEndTest() {
+ Range.between(10, 9);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void negativeLengthTest() {
+ Range.withLength(10, -1);
+ }
+
+ @Test
+ public void constructorEquivalenceTest() {
+ assertEquals("10 == [10,11[", Range.withOnly(10), Range.between(10, 11));
+ assertEquals("[10,20[ == 10, length 10", Range.between(10, 20),
+ Range.withLength(10, 10));
+ assertEquals("10 == 10, length 1", Range.withOnly(10),
+ Range.withLength(10, 1));
+ }
+
+ @Test
+ public void boundsTest() {
+ {
+ final Range range = Range.between(0, 10);
+ assertEquals("between(0, 10) start", 0, range.getStart());
+ assertEquals("between(0, 10) end", 10, range.getEnd());
+ }
+
+ {
+ final Range single = Range.withOnly(10);
+ assertEquals("withOnly(10) start", 10, single.getStart());
+ assertEquals("withOnly(10) end", 11, single.getEnd());
+ }
+
+ {
+ final Range length = Range.withLength(10, 5);
+ assertEquals("withLength(10, 5) start", 10, length.getStart());
+ assertEquals("withLength(10, 5) end", 15, length.getEnd());
+ }
+ }
+
+ @Test
+ @SuppressWarnings("boxing")
+ public void equalsTest() {
+ final Range range1 = Range.between(0, 10);
+ final Range range2 = Range.withLength(0, 11);
+
+ assertTrue("null", !range1.equals(null));
+ assertTrue("reflexive", range1.equals(range1));
+ assertEquals("symmetric", range1.equals(range2), range2.equals(range1));
+ }
+
+ @Test
+ public void containsTest() {
+ final int start = 0;
+ final int end = 10;
+ final Range range = Range.between(start, end);
+
+ assertTrue("start should be contained", range.contains(start));
+ assertTrue("start-1 should not be contained",
+ !range.contains(start - 1));
+ assertTrue("end should not be contained", !range.contains(end));
+ assertTrue("end-1 should be contained", range.contains(end - 1));
+
+ assertTrue("[0..10[ contains 5", Range.between(0, 10).contains(5));
+ assertTrue("empty range does not contain 5", !Range.between(5, 5)
+ .contains(5));
+ }
+
+ @Test
+ public void emptyTest() {
+ assertTrue("[0..0[ should be empty", Range.between(0, 0).isEmpty());
+ assertTrue("Range of length 0 should be empty", Range.withLength(0, 0)
+ .isEmpty());
+
+ assertTrue("[0..1[ should not be empty", !Range.between(0, 1).isEmpty());
+ assertTrue("Range of length 1 should not be empty",
+ !Range.withLength(0, 1).isEmpty());
+ }
+
+ @Test
+ public void splitTest() {
+ final Range startRange = Range.between(0, 10);
+ final Range[] splitRanges = startRange.splitAt(5);
+ assertEquals("[0..10[ split at 5, lower", Range.between(0, 5),
+ splitRanges[0]);
+ assertEquals("[0..10[ split at 5, upper", Range.between(5, 10),
+ splitRanges[1]);
+ }
+
+ @Test
+ public void emptySplitTest() {
+ final Range range = Range.between(5, 10);
+ final Range[] split1 = range.splitAt(0);
+ assertTrue("split1, [0]", split1[0].isEmpty());
+ assertEquals("split1, [1]", range, split1[1]);
+
+ final Range[] split2 = range.splitAt(15);
+ assertEquals("split2, [0]", range, split2[0]);
+ assertTrue("split2, [1]", split2[1].isEmpty());
+ }
+
+ @Test
+ public void lengthTest() {
+ assertEquals("withLength length", 5, Range.withLength(10, 5).length());
+ assertEquals("between length", 5, Range.between(10, 15).length());
+ assertEquals("withOnly 10 length", 1, Range.withOnly(10).length());
+ }
+
+ @Test
+ public void intersectsTest() {
+ assertTrue("[0..10[ intersects [5..15[", Range.between(0, 10)
+ .intersects(Range.between(5, 15)));
+ assertTrue("[0..10[ does not intersect [10..20[", !Range.between(0, 10)
+ .intersects(Range.between(10, 20)));
+ assertTrue("empty range does not intersect with [0..10[", !Range
+ .between(5, 5).intersects(Range.between(0, 10)));
+ assertTrue("[0..10[ does not intersect with empty range", !Range
+ .between(0, 10).intersects(Range.between(5, 5)));
+ }
+
+ @Test
+ public void subsetTest() {
+ assertTrue("[5..10[ is subset of [0..20[", Range.between(5, 10)
+ .isSubsetOf(Range.between(0, 20)));
+
+ final Range range = Range.between(0, 10);
+ assertTrue("range is subset of self", range.isSubsetOf(range));
+
+ assertTrue("[0..10[ is not subset of [5..15[", !Range.between(0, 10)
+ .isSubsetOf(Range.between(5, 15)));
+ }
+
+ @Test
+ public void offsetTest() {
+ assertEquals(Range.between(5, 15), Range.between(0, 10).offsetBy(5));
+ }
+
+ @Test
+ public void rangeStartsBeforeTest() {
+ final Range former = Range.between(0, 5);
+ final Range latter = Range.between(1, 5);
+ assertTrue("former should starts before latter",
+ former.startsBefore(latter));
+ assertTrue("latter shouldn't start before latter",
+ !latter.startsBefore(former));
+
+ assertTrue("no overlap allowed",
+ !Range.between(0, 5).startsBefore(Range.between(0, 10)));
+ }
+
+ @Test
+ public void rangeStartsAfterTest() {
+ final Range former = Range.between(0, 5);
+ final Range latter = Range.between(5, 10);
+ assertTrue("latter should start after former",
+ latter.startsAfter(former));
+ assertTrue("former shouldn't start after latter",
+ !former.startsAfter(latter));
+
+ assertTrue("no overlap allowed",
+ !Range.between(5, 10).startsAfter(Range.between(0, 6)));
+ }
+
+ @Test
+ public void rangeEndsBeforeTest() {
+ final Range former = Range.between(0, 5);
+ final Range latter = Range.between(5, 10);
+ assertTrue("latter should end before former", former.endsBefore(latter));
+ assertTrue("former shouldn't end before latter",
+ !latter.endsBefore(former));
+
+ assertTrue("no overlap allowed",
+ !Range.between(5, 10).endsBefore(Range.between(9, 15)));
+ }
+
+ @Test
+ public void rangeEndsAfterTest() {
+ final Range former = Range.between(1, 5);
+ final Range latter = Range.between(1, 6);
+ assertTrue("latter should end after former", latter.endsAfter(former));
+ assertTrue("former shouldn't end after latter",
+ !former.endsAfter(latter));
+
+ assertTrue("no overlap allowed",
+ !Range.between(0, 10).endsAfter(Range.between(5, 10)));
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridTest.html b/uitest/src/com/vaadin/tests/components/grid/GridTest.html
new file mode 100644
index 0000000000..76ebbf1807
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridTest.html
@@ -0,0 +1,151 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+<link rel="selenium.base" href="http://localhost:8888/" />
+<title>GridTest</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr><td rowspan="1" colspan="3">GridTest</td></tr>
+</thead><tbody>
+<tr>
+ <td>open</td>
+ <td>/run/com.vaadin.tests.components.grid.GridTest?restartApplication</td>
+ <td></td>
+</tr>
+<tr>
+ <td>verifyText</td>
+ <td>vaadin=runcomvaadintestscomponentsgridGridTest::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[1]/domChild[0]/domChild[1]/domChild[0]/domChild[0]</td>
+ <td>Logical row 0/0</td>
+</tr>
+<tr>
+ <td>verifyText</td>
+ <td>vaadin=runcomvaadintestscomponentsgridGridTest::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[1]/domChild[0]/domChild[1]/domChild[9]/domChild[0]</td>
+ <td>Logical row 9/9</td>
+</tr>
+<tr>
+ <td>verifyTextNotPresent</td>
+ <td>Logical row 0/10</td>
+ <td></td>
+</tr>
+<tr>
+ <td>verifyTextNotPresent</td>
+ <td>Logical row 11/11</td>
+ <td></td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>vaadin=runcomvaadintestscomponentsgridGridTest::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[1]/VHorizontalLayout[0]/Slot[0]/VTextField[0]</td>
+ <td>0</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>vaadin=runcomvaadintestscomponentsgridGridTest::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[1]/VHorizontalLayout[0]/Slot[1]/VTextField[0]</td>
+ <td>1</td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>vaadin=runcomvaadintestscomponentsgridGridTest::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[1]/VHorizontalLayout[0]/Slot[2]/VButton[0]/domChild[0]/domChild[0]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>verifyText</td>
+ <td>vaadin=runcomvaadintestscomponentsgridGridTest::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[1]/domChild[0]/domChild[1]/domChild[0]/domChild[0]</td>
+ <td>Logical row 0/10</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>vaadin=runcomvaadintestscomponentsgridGridTest::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[1]/VHorizontalLayout[0]/Slot[0]/VTextField[0]</td>
+ <td>11</td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>vaadin=runcomvaadintestscomponentsgridGridTest::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[1]/VHorizontalLayout[0]/Slot[2]/VButton[0]/domChild[0]/domChild[0]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>verifyText</td>
+ <td>vaadin=runcomvaadintestscomponentsgridGridTest::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[1]/domChild[0]/domChild[1]/domChild[11]/domChild[0]</td>
+ <td>Logical row 11/11</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>vaadin=runcomvaadintestscomponentsgridGridTest::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[1]/VHorizontalLayout[0]/Slot[0]/VTextField[0]</td>
+ <td>0</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>vaadin=runcomvaadintestscomponentsgridGridTest::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[1]/VHorizontalLayout[0]/Slot[1]/VTextField[0]</td>
+ <td>100</td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>vaadin=runcomvaadintestscomponentsgridGridTest::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[1]/VHorizontalLayout[0]/Slot[2]/VButton[0]/domChild[0]/domChild[0]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>verifyText</td>
+ <td>vaadin=runcomvaadintestscomponentsgridGridTest::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[1]/domChild[0]/domChild[1]/domChild[0]/domChild[0]</td>
+ <td>Logical row 0/12</td>
+</tr>
+<tr>
+ <td>verifyText</td>
+ <td>vaadin=runcomvaadintestscomponentsgridGridTest::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[1]/domChild[0]/domChild[1]/domChild[10]/domChild[0]</td>
+ <td>Logical row 17/29</td>
+</tr>
+<tr>
+ <td>scroll</td>
+ <td>vaadin=runcomvaadintestscomponentsgridGridTest::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[0]</td>
+ <td>1109</td>
+</tr>
+<tr>
+ <td>verifyTextPresent</td>
+ <td>Logical row 56/68</td>
+ <td></td>
+</tr>
+<tr>
+ <td>verifyTextPresent</td>
+ <td>Logical row 72/84</td>
+ <td></td>
+</tr>
+<tr>
+ <td>scroll</td>
+ <td>vaadin=runcomvaadintestscomponentsgridGridTest::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[0]</td>
+ <td>1875</td>
+</tr>
+<tr>
+ <td>verifyTextPresent</td>
+ <td>Logical row 111/</td>
+ <td></td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>vaadin=runcomvaadintestscomponentsgridGridTest::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[2]/VHorizontalLayout[0]/Slot[0]/VTextField[0]</td>
+ <td>111</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>vaadin=runcomvaadintestscomponentsgridGridTest::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[2]/VHorizontalLayout[0]/Slot[1]/VTextField[0]</td>
+ <td>1</td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>vaadin=runcomvaadintestscomponentsgridGridTest::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[2]/VHorizontalLayout[0]/Slot[2]/VButton[0]/domChild[0]/domChild[0]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>verifyText</td>
+ <td>vaadin=runcomvaadintestscomponentsgridGridTest::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[1]/domChild[0]/domChild[1]/domChild[17]/domChild[0]</td>
+ <td>Logical row 110/144</td>
+</tr>
+<tr>
+ <td>verifyTextNotPresent</td>
+ <td>Logical row 111/</td>
+ <td></td>
+</tr>
+</tbody></table>
+</body>
+</html>
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridTest.java b/uitest/src/com/vaadin/tests/components/grid/GridTest.java
index cd8a13423c..aab3c66acf 100644
--- a/uitest/src/com/vaadin/tests/components/grid/GridTest.java
+++ b/uitest/src/com/vaadin/tests/components/grid/GridTest.java
@@ -21,6 +21,11 @@ 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;
+import com.vaadin.ui.Button;
+import com.vaadin.ui.Button.ClickEvent;
+import com.vaadin.ui.HorizontalLayout;
+import com.vaadin.ui.Layout;
+import com.vaadin.ui.TextField;
/**
* @since 7.2
@@ -29,8 +34,85 @@ import com.vaadin.tests.widgetset.server.grid.TestGrid;
@Widgetset(TestingWidgetSet.NAME)
public class GridTest extends AbstractTestUI {
@Override
- protected void setup(VaadinRequest request) {
- addComponent(new TestGrid());
+ protected void setup(final VaadinRequest request) {
+ final TestGrid grid = new TestGrid();
+ addComponent(grid);
+
+ final Layout insertRowsLayout = new HorizontalLayout();
+ final TextField insertRowsOffset = new TextField();
+ insertRowsLayout.addComponent(insertRowsOffset);
+ final TextField insertRowsAmount = new TextField();
+ insertRowsLayout.addComponent(insertRowsAmount);
+ insertRowsLayout.addComponent(new Button("insert rows",
+ new Button.ClickListener() {
+ @Override
+ @SuppressWarnings("boxing")
+ public void buttonClick(final ClickEvent event) {
+ int offset = Integer.valueOf(insertRowsOffset
+ .getValue());
+ int amount = Integer.valueOf(insertRowsAmount
+ .getValue());
+ grid.insertRows(offset, amount);
+ }
+ }));
+ addComponent(insertRowsLayout);
+
+ final Layout removeRowsLayout = new HorizontalLayout();
+ final TextField removeRowsOffset = new TextField();
+ removeRowsLayout.addComponent(removeRowsOffset);
+ final TextField removeRowsAmount = new TextField();
+ removeRowsLayout.addComponent(removeRowsAmount);
+ removeRowsLayout.addComponent(new Button("remove rows",
+ new Button.ClickListener() {
+ @Override
+ @SuppressWarnings("boxing")
+ public void buttonClick(final ClickEvent event) {
+ int offset = Integer.valueOf(removeRowsOffset
+ .getValue());
+ int amount = Integer.valueOf(removeRowsAmount
+ .getValue());
+ grid.removeRows(offset, amount);
+ }
+ }));
+ addComponent(removeRowsLayout);
+
+ final Layout insertColumnsLayout = new HorizontalLayout();
+ final TextField insertColumnsOffset = new TextField();
+ insertColumnsLayout.addComponent(insertColumnsOffset);
+ final TextField insertColumnsAmount = new TextField();
+ insertColumnsLayout.addComponent(insertColumnsAmount);
+ insertColumnsLayout.addComponent(new Button("insert columns",
+ new Button.ClickListener() {
+ @Override
+ @SuppressWarnings("boxing")
+ public void buttonClick(final ClickEvent event) {
+ int offset = Integer.valueOf(insertColumnsOffset
+ .getValue());
+ int amount = Integer.valueOf(insertColumnsAmount
+ .getValue());
+ grid.insertColumns(offset, amount);
+ }
+ }));
+ addComponent(insertColumnsLayout);
+
+ final Layout removeColumnsLayout = new HorizontalLayout();
+ final TextField removeColumnsOffset = new TextField();
+ removeColumnsLayout.addComponent(removeColumnsOffset);
+ final TextField removeColumnsAmount = new TextField();
+ removeColumnsLayout.addComponent(removeColumnsAmount);
+ removeColumnsLayout.addComponent(new Button("remove columns",
+ new Button.ClickListener() {
+ @Override
+ @SuppressWarnings("boxing")
+ public void buttonClick(final ClickEvent event) {
+ int offset = Integer.valueOf(removeColumnsOffset
+ .getValue());
+ int amount = Integer.valueOf(removeColumnsAmount
+ .getValue());
+ grid.removeColumns(offset, amount);
+ }
+ }));
+ addComponent(removeColumnsLayout);
}
@Override
diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridClientRpc.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridClientRpc.java
new file mode 100644
index 0000000000..878e04ef39
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridClientRpc.java
@@ -0,0 +1,28 @@
+/*
+ * 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.communication.ClientRpc;
+
+public interface TestGridClientRpc extends ClientRpc {
+ void insertRows(int offset, int amount);
+
+ void removeRows(int offset, int amount);
+
+ void insertColumns(int offset, int amount);
+
+ void removeColumns(int offset, int amount);
+}
diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridConnector.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridConnector.java
index ef624d6fc8..382d01e04e 100644
--- a/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridConnector.java
+++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridConnector.java
@@ -28,6 +28,29 @@ public class TestGridConnector extends AbstractComponentConnector {
@Override
protected void init() {
super.init();
+ registerRpc(TestGridClientRpc.class, new TestGridClientRpc() {
+ @Override
+ public void insertRows(int offset, int amount) {
+ getWidget().getBody().insertRows(offset, amount);
+ }
+
+ @Override
+ public void removeRows(int offset, int amount) {
+ getWidget().getBody().removeRows(offset, amount);
+ }
+
+ @Override
+ public void removeColumns(int offset, int amount) {
+ getWidget().getColumnConfiguration().removeColumns(offset,
+ amount);
+ }
+
+ @Override
+ public void insertColumns(int offset, int amount) {
+ getWidget().getColumnConfiguration().insertColumns(offset,
+ amount);
+ }
+ });
}
@Override
diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridState.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridState.java
index 9aeca0bdbe..8f9cd3c371 100644
--- a/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridState.java
+++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridState.java
@@ -22,7 +22,8 @@ import com.vaadin.shared.AbstractComponentState;
* @author Vaadin Ltd
*/
public class TestGridState extends AbstractComponentState {
- public static final String DEFAULT_HEIGHT = "400px";
+ // public static final String DEFAULT_HEIGHT = "400px";
+ public static final String DEFAULT_HEIGHT = "405px";
/* 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
index 274b01b166..b3dff67338 100644
--- a/uitest/src/com/vaadin/tests/widgetset/client/grid/VTestGrid.java
+++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/VTestGrid.java
@@ -19,10 +19,16 @@ public class VTestGrid extends Composite {
public static class BodyRenderer implements CellRenderer {
private int i = 0;
+ private int ri = 0;
@Override
public void renderCell(final Cell cell) {
- cell.getElement().setInnerText("Cell #" + (i++));
+ if (cell.getColumn() != 0) {
+ cell.getElement().setInnerText("Cell #" + (i++));
+ } else {
+ cell.getElement().setInnerText(
+ "Logical row " + cell.getRow() + "/" + (ri++));
+ }
double c = i * .1;
int r = (int) ((Math.cos(c) + 1) * 128);
@@ -30,8 +36,10 @@ public class VTestGrid extends Composite {
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) {
+ if ((r * .8 + g * 1.3 + b * .9) / 3 < 127) {
cell.getElement().getStyle().setColor("white");
+ } else {
+ cell.getElement().getStyle().clearColor();
}
}
}
@@ -49,12 +57,8 @@ public class VTestGrid extends Composite {
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
+ cConf.insertColumns(cConf.getColumnCount(), 5);
final RowContainer h = escalator.getHeader();
h.setCellRenderer(new HeaderRenderer());
@@ -62,52 +66,22 @@ public class VTestGrid extends Composite {
final RowContainer b = escalator.getBody();
b.setCellRenderer(new BodyRenderer());
- b.insertRows(0, 5);
+ b.insertRows(0, 10);
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
+ public RowContainer getBody() {
+ return escalator.getBody();
}
- private void step4() {
- final ColumnConfiguration cConf = escalator.getColumnConfiguration();
- cConf.removeColumns(0, 1);
- cConf.removeColumns(1, 1);
- cConf.removeColumns(cConf.getColumnCount() - 1, 1);
+ public ColumnConfiguration getColumnConfiguration() {
+ return escalator.getColumnConfiguration();
}
-
- 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
index 7ae7e03193..7fc20420d2 100644
--- a/uitest/src/com/vaadin/tests/widgetset/server/grid/TestGrid.java
+++ b/uitest/src/com/vaadin/tests/widgetset/server/grid/TestGrid.java
@@ -15,6 +15,7 @@
*/
package com.vaadin.tests.widgetset.server.grid;
+import com.vaadin.tests.widgetset.client.grid.TestGridClientRpc;
import com.vaadin.tests.widgetset.client.grid.TestGridState;
import com.vaadin.ui.AbstractComponent;
@@ -32,4 +33,24 @@ public class TestGrid extends AbstractComponent {
protected TestGridState getState() {
return (TestGridState) super.getState();
}
+
+ public void insertRows(int offset, int amount) {
+ rpc().insertRows(offset, amount);
+ }
+
+ public void removeRows(int offset, int amount) {
+ rpc().removeRows(offset, amount);
+ }
+
+ public void insertColumns(int offset, int amount) {
+ rpc().insertColumns(offset, amount);
+ }
+
+ public void removeColumns(int offset, int amount) {
+ rpc().removeColumns(offset, amount);
+ }
+
+ private TestGridClientRpc rpc() {
+ return getRpcProxy(TestGridClientRpc.class);
+ }
}