From ecb954092f2bb3e3b2c5b1acfc7447993ed84468 Mon Sep 17 00:00:00 2001 From: Henrik Paul Date: Wed, 9 Oct 2013 14:35:10 +0300 Subject: [PATCH] Implement escalator pattern for widget (#12645) Change-Id: Ibdc5a5162ae88e886e74d93f3f75f4ea3c6dab89 --- .../themes/base/escalator/escalator.scss | 48 +- .../client/ui/grid/ColumnConfiguration.java | 28 +- .../com/vaadin/client/ui/grid/Escalator.java | 1945 +++++++++++++++-- .../client/ui/grid/PositionFunction.java | 2 +- .../src/com/vaadin/client/ui/grid/Range.java | 362 +++ .../vaadin/client/ui/grid/RowContainer.java | 38 +- .../client/ui/grid/ScrollDestination.java | 45 + .../client/ui/grid/PartitioningTest.java | 102 + .../com/vaadin/client/ui/grid/RangeTest.java | 212 ++ .../tests/components/grid/GridTest.html | 151 ++ .../tests/components/grid/GridTest.java | 86 +- .../client/grid/TestGridClientRpc.java | 28 + .../client/grid/TestGridConnector.java | 23 + .../widgetset/client/grid/TestGridState.java | 3 +- .../widgetset/client/grid/VTestGrid.java | 58 +- .../tests/widgetset/server/grid/TestGrid.java | 21 + 16 files changed, 2823 insertions(+), 329 deletions(-) create mode 100644 client/src/com/vaadin/client/ui/grid/Range.java create mode 100644 client/src/com/vaadin/client/ui/grid/ScrollDestination.java create mode 100644 client/tests/src/com/vaadin/client/ui/grid/PartitioningTest.java create mode 100644 client/tests/src/com/vaadin/client/ui/grid/RangeTest.java create mode 100644 uitest/src/com/vaadin/tests/components/grid/GridTest.html create mode 100644 uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridClientRpc.java 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 to offset the header's dimension is, + * for some strange reason, inherited into each contained . + * 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 - * [offset..(offset+numberOfColumns)] is not an + * [index..(index+numberOfColumns)] is not an * existing column index. * @throws IllegalArgumentException * if numberOfColumns 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. *

- * 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. *

* 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 offset + * the number of columns to insert after the index * @throws IndexOutOfBoundsException - * if offset is not an integer in the range + * if index is not an integer in the range * [0..{@link #getColumnCount()}] * @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 + element within a 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 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. + *

+ * 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 }, {@code } or * {@code }) the rows (i.e. {@code } 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 } or {@code }. - */ - 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 { *

* 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. + *

+ * Usually a {@code } or {@code }. + * + * @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,212 +570,1116 @@ public class Escalator extends Widget { + numberOfRows + ")"); } - if (offset < 0 || offset + numberOfRows > getRowCount()) { - throw new IndexOutOfBoundsException("The given " - + "row range (" + offset + ".." - + (offset + numberOfRows) - + ") was outside of the current number of rows (" - + getRowCount() + ")"); + if (index < 0 || index + numberOfRows > getRowCount()) { + throw new IndexOutOfBoundsException("The given " + + "row range (" + index + ".." + (index + numberOfRows) + + ") was outside of the current number of rows (" + + getRowCount() + ")"); + } + } + + @Override + public int getRowCount() { + return rows; + } + + /** + * {@inheritDoc} + *

+ * Implementation detail: This method does no DOM modifications + * (i.e. is very cheap to call) if there is no data for columns when + * this method is called. + * + * @see #hasColumnAndRowData() + */ + @Override + public void insertRows(final int 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() + ")"); + } + + if (numberOfRows < 1) { + throw new IllegalArgumentException( + "Number of rows must be 1 or greater (was " + + numberOfRows + ")"); + } + + rows += numberOfRows; + + /* + * 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); + } + } + + /** + * 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 paintInsertRows(final int visualIndex, + final int numberOfRows) { + assert isAttached() : "Can't paint rows if Escalator is not attached"; + + final List addedRows = new ArrayList(); + + if (numberOfRows < 1) { + return addedRows; + } + + Node referenceNode; + if (root.getChildCount() != 0 && visualIndex != 0) { + // get the row node we're inserting stuff after + referenceNode = root.getChild(visualIndex - 1); + } else { + // index is 0, so just prepend. + referenceNode = null; + } + + 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(); + paintCell(cellElem, row, col); + tr.appendChild(cellElem); + } + + /* + * TODO [[optimize]] [[rowwidth]]: When this method is updated + * to measure things instead of using hardcoded values, it would + * be better to do everything at once after all rows have been + * updated to reduce the number of reflows. + */ + recalculateRowWidth(tr); + tr.addClassName(CLASS_NAME + "-row"); + + if (referenceNode != null) { + root.insertAfter(tr, referenceNode); + } else { + /* + * referencenode being null means we have index 0, i.e. make + * it the first row + */ + /* + * TODO [[optimize]]: Is insertFirst or append faster for an + * empty root? + */ + root.insertFirst(tr); + } + + /* + * to get the rows to appear one after another in a logical + * order, update the reference + */ + referenceNode = tr; + } + + recalculateSectionHeight(); + + return addedRows; + } + + protected void recalculateSectionHeight() { + final double newHeight = root.getChildCount() * ROW_HEIGHT_PX; + if (newHeight != height) { + height = newHeight; + sectionHeightCalculated(); + } + } + + /** + * {@inheritDoc} + *

+ * Implementation detail: This method does no DOM modifications + * (i.e. is very cheap to call) if there is no data for columns when + * this method is called. + * + * @see #hasColumnAndRowData() + */ + @Override + public void refreshRows(final int index, final int numberOfRows) { + assertArgumentsAreValidAndWithinRange(index, numberOfRows); + + if (!isAttached()) { + return; + } + + /* + * 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 + * refreshRowPositions() as needed + */ + /* + * TODO [[colwidth]]: reapply column and colspan widths as + * needed + */ + + 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) { + /* + * TODO [[optimize]]: Only do this for new cells or when a row + * height or column width actually changes. Or is it a NOOP when + * re-setting a property to its current value? + */ + cellElem.getStyle().setHeight(ROW_HEIGHT_PX, Unit.PX); + cellElem.getStyle().setWidth(COLUMN_WIDTH_PX, Unit.PX); + + /* + * TODO [[optimize]]: Don't create a new instance every time a cell + * is rendered + */ + final CellImpl cell = new CellImpl(cellElem, row, col); + /* + * TODO [[optimize]] [[API]]: Let the renderer know whether the cell + * is new so that it can use a quicker route if it can deduct that + * the elements that it has put there in a previous rendering is + * still there and the contents only need to be updated. + */ + renderer.renderCell(cell); + + /* + * TODO [[optimize]]: Only do this for cells that have not already + * been rendered. + */ + cellElem.addClassName(CLASS_NAME + "-cell"); + } + + /** + * 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 visualRowOrder = new LinkedList(); + + /** + * 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 rowTopPosMap = new HashMap(); + + 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 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 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 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 visualSourceRange.getStart(), + * visualTargetIndex or + * logicalTargetIndex is a negative number; or + * if visualTargetInfo 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 removedRows = new ArrayList( + 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 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 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. + *

+ * Note: {@link Scroller#onScroll(double, double)} + * will 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 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 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(); } } @Override - public int getRowCount() { - return rows; - } + protected void paintRemoveRows(final int index, final int numberOfRows) { - /** - * {@inheritDoc} - *

- * Implementation detail: This method does no DOM modifications - * (i.e. is very cheap to call) if there is no data for columns when - * this method is called. - * - * @see #hasColumnAndRowData() - */ - @Override - public void insertRows(final int offset, final int numberOfRows) { - if (offset < 0 || offset > getRowCount()) { - throw new IndexOutOfBoundsException("The given offset (" - + offset - + ") was outside of the current number of rows (0.." - + getRowCount() + ")"); - } + final Range viewportRange = Range.withLength( + getLogicalRowIndex(visualRowOrder.getFirst()), + visualRowOrder.size()); - if (numberOfRows < 1) { - throw new IllegalArgumentException( - "Number of rows must be 1 or greater (was " - + numberOfRows + ")"); - } + final Range removedRowsRange = Range + .withLength(index, numberOfRows); - rows += numberOfRows; + final Range[] partitions = removedRowsRange + .partitionWith(viewportRange); + final Range removedAbove = partitions[0]; + final Range removedLogicalInside = partitions[1]; + final Range removedVisualInside = convertToVisual(removedLogicalInside); /* - * TODO [[escalator]]: modify offset and numberOfRows so that they - * suit the current viewport. If a partial dataset is shown,update - * only the part that is visible. If the viewport doesn't show any - * of the modifications, this method does nothing. + * TODO: 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. */ /* - * TODO [[escalator]]: assert that escalatorChildIndex is a number - * equal or less than the number of escalator rows + * 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. */ - - Node referenceNode; - if (root.getChildCount() != 0 && offset != 0) { - // get the row node we're inserting stuff after - referenceNode = root.getChild(offset - 1); - } else { - // there are now rows, so just append. - referenceNode = null; + 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()); + } } - for (int row = offset; row < offset + numberOfRows; row++) { - final Element tr = DOM.createTR(); - - for (int col = 0; col < columnConfiguration.getColumnCount(); col++) { - final Element cellElem = createCellElement(); - paintCell(cellElem, row, col); - tr.appendChild(cellElem); - } + // ranges evaluated, let's do things. + if (!removedVisualInside.isEmpty()) { + int escalatorRowCount = bodyElem.getChildCount(); /* - * TODO [[optimize]] [[rowwidth]]: When this method is updated - * to measure things instead of using hardcoded values, it would - * be better to do everything at once after all rows have been - * updated to reduce the number of reflows. + * If we're left with less rows than the number of escalators, + * remove the unused ones. */ - recalculateRowWidth(tr); - tr.addClassName(CLASS_NAME + "-row"); + 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; - position.set(tr, 0, row * ROW_HEIGHT_PX); + /* + * 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); - if (referenceNode != null) { - root.insertAfter(tr, referenceNode); - } else { /* - * referencenode being null means we have offset 0, i.e. - * make it the first row + * 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); + } + /* - * TODO [[optimize]]: Is insertFirst or append faster for an - * empty root? + * this is how many rows appeared into the viewport from + * below */ - root.insertFirst(tr); + 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); + } } - /* - * to get the rows to appear one after another in a logical - * order, update the reference - */ - referenceNode = tr; + 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); + } + } } /* - * we need to update the positions of all rows beneath the ones - * added right now. + * this needs to be done after the escalator has been shrunk down, + * or it won't work correctly (due to setScrollTop invocation) */ - refreshRowPositions(offset + numberOfRows, getRowCount()); + scroller.recalculateScrollbarsForVirtualViewport(); + } - /* - * TODO [[optimize]]: maybe the height doesn't always change? + private void paintRemoveRowsAtMiddle(final Range removedLogicalInside, + final Range removedVisualInside, final int logicalOffset) { + /*- + * : : : + * |2| |2| |2| + * |3| ==> |*| ==> |4| + * |4| |4| |6| <- newly rendered + * : : : */ - recalculateSectionHeight(); + + 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 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; + } } - /** - * 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); + private void paintRemoveRowsAtBottom(final Range removedLogicalInside, + final Range removedVisualInside) { + /*- + * : + * : : |4| <- newly rendered + * |5| |5| |5| + * |6| ==> |*| ==> |7| + * |7| |7| + */ - for (int row = startRow; row < endRow; row++) { - Element tr = (Element) root.getChild(row); - position.set(tr, 0, row * ROW_HEIGHT_PX); + final int logicalTargetIndex = getLogicalRowIndex(visualRowOrder + .getFirst()) - removedVisualInside.length(); + moveAndUpdateEscalatorRows(removedVisualInside, 0, + logicalTargetIndex); + + // move the surrounding rows to their correct places. + final ListIterator 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 void recalculateSectionHeight() { - /* TODO [[optimize]]: only do this if the height has changed */ - sectionHeightCalculated(root.getChildCount() * 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. } /** - * {@inheritDoc} + * Adjusts the row index and number to be relevant for the current + * virtual viewport. *

- * Implementation detail: This method does no DOM modifications - * (i.e. is very cheap to call) if there is no data for columns when - * this method is called. + * It converts a logical range of rows index to the matching visual + * range, truncating the resulting range with the viewport. + *

+ *

* - * @see #hasColumnAndRowData() + * @return a logical range converted to a visual range, truncated to the + * current viewport. The first visual row has the index 0. */ - @Override - public void refreshRows(final int offset, final int numberOfRows) { - assertArgumentsAreValidAndWithinRange(offset, numberOfRows); + private Range convertToVisual(final Range logicalRange) { + if (logicalRange.isEmpty()) { + return logicalRange; + } /* - * TODO [[escalator]]: modify offset and numberOfRows to fit in the - * current viewport. If they don't fall into the current viewport, - * NOOP + * 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); + } - if (hasColumnAndRowData()) { - /* - * TODO [[rowheight]]: nudge rows down with - * refreshRowPositions() as needed - */ - /* - * TODO [[colwidth]]: reapply column and colspan widths as - * needed - */ + @Override + protected Element createCellElement() { + return DOM.createTD(); + } - for (int row = offset; row < offset + numberOfRows; row++) { - Node tr = root.getChild(row); - for (int col = 0; col < tr.getChildCount(); col++) { - paintCell((Element) tr.getChild(col), row, col); - } + private 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); } } } - private void paintCell(final Element cellElem, final int row, - final int col) { - /* - * TODO [[optimize]]: Only do this for new cells or when a row - * height or column width actually changes. Or is it a NOOP when - * re-setting a property to its current value? - */ - cellElem.getStyle().setHeight(ROW_HEIGHT_PX, Unit.PX); - cellElem.getStyle().setWidth(COLUMN_WIDTH_PX, Unit.PX); - - /* - * TODO [[optimize]]: Don't create a new instance every time a cell - * is rendered - */ - final CellImpl cell = new CellImpl(cellElem, row, col); - /* - * TODO [[optimize]] [[API]]: Let the renderer know whether the cell - * is new so that it can use a quicker route if it can deduct that - * the elements that it has put there in a previous rendering is - * still there and the contents only need to be updated. - */ - renderer.renderCell(cell); + @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); + } + } - /* - * TODO [[optimize]]: Only do this for cells that have not already - * been rendered. - */ - cellElem.addClassName(CLASS_NAME + "-cell"); + private void setBodyScrollPosition(final int scrollLeft, + final int scrollTop) { + tBodyScrollLeft = scrollLeft; + tBodyScrollTop = scrollTop; + position.set(bodyElem, -tBodyScrollLeft, -tBodyScrollTop); } } @@ -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 } tag. */ private final Element headElem = DOM.createTHead(); + /** The {@code } tag. */ private final Element bodyElem = DOM.createTBody(); + /** The {@code } 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)} + *

+ * 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. + *

+ * 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. + *

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

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

+ * The range start is inclusive and the end is exclusive. + * So, a range "between" 0 and 5 represents the numbers 0, 1, 2, 3 and 4, + * but not 5. + * + * @param start + * the start of the the range, inclusive + * @param end + * the end of the range, exclusive + * @return a range representing [start..end[ + * @throws IllegalArgumentException + * if start > end + */ + public static Range between(final int start, final int end) + throws IllegalArgumentException { + return new Range(start, end); + } + + /** + * Creates a range from a start point, with a given length. + * + * @param start + * the first integer to include in the range + * @param length + * the length of the resulting range + * @return a range of all the integers from start, with + * length number of integers following + * @throws IllegalArgumentException + * if length < 0 + */ + public static Range withLength(final int start, final int length) + throws IllegalArgumentException { + if (length < 0) { + /* + * The constructor of Range will throw an exception if start > + * start+length (i.e. if length is negative). We're throwing the + * same exception type, just with a more descriptive message. + */ + throw new IllegalArgumentException("length must not be negative"); + } + return new Range(start, start + length); + } + + /** + * Creates a new range between two numbers: [start..end[. + * + * @param start + * the start integer, inclusive + * @param end + * the end integer, exclusive + * @throws IllegalArgumentException + * if start > end + */ + private Range(final int start, final int end) + throws IllegalArgumentException { + if (start > end) { + throw new IllegalArgumentException( + "start must not be greater than end"); + } + + this.start = start; + this.end = end; + } + + /** + * Returns the inclusivestart point of this range. + * + * @return the start point of this range + */ + public int getStart() { + return start; + } + + /** + * Returns the exclusive end point of this range. + * + * @return the end point of this range + */ + public int getEnd() { + return end; + } + + /** + * The number of integers contained in the range. + * + * @return the number of integers contained in the range + */ + public int length() { + return getEnd() - getStart(); + } + + /** + * Checks whether the range has no elements between the start and end. + * + * @return true iff the range contains no elements. + */ + public boolean isEmpty() { + return getStart() >= getEnd(); + } + + /** + * Checks whether this range and another range shares integers. + *

+ * An empty range never intersects with any other range. + * + * @param other + * the other range to check against + * @return true if this and other 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 true iff integer is in this range + */ + public boolean contains(final int integer) { + return getStart() <= integer && integer < getEnd(); + } + + /** + * Checks whether this range is a subset of another range. + * + * @return true iff other completely wraps this + * range + */ + public boolean isSubsetOf(final Range other) { + return other.getStart() <= getStart() && getEnd() <= other.getEnd(); + } + + /** + * Overlay this range with another one, and partition the ranges according + * to how they position relative to each other. + *

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

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

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

+ * If {@code integer} is less than {@code start}, [empty, + * {@code this} ] is returned. if integer is equal to + * or greater than {@code end}, [{@code this}, empty] is returned + * instead. + */ + public Range[] splitAt(final int integer) { + if (integer < start) { + return new Range[] { Range.withLength(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. + *

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

+ * Example: + * [5..10[.splitAtFromStart(2) == [5..7[, [7..10[ + * + * @param length + * the length at which to split this range into two + * @return an array of two ranges, having the length-first + * elements of this range, and the second range having the rest. If + * length ≤ 0, the first element will be empty, and + * the second element will be this range. If length + * ≥ {@link #length()}, the first element will be this range, + * and the second element will be empty. + */ + public Range[] splitAtFromStart(final int length) { + return splitAt(getStart() + length); + } +} 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 - * [offset..(offset+numberOfRows)] is not an - * existing row index + * [index..(index+numberOfRows)] 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. *

- * 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. *

* 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 offset + * the number of rows to insert after the index * @see #setCellRenderer(CellRenderer) * @throws IndexOutOfBoundsException - * if offset is not an integer in the range + * if index is not an integer in the range * [0..{@link #getRowCount()}] * @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 - * [offset..(offset+numberOfColumns)] is not an + * [index..(index+numberOfColumns)] 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 @@ + + + + + + +GridTest + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
GridTest
open/run/com.vaadin.tests.components.grid.GridTest?restartApplication
verifyTextvaadin=runcomvaadintestscomponentsgridGridTest::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[1]/domChild[0]/domChild[1]/domChild[0]/domChild[0]Logical row 0/0
verifyTextvaadin=runcomvaadintestscomponentsgridGridTest::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[1]/domChild[0]/domChild[1]/domChild[9]/domChild[0]Logical row 9/9
verifyTextNotPresentLogical row 0/10
verifyTextNotPresentLogical row 11/11
typevaadin=runcomvaadintestscomponentsgridGridTest::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[1]/VHorizontalLayout[0]/Slot[0]/VTextField[0]0
typevaadin=runcomvaadintestscomponentsgridGridTest::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[1]/VHorizontalLayout[0]/Slot[1]/VTextField[0]1
clickvaadin=runcomvaadintestscomponentsgridGridTest::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[1]/VHorizontalLayout[0]/Slot[2]/VButton[0]/domChild[0]/domChild[0]
verifyTextvaadin=runcomvaadintestscomponentsgridGridTest::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[1]/domChild[0]/domChild[1]/domChild[0]/domChild[0]Logical row 0/10
typevaadin=runcomvaadintestscomponentsgridGridTest::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[1]/VHorizontalLayout[0]/Slot[0]/VTextField[0]11
clickvaadin=runcomvaadintestscomponentsgridGridTest::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[1]/VHorizontalLayout[0]/Slot[2]/VButton[0]/domChild[0]/domChild[0]
verifyTextvaadin=runcomvaadintestscomponentsgridGridTest::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[1]/domChild[0]/domChild[1]/domChild[11]/domChild[0]Logical row 11/11
typevaadin=runcomvaadintestscomponentsgridGridTest::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[1]/VHorizontalLayout[0]/Slot[0]/VTextField[0]0
typevaadin=runcomvaadintestscomponentsgridGridTest::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[1]/VHorizontalLayout[0]/Slot[1]/VTextField[0]100
clickvaadin=runcomvaadintestscomponentsgridGridTest::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[1]/VHorizontalLayout[0]/Slot[2]/VButton[0]/domChild[0]/domChild[0]
verifyTextvaadin=runcomvaadintestscomponentsgridGridTest::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[1]/domChild[0]/domChild[1]/domChild[0]/domChild[0]Logical row 0/12
verifyTextvaadin=runcomvaadintestscomponentsgridGridTest::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[1]/domChild[0]/domChild[1]/domChild[10]/domChild[0]Logical row 17/29
scrollvaadin=runcomvaadintestscomponentsgridGridTest::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[0]1109
verifyTextPresentLogical row 56/68
verifyTextPresentLogical row 72/84
scrollvaadin=runcomvaadintestscomponentsgridGridTest::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[0]1875
verifyTextPresentLogical row 111/
typevaadin=runcomvaadintestscomponentsgridGridTest::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[2]/VHorizontalLayout[0]/Slot[0]/VTextField[0]111
typevaadin=runcomvaadintestscomponentsgridGridTest::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[2]/VHorizontalLayout[0]/Slot[1]/VTextField[0]1
clickvaadin=runcomvaadintestscomponentsgridGridTest::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[2]/VHorizontalLayout[0]/Slot[2]/VButton[0]/domChild[0]/domChild[0]
verifyTextvaadin=runcomvaadintestscomponentsgridGridTest::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[1]/domChild[0]/domChild[1]/domChild[17]/domChild[0]Logical row 110/144
verifyTextNotPresentLogical row 111/
+ + 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); + } } -- 2.39.5