Browse Source

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
tags/7.5.0.alpha1
Henrik Paul 9 years ago
parent
commit
556c1aa06c

+ 13
- 0
WebContent/VAADIN/themes/base/escalator/escalator.scss View File

z-index: 1; 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%;
}
}
} }

+ 35
- 4
client/src/com/vaadin/client/widget/escalator/RowContainer.java View File



/** /**
* A representation of the rows in each of the sections (header, body and * 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 * @since 7.4
* @author Vaadin Ltd * @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 { 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 * An arbitrary pixel height of a row, before any autodetection for the row
* height has been made. * height has been made.

+ 270
- 181
client/src/com/vaadin/client/widgets/Escalator.java View File

import java.util.ListIterator; import java.util.ListIterator;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;


import com.vaadin.client.widget.escalator.PositionFunction.TranslatePosition; import com.vaadin.client.widget.escalator.PositionFunction.TranslatePosition;
import com.vaadin.client.widget.escalator.PositionFunction.WebkitTranslate3DPosition; import com.vaadin.client.widget.escalator.PositionFunction.WebkitTranslate3DPosition;
import com.vaadin.client.widget.escalator.RowContainer; 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.RowVisibilityChangeEvent;
import com.vaadin.client.widget.escalator.RowVisibilityChangeHandler; import com.vaadin.client.widget.escalator.RowVisibilityChangeHandler;
import com.vaadin.client.widget.escalator.ScrollbarBundle; import com.vaadin.client.widget.escalator.ScrollbarBundle;
|-- AbstractStaticRowContainer |-- AbstractStaticRowContainer
| |-- HeaderRowContainer | |-- HeaderRowContainer
| `-- FooterContainer | `-- FooterContainer
`---- BodyRowContainer
`---- BodyRowContainerImpl


AbstractRowContainer is intended to contain all common logic AbstractRowContainer is intended to contain all common logic
between RowContainers. It manages the bookkeeping of row between RowContainers. It manages the bookkeeping of row
are pretty thin special cases of a StaticRowContainer are pretty thin special cases of a StaticRowContainer
(mostly relating to positioning of the root element). (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 "AbstractScrollingRowContainer", but I felt that no more
inner classes were needed. So it contains both logic inner classes were needed. So it contains both logic
required for making things scroll about, and equivalent required for making things scroll about, and equivalent


Each RowContainer can be thought to have three levels of Each RowContainer can be thought to have three levels of
indices for any given displayed row (but the distinction 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 - Logical index
- Physical (or DOM) index - Physical (or DOM) index
(because of 0-based indices). In Header and (because of 0-based indices). In Header and
FooterRowContainers, you are safe to assume that the logical FooterRowContainers, you are safe to assume that the logical
index is the same as the physical index. But because the 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 VISUAL INDEX is the index relating to the order that you
see a row in, in the browser, as it is rendered. The see a row in, in the browser, as it is rendered. The
index is similar to the physical index in the sense that index is similar to the physical index in the sense that
Header and FooterRowContainers can assume a 1:1 Header and FooterRowContainers can assume a 1:1
relationship between visual index and logical index. And 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 body's visual index has additionally no apparent
relationship with its physical index. Because the <tr> tags relationship with its physical index. Because the <tr> tags
are reused in the body and visually repositioned with CSS are reused in the body and visually repositioned with CSS
as the user scrolls, the relationship between physical as the user scrolls, the relationship between physical
index and visual index is quickly broken. You can get an index and visual index is quickly broken. You can get an
element's visual index via the field element's visual index via the field
BodyRowContainer.visualRowOrder.
BodyRowContainerImpl.visualRowOrder.


Currently, the physical and visual indices are kept in sync Currently, the physical and visual indices are kept in sync
_most of the time_ by a deferred rearrangement of rows. _most of the time_ by a deferred rearrangement of rows.
They become desynced when scrolling. This is to help screen They become desynced when scrolling. This is to help screen
readers to read the contents from the DOM in a natural 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. about that.


*/ */
* that it _might_ perform better (rememeber to measure, implement, * that it _might_ perform better (rememeber to measure, implement,
* re-measure) * 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 * [[mpixscroll]]: This code will require alterations that are relevant for
* supporting the scrolling through more pixels than some browsers normally * supporting the scrolling through more pixels than some browsers normally
* escalator DOM). NOTE: these bits can most often also be identified by * escalator DOM). NOTE: these bits can most often also be identified by
* searching for code that call scrollElem.getScrollTop();. * 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 * A utility class that contains utility methods that are usually called


