diff options
author | Henrik Paul <henrik@vaadin.com> | 2015-02-11 14:52:02 +0200 |
---|---|---|
committer | Henrik Paul <henrik@vaadin.com> | 2015-02-16 11:35:13 +0200 |
commit | 556c1aa06c547933bc36f347693c4b5b85bac149 (patch) | |
tree | 02d3263e3e3315d607d95f5d73d8d2d2efc64c4d | |
parent | 294dc686f6d05dde90f852f46429b46c2f87af35 (diff) | |
download | vaadin-framework-556c1aa06c547933bc36f347693c4b5b85bac149.tar.gz vaadin-framework-556c1aa06c547933bc36f347693c4b5b85bac149.zip |
Escalator supports adding spacer elements into DOM. (#16644)
This is the first step towards Grid's details rows: Escalator puts spacer elements
in the DOM, and is able to scroll around with them. The spacers are put in their
correct locations, but they will not affect the normal row elements in any way at
this time.
Change-Id: Id20090c4de117e07e332dcc81e9964360f778258
7 files changed, 441 insertions, 188 deletions
diff --git a/WebContent/VAADIN/themes/base/escalator/escalator.scss b/WebContent/VAADIN/themes/base/escalator/escalator.scss index 606dc6a7dd..6d146f3a74 100644 --- a/WebContent/VAADIN/themes/base/escalator/escalator.scss +++ b/WebContent/VAADIN/themes/base/escalator/escalator.scss @@ -133,4 +133,17 @@ z-index: 1; } + .#{$primaryStyleName}-spacer { + position: absolute; + display: block; + + // debug + background-color: rgba(0,0,0,0.6); + color: white; + + > td { + width: 100%; + height: 100%; + } + } } diff --git a/client/src/com/vaadin/client/widget/escalator/RowContainer.java b/client/src/com/vaadin/client/widget/escalator/RowContainer.java index 397336450e..5cc58cb47e 100644 --- a/client/src/com/vaadin/client/widget/escalator/RowContainer.java +++ b/client/src/com/vaadin/client/widget/escalator/RowContainer.java @@ -22,17 +22,48 @@ import com.google.gwt.dom.client.TableSectionElement; /** * A representation of the rows in each of the sections (header, body and - * footer) in an {@link Escalator}. + * footer) in an {@link com.vaadin.client.widgets.Escalator}. * * @since 7.4 * @author Vaadin Ltd - * @see Escalator#getHeader() - * @see Escalator#getBody() - * @see Escalator#getFooter() + * @see com.vaadin.client.widgets.Escalator#getHeader() + * @see com.vaadin.client.widgets.Escalator#getBody() + * @see com.vaadin.client.widgets.Escalator#getFooter() + * @see SpacerContainer */ public interface RowContainer { /** + * The row container for the body section in an + * {@link com.vaadin.client.widgets.Escalator}. + * <p> + * The body section can contain both rows and spacers. + * + * @since + * @author Vaadin Ltd + * @see com.vaadin.client.widgets.Escalator#getBody() + */ + public interface BodyRowContainer extends RowContainer { + /** + * Marks a spacer and its height. + * <p> + * If a spacer is already registered with the given row index, that + * spacer will be updated with the given height. + * + * @param rowIndex + * the row index for the spacer to modify. The affected + * spacer is underneath the given index + * @param height + * the pixel height of the spacer. If {@code height} is + * negative, the affected spacer (if exists) will be removed + * @throws IllegalArgumentException + * if {@code rowIndex} is not a valid row index + */ + void setSpacer(int rowIndex, double height) + throws IllegalArgumentException; + } + + /** * An arbitrary pixel height of a row, before any autodetection for the row * height has been made. * */ diff --git a/client/src/com/vaadin/client/widgets/Escalator.java b/client/src/com/vaadin/client/widgets/Escalator.java index ca54b97ca5..2536356c94 100644 --- a/client/src/com/vaadin/client/widgets/Escalator.java +++ b/client/src/com/vaadin/client/widgets/Escalator.java @@ -23,6 +23,7 @@ import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Map.Entry; +import java.util.TreeMap; import java.util.logging.Level; import java.util.logging.Logger; @@ -67,6 +68,7 @@ import com.vaadin.client.widget.escalator.PositionFunction.Translate3DPosition; import com.vaadin.client.widget.escalator.PositionFunction.TranslatePosition; import com.vaadin.client.widget.escalator.PositionFunction.WebkitTranslate3DPosition; import com.vaadin.client.widget.escalator.RowContainer; +import com.vaadin.client.widget.escalator.RowContainer.BodyRowContainer; import com.vaadin.client.widget.escalator.RowVisibilityChangeEvent; import com.vaadin.client.widget.escalator.RowVisibilityChangeHandler; import com.vaadin.client.widget.escalator.ScrollbarBundle; @@ -93,7 +95,7 @@ import com.vaadin.shared.util.SharedUtil; |-- AbstractStaticRowContainer | |-- HeaderRowContainer | `-- FooterContainer - `---- BodyRowContainer + `---- BodyRowContainerImpl AbstractRowContainer is intended to contain all common logic between RowContainers. It manages the bookkeeping of row @@ -106,7 +108,7 @@ import com.vaadin.shared.util.SharedUtil; are pretty thin special cases of a StaticRowContainer (mostly relating to positioning of the root element). - BodyRowContainer could also be split into an additional + BodyRowContainerImpl 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 @@ -118,8 +120,8 @@ import com.vaadin.shared.util.SharedUtil; 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): + matters primarily for the BodyRowContainerImpl, because of + the way it scrolls through data): - Logical index - Physical (or DOM) index @@ -137,9 +139,9 @@ import com.vaadin.shared.util.SharedUtil; (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. + BodyRowContainerImpl 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 @@ -147,20 +149,20 @@ import com.vaadin.shared.util.SharedUtil; 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 + again, BodyRowContainerImpl has no such relationship. The body's visual index has additionally no apparent relationship with its physical index. Because the <tr> tags are reused in the body and visually repositioned with CSS as the user scrolls, the relationship between physical index and visual index is quickly broken. You can get an element's visual index via the field - BodyRowContainer.visualRowOrder. + BodyRowContainerImpl.visualRowOrder. Currently, the physical and visual indices are kept in sync _most of the time_ by a deferred rearrangement of rows. They become desynced when scrolling. This is to help screen readers to read the contents from the DOM in a natural - order. See BodyRowContainer.DeferredDomSorter for more + order. See BodyRowContainerImpl.DeferredDomSorter for more about that. */ @@ -272,18 +274,15 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * re-measure) */ /* - * [[rowheight]]: This code will require alterations that are relevant for - * being able to support variable row heights. NOTE: these bits can most - * often also be identified by searching for code reading the ROW_HEIGHT_PX - * constant. - */ - /* * [[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();. */ + /* + * [[spacer]]: Code that is important to make spacers work. + */ /** * A utility class that contains utility methods that are usually called @@ -1139,10 +1138,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker public void scrollToRow(final int rowIndex, final ScrollDestination destination, final double padding) { - /* - * FIXME [[rowheight]]: coded to work only with default row heights - * - will not work with variable row heights - */ final double targetStartPx = body.getDefaultRowHeight() * rowIndex; final double targetEndPx = targetStartPx + body.getDefaultRowHeight(); @@ -1183,19 +1178,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker */ private String primaryStyleName = null; - /** - * A map containing cached values of an element's current top position. - * <p> - * 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)}, - * {@link #getRowTop(Element)} and - * {@link #removeRowPosition(Element)} instead. - */ - @Deprecated - private final Map<TableRowElement, Double> rowTopPositionMap = new HashMap<TableRowElement, Double>(); - private boolean defaultRowHeightShouldBeAutodetected = true; private double defaultRowHeight = INITIAL_DEFAULT_ROW_HEIGHT; @@ -1907,20 +1889,17 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker */ } - @SuppressWarnings("boxing") protected void setRowPosition(final TableRowElement tr, final int x, final double y) { - position.set(tr, x, y); - rowTopPositionMap.put(tr, y); + positions.set(tr, x, y); } - @SuppressWarnings("boxing") protected double getRowTop(final TableRowElement tr) { - return rowTopPositionMap.get(tr); + return positions.getTop(tr); } protected void removeRowPosition(TableRowElement tr) { - rowTopPositionMap.remove(tr); + positions.remove(tr); } public void autodetectRowHeightLater() { @@ -2204,18 +2183,7 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker return; } - /* - * TODO [[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 - */ for (int row = logicalRowRange.getStart(); row < logicalRowRange .getEnd(); row++) { final TableRowElement tr = getTrByVisualIndex(row); @@ -2290,7 +2258,8 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker } } - private class BodyRowContainer extends AbstractRowContainer { + private class BodyRowContainerImpl extends AbstractRowContainer implements + BodyRowContainer { /* * TODO [[optimize]]: check whether a native JsArray might be faster * than LinkedList @@ -2401,7 +2370,9 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker private DeferredDomSorter domSorter = new DeferredDomSorter(); - public BodyRowContainer(final TableSectionElement bodyElement) { + private final SpacerContainer spacerContainer = new SpacerContainer(); + + public BodyRowContainerImpl(final TableSectionElement bodyElement) { super(bodyElement); } @@ -2409,6 +2380,7 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker public void setStylePrimaryName(String primaryStyleName) { super.setStylePrimaryName(primaryStyleName); UIObject.setStylePrimaryName(root, primaryStyleName + "-body"); + spacerContainer.setStylePrimaryName(primaryStyleName); } public void updateEscalatorRowsOnScroll() { @@ -2431,21 +2403,13 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker if (viewportOffset > 0) { // there's empty room on top - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ int originalRowsToMove = (int) Math.ceil(viewportOffset / getDefaultRowHeight()); int rowsToMove = Math.min(originalRowsToMove, - root.getChildCount()); + visualRowOrder.size()); - final int end = root.getChildCount(); + final int end = visualRowOrder.size(); final int start = end - rowsToMove; - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ final int logicalRowIndex = (int) (scrollTop / getDefaultRowHeight()); moveAndUpdateEscalatorRows(Range.between(start, end), 0, logicalRowIndex); @@ -2457,11 +2421,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker else if (viewportOffset + getDefaultRowHeight() <= 0) { /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ - - /* * the viewport has been scrolled more than the topmost visual * row. */ @@ -2469,10 +2428,10 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker int originalRowsToMove = (int) Math.abs(viewportOffset / getDefaultRowHeight()); int rowsToMove = Math.min(originalRowsToMove, - root.getChildCount()); + visualRowOrder.size()); int logicalRowIndex; - if (rowsToMove < root.getChildCount()) { + if (rowsToMove < visualRowOrder.size()) { /* * We scroll so little that we can just keep adding the rows * below the current escalator @@ -2481,10 +2440,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker .getLast()) + 1; } else { /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ - /* * Since we're moving all escalator rows, we need to * calculate the first logical row index from the scroll * position. @@ -2498,13 +2453,13 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * 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(); + final int targetVisualIndex = visualRowOrder.size(); // 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 + * TODO [[spacer]]: 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 @@ -2583,10 +2538,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker */ scroller.recalculateScrollbarsForVirtualViewport(); - /* - * FIXME [[rowheight]]: coded to work only with default row heights - * - will not work with variable row heights - */ final boolean addedRowsAboveCurrentViewport = index * getDefaultRowHeight() < getScrollTop(); final boolean addedRowsBelowCurrentViewport = index @@ -2600,10 +2551,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * without re-evaluating any rows. */ - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ final double yDelta = numberOfRows * getDefaultRowHeight(); adjustScrollPosIgnoreEvents(yDelta); updateTopRowLogicalIndex(numberOfRows); @@ -2637,10 +2584,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker moveAndUpdateEscalatorRows(Range.between(start, end), visualTargetIndex, unupdatedLogicalStart); - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ // move the surrounding rows to their correct places. double rowTop = (unupdatedLogicalStart + (end - start)) * getDefaultRowHeight(); @@ -2649,10 +2592,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker while (i.hasNext()) { final TableRowElement tr = i.next(); setRowPosition(tr, 0, rowTop); - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ rowTop += getDefaultRowHeight(); } @@ -2762,10 +2701,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker } { // Reposition the rows that were moved - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ double newRowTop = logicalTargetIndex * getDefaultRowHeight(); final ListIterator<TableRowElement> iter = visualRowOrder @@ -2773,10 +2708,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker for (int i = 0; i < visualSourceRange.length(); i++) { final TableRowElement tr = iter.next(); setRowPosition(tr, 0, newRowTop); - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ newRowTop += getDefaultRowHeight(); } } @@ -2802,10 +2733,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker verticalScrollbar.setScrollPosByDelta(yDelta); - /* - * FIXME [[rowheight]]: coded to work only with default row heights - * - will not work with variable row heights - */ final double rowTopPos = yDelta - (yDelta % getDefaultRowHeight()); for (final TableRowElement tr : visualRowOrder) { setRowPosition(tr, 0, getRowTop(tr) + rowTopPos); @@ -2847,10 +2774,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * added. */ for (int i = 0; i < addedRows.size(); i++) { - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ setRowPosition(addedRows.get(i), 0, (index + i) * getDefaultRowHeight()); } @@ -2859,10 +2782,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker for (int i = index + addedRows.size(); i < visualRowOrder .size(); i++) { final TableRowElement tr = visualRowOrder.get(i); - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ setRowPosition(tr, 0, i * getDefaultRowHeight()); } @@ -2873,10 +2792,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker } private int getMaxEscalatorRowCapacity() { - /* - * FIXME [[rowheight]]: coded to work only with default row heights - * - will not work with variable row heights - */ final int maxEscalatorRowCapacity = (int) Math .ceil(calculateHeight() / getDefaultRowHeight()) + 1; @@ -2930,10 +2845,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker .isEmpty() && removedVisualInside.getStart() == 0; if (!removedAbove.isEmpty() || firstVisualRowIsRemoved) { - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ final double yDelta = removedAbove.length() * getDefaultRowHeight(); final double firstLogicalRowHeight = getDefaultRowHeight(); @@ -2996,10 +2907,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker final int dirtyRowsStart = removedLogicalInside.getStart(); for (int i = dirtyRowsStart; i < escalatorRowCount; i++) { final TableRowElement tr = visualRowOrder.get(i); - /* - * FIXME [[rowheight]]: coded to work only with default - * row heights - will not work with variable row heights - */ setRowPosition(tr, 0, i * getDefaultRowHeight()); } @@ -3039,10 +2946,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * double-refreshing. */ - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ final double contentBottom = getRowCount() * getDefaultRowHeight(); final double viewportBottom = tBodyScrollTop @@ -3097,7 +3000,7 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker */ /* - * FIXME [[rowheight]]: above if-clause is coded to only + * FIXME [[spacer]]: above if-clause is coded to only * work with default row heights - will not work with * variable row heights */ @@ -3160,12 +3063,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker for (int i = removedVisualInside.getStart(); i < escalatorRowCount; i++) { final TableRowElement tr = visualRowOrder.get(i); setRowPosition(tr, 0, (int) newTop); - - /* - * FIXME [[rowheight]]: coded to work only with - * default row heights - will not work with variable - * row heights - */ newTop += getDefaultRowHeight(); } @@ -3214,10 +3111,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * 5 */ - /* - * FIXME [[rowheight]]: coded to work only with default - * row heights - will not work with variable row heights - */ final int rowsScrolled = (int) (Math .ceil((viewportBottom - contentBottom) / getDefaultRowHeight())); @@ -3269,20 +3162,12 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker final ListIterator<TableRowElement> iterator = visualRowOrder .listIterator(removedVisualInside.getStart()); - /* - * FIXME [[rowheight]]: coded to work only with default row heights - * - will not work with variable row heights - */ double rowTop = (removedLogicalInside.getStart() + logicalOffset) * getDefaultRowHeight(); for (int i = removedVisualInside.getStart(); i < escalatorRowCount - removedVisualInside.length(); i++) { final TableRowElement tr = iterator.next(); setRowPosition(tr, 0, rowTop); - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ rowTop += getDefaultRowHeight(); } } @@ -3305,19 +3190,11 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker // move the surrounding rows to their correct places. final ListIterator<TableRowElement> iterator = visualRowOrder .listIterator(removedVisualInside.getEnd()); - /* - * FIXME [[rowheight]]: coded to work only with default row heights - * - will not work with variable row heights - */ double rowTop = removedLogicalInside.getStart() * getDefaultRowHeight(); while (iterator.hasNext()) { final TableRowElement tr = iterator.next(); setRowPosition(tr, 0, rowTop); - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ rowTop += getDefaultRowHeight(); } } @@ -3364,8 +3241,8 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker } /* - * TODO [[rowheight]]: these assumptions will be totally broken with - * variable row heights. + * TODO [[spacer]]: these assumptions will be totally broken with + * spacers. */ final int maxEscalatorRows = getMaxEscalatorRowCapacity(); final int currentTopRowIndex = getLogicalRowIndex(visualRowOrder @@ -3586,10 +3463,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker if (!visualRowOrder.isEmpty()) { final double firstRowTop = getRowTop(visualRowOrder .getFirst()); - /* - * FIXME [[rowheight]]: coded to work only with default row - * heights - will not work with variable row heights - */ final double firstRowMinTop = tBodyScrollTop - getDefaultRowHeight(); if (firstRowTop < firstRowMinTop) { @@ -3614,20 +3487,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker return; } - /* - * As an intermediate step between hard-coded row heights to crazily - * varying row heights, Escalator will support the modification of - * the default row height (which is applied to all rows). - * - * This allows us to do some assumptions and simplifications for - * now. This code is intended to be quite short-lived, but gives - * insight into what needs to be done when row heights change in the - * body, in a general sense. - * - * TODO [[rowheight]] remove this comment once row heights may - * genuinely vary. - */ - Profiler.enter("Escalator.BodyRowContainer.reapplyDefaultRowHeights"); /* step 1: resize and reposition rows */ @@ -3661,10 +3520,6 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker /* step 3: make sure we have the correct amount of escalator rows. */ verifyEscalatorCount(); - /* - * TODO [[rowheight]] This simply doesn't work with variable rows - * heights. - */ int logicalLogical = (int) (getRowTop(visualRowOrder.getFirst()) / getDefaultRowHeight()); setTopRowLogicalIndex(logicalLogical); @@ -3775,6 +3630,12 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker return new Cell(getLogicalRowIndex(rowElement), cell.getColumn(), cell.getElement()); } + + @Override + public void setSpacer(int rowIndex, double height) + throws IllegalArgumentException { + spacerContainer.setSpacer(rowIndex, height); + } } private class ColumnConfigurationImpl implements ColumnConfiguration { @@ -4250,6 +4111,226 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker } } + private class SpacerContainer { + + /** This is used mainly for testing purposes */ + private static final String SPACER_LOGICAL_ROW_PROPERTY = "vLogicalRow"; + + /* + * TODO [[optimize]] maybe convert the usage of this class to flyweight + * or object pooling pattern? + */ + private final class Spacer { + private TableCellElement spacerElement; + private TableRowElement root; + + public TableRowElement getRootElement() { + return root; + } + + public void setPosition(double x, double y) { + positions.set(root, x, y); + } + + /** + * Creates a new element structure for the spacer. + * <p> + * {@link #createDomStructure()} and + * {@link #setRootElement(Element)} can collectively only be called + * once, otherwise an {@link AssertionError} will be raised (if + * asserts are enabled). + */ + public void createDomStructure(double height) { + assert root == null || root.getParentElement() == null : "this spacer was already attached"; + + root = TableRowElement.as(DOM.createTR()); + spacerElement = TableCellElement.as(DOM.createTD()); + root.appendChild(spacerElement); + spacerElement.setInnerText("IAMA SPACER, AMA"); + initElements(height); + } + + private void initElements(double height) { + setHeight(height); + root.getStyle().setWidth(100, Unit.PCT); + + spacerElement.getStyle().setWidth(100, Unit.PCT); + spacerElement.setColSpan(getColumnConfiguration() + .getColumnCount()); + + setStylePrimaryName(getStylePrimaryName()); + } + + public void setRootElement(TableRowElement tr) { + assert root == null || root.getParentElement() == null : "this spacer was already attached"; + + assert tr != null : "tr may not be null"; + root = tr; + + assert tr.getChildCount() == 1 : "tr must have exactly one child"; + spacerElement = tr.getCells().getItem(0); + assert spacerElement != null : "spacer element somehow was null"; + } + + public void setStylePrimaryName(String style) { + UIObject.setStylePrimaryName(root, style + "-spacer"); + } + + public void setHeight(double newHeight) { + root.getStyle().setHeight(newHeight, Unit.PX); + getLogger().warning( + "spacer's height changed, but pushing rows out of " + + "the way not implemented yet"); + } + } + + private final TreeMap<Integer, Double> rowIndexToHeight = new TreeMap<Integer, Double>(); + private final TreeMap<Integer, TableRowElement> rowIndexToSpacerElement = new TreeMap<Integer, TableRowElement>(); + + public void setSpacer(int rowIndex, double height) + throws IllegalArgumentException { + if (rowIndex < 0 || rowIndex >= getBody().getRowCount()) { + throw new IllegalArgumentException("invalid row index: " + + rowIndex + ", while the body only has " + + getBody().getRowCount() + " rows."); + } + + if (height >= 0) { + insertOrUpdateSpacer(rowIndex, height); + } else if (spacerExists(rowIndex)) { + removeSpacer(rowIndex); + } + } + + @SuppressWarnings("boxing") + private void insertOrUpdateSpacer(int rowIndex, double height) { + if (!spacerExists(rowIndex)) { + insertSpacer(rowIndex, height); + } else { + updateSpacer(rowIndex, height); + } + rowIndexToHeight.put(rowIndex, height); + } + + private boolean spacerExists(int rowIndex) { + Integer rowIndexObj = Integer.valueOf(rowIndex); + boolean spacerExists = rowIndexToHeight.containsKey(rowIndexObj); + assert spacerExists == rowIndexToSpacerElement + .containsKey(rowIndexObj) : "Inconsistent bookkeeping detected."; + return spacerExists; + } + + @SuppressWarnings("boxing") + private void insertSpacer(int rowIndex, double height) { + Spacer spacer = createSpacer(height); + spacer.getRootElement().setPropertyInt(SPACER_LOGICAL_ROW_PROPERTY, + rowIndex); + TableRowElement spacerRoot = spacer.getRootElement(); + rowIndexToSpacerElement.put(rowIndex, spacerRoot); + spacer.setPosition(0, getSpacerTop(rowIndex)); + spacerRoot.getStyle().setWidth( + columnConfiguration.calculateRowWidth(), Unit.PX); + body.getElement().appendChild(spacerRoot); + } + + private void updateSpacer(int rowIndex, double newHeight) { + getSpacer(rowIndex).setHeight(newHeight); + } + + @SuppressWarnings("boxing") + private Spacer getSpacer(int rowIndex) { + Spacer spacer = new Spacer(); + spacer.setRootElement(rowIndexToSpacerElement.get(rowIndex)); + return spacer; + } + + private Spacer createSpacer(double height) { + /* + * Optimally, this would be a factory method in SpacerImpl, but + * since it's not a static class, we can't do that directly. We + * could make it static, and pass in references, but that probably + * will become hairy pretty quickly. + */ + + Spacer spacer = new Spacer(); + spacer.createDomStructure(height); + return spacer; + } + + private double getSpacerTop(int rowIndex) { + double spacerHeights = 0; + + /*- + * TODO: uncomment this bit once the spacers start pushing the + * rows downwards, offseting indices. OTOH this entire method + * probably needs to be usable by BodyContainerImpl as well, + * since this same logic can/should be used for calculating the + * top position for rows. + + // Sum all spacer heights that occur before rowIndex. + for (Double spacerHeight : rowIndexToHeight.headMap( + Integer.valueOf(rowIndex), false).values()) { + spacerHeights += spacerHeight.doubleValue(); + } + */ + + double rowHeights = getBody().getDefaultRowHeight() + * (rowIndex + 1); + + return rowHeights + spacerHeights; + } + + @SuppressWarnings("boxing") + private void removeSpacer(int rowIndex) { + Spacer spacer = getSpacer(rowIndex); + + // fix DOM + spacer.setHeight(0); // resets row offsets + spacer.getRootElement().removeFromParent(); + + // fix bookkeeping + rowIndexToHeight.remove(rowIndex); + rowIndexToSpacerElement.remove(rowIndex); + } + + public void setStylePrimaryName(String style) { + for (TableRowElement spacerRoot : rowIndexToSpacerElement.values()) { + Spacer spacer = new Spacer(); + spacer.setRootElement(spacerRoot); + spacer.setStylePrimaryName(style); + } + } + } + + private class ElementPositionBookkeeper { + /** + * A map containing cached values of an element's current top position. + * <p> + * Don't use this field directly, because it will not take proper care + * of all the bookkeeping required. + */ + private final Map<Element, Double> elementTopPositionMap = new HashMap<Element, Double>(); + + public void set(final Element e, final double x, final double y) { + assert e != null : "Element was null"; + position.set(e, x, y); + elementTopPositionMap.put(e, Double.valueOf(y)); + } + + public double getTop(final Element e) { + Double top = elementTopPositionMap.get(e); + if (top == null) { + throw new IllegalArgumentException("Element " + e + + " was not found in the position bookkeeping"); + } + return top.doubleValue(); + } + + public void remove(Element e) { + elementTopPositionMap.remove(e); + } + } + // abs(atan(y/x))*(180/PI) = n deg, x = 1, solve y /** * The solution to @@ -4309,7 +4390,7 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker private final HorizontalScrollbarBundle horizontalScrollbar = new HorizontalScrollbarBundle(); private final HeaderRowContainer header = new HeaderRowContainer(headElem); - private final BodyRowContainer body = new BodyRowContainer(bodyElem); + private final BodyRowContainerImpl body = new BodyRowContainerImpl(bodyElem); private final FooterRowContainer footer = new FooterRowContainer(footElem); private final Scroller scroller = new Scroller(); @@ -4346,6 +4427,8 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker } }; + private final ElementPositionBookkeeper positions = new ElementPositionBookkeeper(); + /** * Creates a new Escalator widget instance. */ @@ -4517,7 +4600,7 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker int index = rowsToRemove - i - 1; TableRowElement tr = bodyElem.getRows().getItem(index); body.paintRemoveRow(tr, index); - body.removeRowPosition(tr); + positions.remove(tr); } body.visualRowOrder.clear(); body.setTopRowLogicalIndex(0); @@ -4595,7 +4678,7 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker * * @return the body. Never <code>null</code> */ - public RowContainer getBody() { + public BodyRowContainer getBody() { return body; } @@ -5190,4 +5273,10 @@ public class Escalator extends Widget implements RequiresResize, DeferredWorker columnConfiguration.getColumnWidth(i)); } } + + private Range getViewportPixels() { + int from = (int) Math.floor(verticalScrollbar.getScrollPos()); + int to = (int) Math.ceil(body.heightOfSection); + return Range.between(from, to); + } } diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeaturesTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeaturesTest.java index 92c7f3e6a6..b8bc644948 100644 --- a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeaturesTest.java +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeaturesTest.java @@ -19,6 +19,8 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import java.util.List; + import org.openqa.selenium.By; import org.openqa.selenium.Dimension; import org.openqa.selenium.JavascriptExecutor; @@ -64,6 +66,10 @@ public abstract class EscalatorBasicClientFeaturesTest extends MultiBrowserTest protected static final String COLUMN_SPANNING = "Column spanning"; protected static final String COLSPAN_NORMAL = "Apply normal colspan"; protected static final String COLSPAN_NONE = "Apply no colspan"; + protected static final String SPACERS = "Spacers"; + protected static final String ROW_1 = "Row 1"; + protected static final String SET_100PX = "Set 100px"; + protected static final String REMOVE = "Remove"; @Override protected Class<?> getUIClass() { @@ -259,4 +265,24 @@ public abstract class EscalatorBasicClientFeaturesTest extends MultiBrowserTest protected void populate() { selectMenuPath(GENERAL, POPULATE_COLUMN_ROW); } + + private List<WebElement> getSpacers() { + return getEscalator().findElements(By.className("v-escalator-spacer")); + } + + @SuppressWarnings("boxing") + protected WebElement getSpacer(int logicalRowIndex) { + List<WebElement> spacers = getSpacers(); + System.out.println("size: " + spacers.size()); + for (WebElement spacer : spacers) { + System.out.println(spacer + ", " + logicalRowIndex); + Boolean isInDom = (Boolean) executeScript( + "return arguments[0]['vLogicalRow'] === arguments[1]", + spacer, logicalRowIndex); + if (isInDom) { + return spacer; + } + } + return null; + } } diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java new file mode 100644 index 0000000000..24e474a767 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java @@ -0,0 +1,48 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.components.grid.basicfeatures.escalator; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.tests.components.grid.basicfeatures.EscalatorBasicClientFeaturesTest; + +public class EscalatorSpacerTest extends EscalatorBasicClientFeaturesTest { + + @Before + public void before() { + openTestURL(); + populate(); + } + + @Test + public void openVisibleSpacer() { + assertNull("No spacers should be shown at the start", getSpacer(1)); + selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX); + assertNotNull("Spacer should be shown after setting it", getSpacer(1)); + } + + @Test + public void closeVisibleSpacer() { + selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX); + selectMenuPath(FEATURES, SPACERS, ROW_1, REMOVE); + assertNull("Spacer should not exist after removing it", getSpacer(1)); + } + +} diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorBasicClientFeaturesWidget.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorBasicClientFeaturesWidget.java index 761f32bc9a..0905cde4eb 100644 --- a/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorBasicClientFeaturesWidget.java +++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorBasicClientFeaturesWidget.java @@ -303,6 +303,7 @@ public class EscalatorBasicClientFeaturesWidget extends createColumnsAndRowsMenu(); createFrozenMenu(); createColspanMenu(); + createSpacerMenu(); } private void createFrozenMenu() { @@ -612,6 +613,34 @@ public class EscalatorBasicClientFeaturesWidget extends }, menupath); } + private void createSpacerMenu() { + String[] menupath = { "Features", "Spacers" }; + createSpacersMenuForRow(1, menupath); + createSpacersMenuForRow(50, menupath); + } + + private void createSpacersMenuForRow(final int rowIndex, String[] menupath) { + menupath = new String[] { menupath[0], menupath[1], "Row " + rowIndex }; + addMenuCommand("Set 100px", new ScheduledCommand() { + @Override + public void execute() { + escalator.getBody().setSpacer(rowIndex, 100); + } + }, menupath); + addMenuCommand("Set 50px", new ScheduledCommand() { + @Override + public void execute() { + escalator.getBody().setSpacer(rowIndex, 50); + } + }, menupath); + addMenuCommand("Remove", new ScheduledCommand() { + @Override + public void execute() { + escalator.getBody().setSpacer(rowIndex, -1); + } + }, menupath); + } + private void insertRows(final RowContainer container, int offset, int number) { if (container == escalator.getBody()) { data.insertRows(offset, number); diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorProxy.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorProxy.java index 7f813b9d0f..63286af86f 100644 --- a/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorProxy.java +++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorProxy.java @@ -24,6 +24,7 @@ import com.vaadin.client.widget.escalator.Cell; import com.vaadin.client.widget.escalator.ColumnConfiguration; import com.vaadin.client.widget.escalator.EscalatorUpdater; import com.vaadin.client.widget.escalator.RowContainer; +import com.vaadin.client.widget.escalator.RowContainer.BodyRowContainer; import com.vaadin.client.widgets.Escalator; import com.vaadin.tests.widgetset.client.grid.EscalatorBasicClientFeaturesWidget.LogWidget; @@ -97,6 +98,22 @@ public class EscalatorProxy extends Escalator { } } + private class BodyRowContainerProxy extends RowContainerProxy implements + BodyRowContainer { + private BodyRowContainer rowContainer; + + public BodyRowContainerProxy(BodyRowContainer rowContainer) { + super(rowContainer); + this.rowContainer = rowContainer; + } + + @Override + public void setSpacer(int rowIndex, double height) + throws IllegalArgumentException { + rowContainer.setSpacer(rowIndex, height); + } + } + private class RowContainerProxy implements RowContainer { private final RowContainer rowContainer; @@ -176,7 +193,7 @@ public class EscalatorProxy extends Escalator { } private RowContainer headerProxy = null; - private RowContainer bodyProxy = null; + private BodyRowContainer bodyProxy = null; private RowContainer footerProxy = null; private ColumnConfiguration columnProxy = null; private LogWidget logWidget; @@ -198,9 +215,9 @@ public class EscalatorProxy extends Escalator { } @Override - public RowContainer getBody() { + public BodyRowContainer getBody() { if (bodyProxy == null) { - bodyProxy = new RowContainerProxy(super.getBody()); + bodyProxy = new BodyRowContainerProxy(super.getBody()); } return bodyProxy; } |