public void scrollToRow(final int rowIndex, public void scrollToRow(final int rowIndex,
final ScrollDestination destination, final double padding) { 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 targetStartPx = body.getDefaultRowHeight() * rowIndex;
final double targetEndPx = targetStartPx final double targetEndPx = targetStartPx
+ body.getDefaultRowHeight(); + body.getDefaultRowHeight();
*/ */
private String primaryStyleName = null; 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 boolean defaultRowHeightShouldBeAutodetected = true;


private double defaultRowHeight = INITIAL_DEFAULT_ROW_HEIGHT; private double defaultRowHeight = INITIAL_DEFAULT_ROW_HEIGHT;
*/ */
} }


@SuppressWarnings("boxing")
protected void setRowPosition(final TableRowElement tr, final int x, protected void setRowPosition(final TableRowElement tr, final int x,
final double y) { 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) { protected double getRowTop(final TableRowElement tr) {
return rowTopPositionMap.get(tr);
return positions.getTop(tr);
} }


protected void removeRowPosition(TableRowElement tr) { protected void removeRowPosition(TableRowElement tr) {
rowTopPositionMap.remove(tr);
positions.remove(tr);
} }


public void autodetectRowHeightLater() { public void autodetectRowHeightLater() {
return; 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()) { if (hasColumnAndRowData()) {
/*
* TODO [[rowheight]]: nudge rows down with
* refreshRowPositions() as needed
*/
for (int row = logicalRowRange.getStart(); row < logicalRowRange for (int row = logicalRowRange.getStart(); row < logicalRowRange
.getEnd(); row++) { .getEnd(); row++) {
final TableRowElement tr = getTrByVisualIndex(row); final TableRowElement tr = getTrByVisualIndex(row);
} }
} }


private class BodyRowContainer extends AbstractRowContainer {
private class BodyRowContainerImpl extends AbstractRowContainer implements
BodyRowContainer {
/* /*
* TODO [[optimize]]: check whether a native JsArray might be faster * TODO [[optimize]]: check whether a native JsArray might be faster
* than LinkedList * than LinkedList


private DeferredDomSorter domSorter = new DeferredDomSorter(); private DeferredDomSorter domSorter = new DeferredDomSorter();


public BodyRowContainer(final TableSectionElement bodyElement) {
private final SpacerContainer spacerContainer = new SpacerContainer();

public BodyRowContainerImpl(final TableSectionElement bodyElement) {
super(bodyElement); super(bodyElement);
} }


public void setStylePrimaryName(String primaryStyleName) { public void setStylePrimaryName(String primaryStyleName) {
super.setStylePrimaryName(primaryStyleName); super.setStylePrimaryName(primaryStyleName);
UIObject.setStylePrimaryName(root, primaryStyleName + "-body"); UIObject.setStylePrimaryName(root, primaryStyleName + "-body");
spacerContainer.setStylePrimaryName(primaryStyleName);
} }


public void updateEscalatorRowsOnScroll() { public void updateEscalatorRowsOnScroll() {
if (viewportOffset > 0) { if (viewportOffset > 0) {
// there's empty room on top // 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 int originalRowsToMove = (int) Math.ceil(viewportOffset
/ getDefaultRowHeight()); / getDefaultRowHeight());
int rowsToMove = Math.min(originalRowsToMove, int rowsToMove = Math.min(originalRowsToMove,
root.getChildCount());
visualRowOrder.size());


final int end = root.getChildCount();
final int end = visualRowOrder.size();
final int start = end - rowsToMove; 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()); final int logicalRowIndex = (int) (scrollTop / getDefaultRowHeight());
moveAndUpdateEscalatorRows(Range.between(start, end), 0, moveAndUpdateEscalatorRows(Range.between(start, end), 0,
logicalRowIndex); logicalRowIndex);
} }


else if (viewportOffset + getDefaultRowHeight() <= 0) { 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 * the viewport has been scrolled more than the topmost visual
* row. * row.
int originalRowsToMove = (int) Math.abs(viewportOffset int originalRowsToMove = (int) Math.abs(viewportOffset
/ getDefaultRowHeight()); / getDefaultRowHeight());
int rowsToMove = Math.min(originalRowsToMove, int rowsToMove = Math.min(originalRowsToMove,
root.getChildCount());
visualRowOrder.size());


int logicalRowIndex; int logicalRowIndex;
if (rowsToMove < root.getChildCount()) {
if (rowsToMove < visualRowOrder.size()) {
/* /*
* We scroll so little that we can just keep adding the rows * We scroll so little that we can just keep adding the rows
* below the current escalator * below the current escalator
logicalRowIndex = getLogicalRowIndex(visualRowOrder logicalRowIndex = getLogicalRowIndex(visualRowOrder
.getLast()) + 1; .getLast()) + 1;
} else { } 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 * Since we're moving all escalator rows, we need to
* calculate the first logical row index from the scroll * calculate the first logical row index from the scroll
* moveAndUpdateEscalatorRows works, this will work out even if * moveAndUpdateEscalatorRows works, this will work out even if
* we move all the rows, and try to place them "at the end". * 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 // make sure that we don't move rows over the data boundary
boolean aRowWasLeftBehind = false; boolean aRowWasLeftBehind = false;
if (logicalRowIndex + rowsToMove > getRowCount()) { 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 * always exactly one row that will be moved beyond the data
* source, when viewport is scrolled to the end. This, * source, when viewport is scrolled to the end. This,
* however, isn't guaranteed anymore once row heights start * however, isn't guaranteed anymore once row heights start
*/ */
scroller.recalculateScrollbarsForVirtualViewport(); scroller.recalculateScrollbarsForVirtualViewport();


/*
* FIXME [[rowheight]]: coded to work only with default row heights
* - will not work with variable row heights
*/
final boolean addedRowsAboveCurrentViewport = index final boolean addedRowsAboveCurrentViewport = index
* getDefaultRowHeight() < getScrollTop(); * getDefaultRowHeight() < getScrollTop();
final boolean addedRowsBelowCurrentViewport = index final boolean addedRowsBelowCurrentViewport = index
* without re-evaluating any rows. * 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(); final double yDelta = numberOfRows * getDefaultRowHeight();
adjustScrollPosIgnoreEvents(yDelta); adjustScrollPosIgnoreEvents(yDelta);
updateTopRowLogicalIndex(numberOfRows); updateTopRowLogicalIndex(numberOfRows);
moveAndUpdateEscalatorRows(Range.between(start, end), moveAndUpdateEscalatorRows(Range.between(start, end),
visualTargetIndex, unupdatedLogicalStart); 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. // move the surrounding rows to their correct places.
double rowTop = (unupdatedLogicalStart + (end - start)) double rowTop = (unupdatedLogicalStart + (end - start))
* getDefaultRowHeight(); * getDefaultRowHeight();
while (i.hasNext()) { while (i.hasNext()) {
final TableRowElement tr = i.next(); final TableRowElement tr = i.next();
setRowPosition(tr, 0, rowTop); setRowPosition(tr, 0, rowTop);
/*
* FIXME [[rowheight]]: coded to work only with default row
* heights - will not work with variable row heights
*/
rowTop += getDefaultRowHeight(); rowTop += getDefaultRowHeight();
} }


} }


{ // Reposition the rows that were moved { // 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(); double newRowTop = logicalTargetIndex * getDefaultRowHeight();


final ListIterator<TableRowElement> iter = visualRowOrder final ListIterator<TableRowElement> iter = visualRowOrder
for (int i = 0; i < visualSourceRange.length(); i++) { for (int i = 0; i < visualSourceRange.length(); i++) {
final TableRowElement tr = iter.next(); final TableRowElement tr = iter.next();
setRowPosition(tr, 0, newRowTop); setRowPosition(tr, 0, newRowTop);
/*
* FIXME [[rowheight]]: coded to work only with default row
* heights - will not work with variable row heights
*/
newRowTop += getDefaultRowHeight(); newRowTop += getDefaultRowHeight();
} }
} }


verticalScrollbar.setScrollPosByDelta(yDelta); 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()); final double rowTopPos = yDelta - (yDelta % getDefaultRowHeight());
for (final TableRowElement tr : visualRowOrder) { for (final TableRowElement tr : visualRowOrder) {
setRowPosition(tr, 0, getRowTop(tr) + rowTopPos); setRowPosition(tr, 0, getRowTop(tr) + rowTopPos);
* added. * added.
*/ */
for (int i = 0; i < addedRows.size(); i++) { 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) setRowPosition(addedRows.get(i), 0, (index + i)
* getDefaultRowHeight()); * getDefaultRowHeight());
} }
for (int i = index + addedRows.size(); i < visualRowOrder for (int i = index + addedRows.size(); i < visualRowOrder
.size(); i++) { .size(); i++) {
final TableRowElement tr = visualRowOrder.get(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()); setRowPosition(tr, 0, i * getDefaultRowHeight());
} }


} }


private int getMaxEscalatorRowCapacity() { 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 final int maxEscalatorRowCapacity = (int) Math
.ceil(calculateHeight() / getDefaultRowHeight()) + 1; .ceil(calculateHeight() / getDefaultRowHeight()) + 1;


.isEmpty() && removedVisualInside.getStart() == 0; .isEmpty() && removedVisualInside.getStart() == 0;


if (!removedAbove.isEmpty() || firstVisualRowIsRemoved) { 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() final double yDelta = removedAbove.length()
* getDefaultRowHeight(); * getDefaultRowHeight();
final double firstLogicalRowHeight = getDefaultRowHeight(); final double firstLogicalRowHeight = getDefaultRowHeight();
final int dirtyRowsStart = removedLogicalInside.getStart(); final int dirtyRowsStart = removedLogicalInside.getStart();
for (int i = dirtyRowsStart; i < escalatorRowCount; i++) { for (int i = dirtyRowsStart; i < escalatorRowCount; i++) {
final TableRowElement tr = visualRowOrder.get(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()); setRowPosition(tr, 0, i * getDefaultRowHeight());
} }


* double-refreshing. * double-refreshing.
*/ */


/*
* FIXME [[rowheight]]: coded to work only with default row
* heights - will not work with variable row heights
*/
final double contentBottom = getRowCount() final double contentBottom = getRowCount()
* getDefaultRowHeight(); * getDefaultRowHeight();
final double viewportBottom = tBodyScrollTop final double viewportBottom = tBodyScrollTop
*/ */


/* /*
* 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 * work with default row heights - will not work with
* variable row heights * variable row heights
*/ */
for (int i = removedVisualInside.getStart(); i < escalatorRowCount; i++) { for (int i = removedVisualInside.getStart(); i < escalatorRowCount; i++) {
final TableRowElement tr = visualRowOrder.get(i); final TableRowElement tr = visualRowOrder.get(i);
setRowPosition(tr, 0, (int) newTop); setRowPosition(tr, 0, (int) newTop);

/*
* FIXME [[rowheight]]: coded to work only with
* default row heights - will not work with variable
* row heights
*/
newTop += getDefaultRowHeight(); newTop += getDefaultRowHeight();
} }


* 5 * 5
*/ */


/*
* FIXME [[rowheight]]: coded to work only with default
* row heights - will not work with variable row heights
*/
final int rowsScrolled = (int) (Math final int rowsScrolled = (int) (Math
.ceil((viewportBottom - contentBottom) .ceil((viewportBottom - contentBottom)
/ getDefaultRowHeight())); / getDefaultRowHeight()));
final ListIterator<TableRowElement> iterator = visualRowOrder final ListIterator<TableRowElement> iterator = visualRowOrder
.listIterator(removedVisualInside.getStart()); .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) double rowTop = (removedLogicalInside.getStart() + logicalOffset)
* getDefaultRowHeight(); * getDefaultRowHeight();
for (int i = removedVisualInside.getStart(); i < escalatorRowCount for (int i = removedVisualInside.getStart(); i < escalatorRowCount
- removedVisualInside.length(); i++) { - removedVisualInside.length(); i++) {
final TableRowElement tr = iterator.next(); final TableRowElement tr = iterator.next();
setRowPosition(tr, 0, rowTop); setRowPosition(tr, 0, rowTop);
/*
* FIXME [[rowheight]]: coded to work only with default row
* heights - will not work with variable row heights
*/
rowTop += getDefaultRowHeight(); rowTop += getDefaultRowHeight();
} }
} }
// move the surrounding rows to their correct places. // move the surrounding rows to their correct places.
final ListIterator<TableRowElement> iterator = visualRowOrder final ListIterator<TableRowElement> iterator = visualRowOrder
.listIterator(removedVisualInside.getEnd()); .listIterator(removedVisualInside.getEnd());
/*
* FIXME [[rowheight]]: coded to work only with default row heights
* - will not work with variable row heights
*/
double rowTop = removedLogicalInside.getStart() double rowTop = removedLogicalInside.getStart()
* getDefaultRowHeight(); * getDefaultRowHeight();
while (iterator.hasNext()) { while (iterator.hasNext()) {
final TableRowElement tr = iterator.next(); final TableRowElement tr = iterator.next();
setRowPosition(tr, 0, rowTop); setRowPosition(tr, 0, rowTop);
/*
* FIXME [[rowheight]]: coded to work only with default row
* heights - will not work with variable row heights
*/
rowTop += getDefaultRowHeight(); rowTop += getDefaultRowHeight();
} }
} }
} }


/* /*
* 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 maxEscalatorRows = getMaxEscalatorRowCapacity();
final int currentTopRowIndex = getLogicalRowIndex(visualRowOrder final int currentTopRowIndex = getLogicalRowIndex(visualRowOrder
if (!visualRowOrder.isEmpty()) { if (!visualRowOrder.isEmpty()) {
final double firstRowTop = getRowTop(visualRowOrder final double firstRowTop = getRowTop(visualRowOrder
.getFirst()); .getFirst());
/*
* FIXME [[rowheight]]: coded to work only with default row
* heights - will not work with variable row heights
*/
final double firstRowMinTop = tBodyScrollTop final double firstRowMinTop = tBodyScrollTop
- getDefaultRowHeight(); - getDefaultRowHeight();
if (firstRowTop < firstRowMinTop) { if (firstRowTop < firstRowMinTop) {
return; 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"); Profiler.enter("Escalator.BodyRowContainer.reapplyDefaultRowHeights");


/* step 1: resize and reposition rows */ /* step 1: resize and reposition rows */
/* step 3: make sure we have the correct amount of escalator rows. */ /* step 3: make sure we have the correct amount of escalator rows. */
verifyEscalatorCount(); verifyEscalatorCount();


/*
* TODO [[rowheight]] This simply doesn't work with variable rows
* heights.
*/
int logicalLogical = (int) (getRowTop(visualRowOrder.getFirst()) / getDefaultRowHeight()); int logicalLogical = (int) (getRowTop(visualRowOrder.getFirst()) / getDefaultRowHeight());
setTopRowLogicalIndex(logicalLogical); setTopRowLogicalIndex(logicalLogical);


return new Cell(getLogicalRowIndex(rowElement), cell.getColumn(), return new Cell(getLogicalRowIndex(rowElement), cell.getColumn(),
cell.getElement()); cell.getElement());
} }

@Override
public void setSpacer(int rowIndex, double height)
throws IllegalArgumentException {
spacerContainer.setSpacer(rowIndex, height);
}
} }


private class ColumnConfigurationImpl implements ColumnConfiguration { private class ColumnConfigurationImpl implements ColumnConfiguration {
} }
} }


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 // abs(atan(y/x))*(180/PI) = n deg, x = 1, solve y
/** /**
* The solution to * The solution to
private final HorizontalScrollbarBundle horizontalScrollbar = new HorizontalScrollbarBundle(); private final HorizontalScrollbarBundle horizontalScrollbar = new HorizontalScrollbarBundle();


private final HeaderRowContainer header = new HeaderRowContainer(headElem); 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 FooterRowContainer footer = new FooterRowContainer(footElem);


private final Scroller scroller = new Scroller(); private final Scroller scroller = new Scroller();
} }
}; };


private final ElementPositionBookkeeper positions = new ElementPositionBookkeeper();

/** /**
* Creates a new Escalator widget instance. * Creates a new Escalator widget instance.
*/ */
int index = rowsToRemove - i - 1; int index = rowsToRemove - i - 1;
TableRowElement tr = bodyElem.getRows().getItem(index); TableRowElement tr = bodyElem.getRows().getItem(index);
body.paintRemoveRow(tr, index); body.paintRemoveRow(tr, index);
body.removeRowPosition(tr);
positions.remove(tr);
} }
body.visualRowOrder.clear(); body.visualRowOrder.clear();
body.setTopRowLogicalIndex(0); body.setTopRowLogicalIndex(0);
* *
* @return the body. Never <code>null</code> * @return the body. Never <code>null</code>
*/ */
public RowContainer getBody() {
public BodyRowContainer getBody() {
return body; return body;
} }


columnConfiguration.getColumnWidth(i)); 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);
}
} }

+ 26
- 0
uitest/src/com/vaadin/tests/components/grid/basicfeatures/EscalatorBasicClientFeaturesTest.java View File

import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;


import java.util.List;

import org.openqa.selenium.By; import org.openqa.selenium.By;
import org.openqa.selenium.Dimension; import org.openqa.selenium.Dimension;
import org.openqa.selenium.JavascriptExecutor; import org.openqa.selenium.JavascriptExecutor;
protected static final String COLUMN_SPANNING = "Column spanning"; protected static final String COLUMN_SPANNING = "Column spanning";
protected static final String COLSPAN_NORMAL = "Apply normal colspan"; protected static final String COLSPAN_NORMAL = "Apply normal colspan";
protected static final String COLSPAN_NONE = "Apply no 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 @Override
protected Class<?> getUIClass() { protected Class<?> getUIClass() {
protected void populate() { protected void populate() {
selectMenuPath(GENERAL, POPULATE_COLUMN_ROW); 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;
}
} }

+ 48
- 0
uitest/src/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java View File

/*
* 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));
}

}

+ 29
- 0
uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorBasicClientFeaturesWidget.java View File

createColumnsAndRowsMenu(); createColumnsAndRowsMenu();
createFrozenMenu(); createFrozenMenu();
createColspanMenu(); createColspanMenu();
createSpacerMenu();
} }


private void createFrozenMenu() { private void createFrozenMenu() {
}, menupath); }, 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) { private void insertRows(final RowContainer container, int offset, int number) {
if (container == escalator.getBody()) { if (container == escalator.getBody()) {
data.insertRows(offset, number); data.insertRows(offset, number);

+ 20
- 3
uitest/src/com/vaadin/tests/widgetset/client/grid/EscalatorProxy.java View File

import com.vaadin.client.widget.escalator.ColumnConfiguration; import com.vaadin.client.widget.escalator.ColumnConfiguration;
import com.vaadin.client.widget.escalator.EscalatorUpdater; import com.vaadin.client.widget.escalator.EscalatorUpdater;
import com.vaadin.client.widget.escalator.RowContainer; import com.vaadin.client.widget.escalator.RowContainer;
import com.vaadin.client.widget.escalator.RowContainer.BodyRowContainer;
import com.vaadin.client.widgets.Escalator; import com.vaadin.client.widgets.Escalator;
import com.vaadin.tests.widgetset.client.grid.EscalatorBasicClientFeaturesWidget.LogWidget; import com.vaadin.tests.widgetset.client.grid.EscalatorBasicClientFeaturesWidget.LogWidget;


} }
} }


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 class RowContainerProxy implements RowContainer {
private final RowContainer rowContainer; private final RowContainer rowContainer;


} }


private RowContainer headerProxy = null; private RowContainer headerProxy = null;
private RowContainer bodyProxy = null;
private BodyRowContainer bodyProxy = null;
private RowContainer footerProxy = null; private RowContainer footerProxy = null;
private ColumnConfiguration columnProxy = null; private ColumnConfiguration columnProxy = null;
private LogWidget logWidget; private LogWidget logWidget;
} }


@Override @Override
public RowContainer getBody() {
public BodyRowContainer getBody() {
if (bodyProxy == null) { if (bodyProxy == null) {
bodyProxy = new RowContainerProxy(super.getBody());
bodyProxy = new BodyRowContainerProxy(super.getBody());
} }
return bodyProxy; return bodyProxy;
} }

Loading…
Cancel
Save