/* * Copyright 2000-2016 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.widgets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; 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.Map.Entry; import java.util.TreeMap; import java.util.logging.Level; import java.util.logging.Logger; import com.google.gwt.animation.client.Animation; import com.google.gwt.animation.client.AnimationScheduler; import com.google.gwt.animation.client.AnimationScheduler.AnimationCallback; import com.google.gwt.animation.client.AnimationScheduler.AnimationHandle; import com.google.gwt.core.client.Duration; import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.core.client.JsArray; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.dom.client.DivElement; import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.NativeEvent; import com.google.gwt.dom.client.Node; import com.google.gwt.dom.client.NodeList; import com.google.gwt.dom.client.Style; import com.google.gwt.dom.client.Style.Display; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.dom.client.TableCellElement; import com.google.gwt.dom.client.TableRowElement; import com.google.gwt.dom.client.TableSectionElement; import com.google.gwt.dom.client.Touch; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.logging.client.LogConfiguration; import com.google.gwt.user.client.Command; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.RequiresResize; import com.google.gwt.user.client.ui.RootPanel; import com.google.gwt.user.client.ui.UIObject; import com.google.gwt.user.client.ui.Widget; import com.vaadin.client.BrowserInfo; import com.vaadin.client.DeferredWorker; import com.vaadin.client.Profiler; import com.vaadin.client.WidgetUtil; import com.vaadin.client.ui.SubPartAware; 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.FlyweightCell; import com.vaadin.client.widget.escalator.FlyweightRow; import com.vaadin.client.widget.escalator.PositionFunction; 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.Row; 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; import com.vaadin.client.widget.escalator.ScrollbarBundle.HorizontalScrollbarBundle; import com.vaadin.client.widget.escalator.ScrollbarBundle.VerticalScrollbarBundle; import com.vaadin.client.widget.escalator.Spacer; import com.vaadin.client.widget.escalator.SpacerUpdater; import com.vaadin.client.widget.escalator.events.RowHeightChangedEvent; import com.vaadin.client.widget.grid.events.ScrollEvent; import com.vaadin.client.widget.grid.events.ScrollHandler; import com.vaadin.client.widgets.Escalator.JsniUtil.TouchHandlerBundle; import com.vaadin.shared.Range; import com.vaadin.shared.ui.grid.HeightMode; import com.vaadin.shared.ui.grid.ScrollDestination; import com.vaadin.shared.util.SharedUtil; /*- Maintenance Notes! Reading these might save your day. (note for editors: line width is 80 chars, including the one-space indentation) == Row Container Structure AbstractRowContainer |-- AbstractStaticRowContainer | |-- HeaderRowContainer | `-- FooterContainer `---- BodyRowContainerImpl 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). 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 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 BodyRowContainerImpl, 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
* GWT is unable to handle some method calls to Java methods in inner-classes * from within JSNI blocks. Having that inner class extend a non-inner-class (or * implement such an interface), makes it possible for JSNI to indirectly refer * to the inner class, by invoking methods and fields in the non-inner-class * API. * * @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() * @see Escalator.Scroller#onScroll() */ 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() * @see Escalator.Scroller#onScroll() */ protected final JavaScriptObject mousewheelListenerFunction; /** * A JavaScript function that handles the touch start DOM event, and passes * it on to Java code. * * @see TouchHandlerBundle#touchStart(Escalator.JsniUtil.TouchHandlerBundle.CustomTouchEvent) */ protected JavaScriptObject touchStartFunction; /** * A JavaScript function that handles the touch move DOM event, and passes * it on to Java code. * * @see TouchHandlerBundle#touchMove(Escalator.JsniUtil.TouchHandlerBundle.CustomTouchEvent) */ protected JavaScriptObject touchMoveFunction; /** * A JavaScript function that handles the touch end and cancel DOM events, * and passes them on to Java code. * * @see TouchHandlerBundle#touchEnd(Escalator.JsniUtil.TouchHandlerBundle.CustomTouchEvent) */ protected JavaScriptObject touchEndFunction; protected TouchHandlerBundle touchHandlerBundle; protected JsniWorkaround(final Escalator escalator) { scrollListenerFunction = createScrollListenerFunction(escalator); mousewheelListenerFunction = createMousewheelListenerFunction( escalator); touchHandlerBundle = new TouchHandlerBundle(escalator); touchStartFunction = touchHandlerBundle.getTouchStartHandler(); touchMoveFunction = touchHandlerBundle.getTouchMoveHandler(); touchEndFunction = touchHandlerBundle.getTouchEndHandler(); } /** * 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() */ 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() */ protected abstract JavaScriptObject createMousewheelListenerFunction( Escalator esc); } /** * A low-level table-like widget that features a scrolling virtual viewport and * lazily generated rows. * * @since 7.4 * @author Vaadin Ltd */ public class Escalator extends Widget implements RequiresResize, DeferredWorker, SubPartAware { // todo comments legend /* * [[optimize]]: There's an opportunity to rewrite the code in such a way * that it _might_ perform better (rememeber to measure, implement, * re-measure) */ /* * [[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 * from JSNI. *
* The methods are moved in this class to minimize the amount of JSNI code * as much as feasible. */ static class JsniUtil { public static class TouchHandlerBundle { public static final String POINTER_EVENT_TYPE_TOUCH = "touch"; /** * A JavaScriptObject overlay for the * JavaScript * TouchEvent object. *
* This needs to be used in the touch event handlers, since GWT's
* {@link com.google.gwt.event.dom.client.TouchEvent TouchEvent}
* can't be cast from the JSNI call, and the
* {@link com.google.gwt.dom.client.NativeEvent NativeEvent} isn't
* properly populated with the correct values.
*/
private final static class CustomTouchEvent
extends JavaScriptObject {
protected CustomTouchEvent() {
}
public native NativeEvent getNativeEvent()
/*-{
return this;
}-*/;
public native int getPageX()
/*-{
return this.targetTouches[0].pageX;
}-*/;
public native int getPageY()
/*-{
return this.targetTouches[0].pageY;
}-*/;
public native String getPointerType()
/*-{
return this.pointerType;
}-*/;
}
private final Escalator escalator;
public TouchHandlerBundle(final Escalator escalator) {
this.escalator = escalator;
}
public native JavaScriptObject getTouchStartHandler()
/*-{
// we need to store "this", since it won't be preserved on call.
var self = this;
return $entry(function (e) {
self.@com.vaadin.client.widgets.Escalator.JsniUtil.TouchHandlerBundle::touchStart(*)(e);
});
}-*/;
public native JavaScriptObject getTouchMoveHandler()
/*-{
// we need to store "this", since it won't be preserved on call.
var self = this;
return $entry(function (e) {
self.@com.vaadin.client.widgets.Escalator.JsniUtil.TouchHandlerBundle::touchMove(*)(e);
});
}-*/;
public native JavaScriptObject getTouchEndHandler()
/*-{
// we need to store "this", since it won't be preserved on call.
var self = this;
return $entry(function (e) {
self.@com.vaadin.client.widgets.Escalator.JsniUtil.TouchHandlerBundle::touchEnd(*)(e);
});
}-*/;
// Duration of the inertial scrolling simulation. Devices with
// larger screens take longer durations.
private static final int DURATION = Window.getClientHeight();
// multiply scroll velocity with repeated touching
private int acceleration = 1;
private boolean touching = false;
// Two movement objects for storing status and processing touches
private Movement yMov, xMov;
final double MIN_VEL = 0.6, MAX_VEL = 4, F_VEL = 1500, F_ACC = 0.7,
F_AXIS = 1;
// The object to deal with one direction scrolling
private class Movement {
final List
* Usually {@code "th"} or {@code "td"}.
*
* Note: To actually create such an element, use
* {@link #createCellElement(int, int)} instead.
*
* @return the tag name for the element to represent cells as
* @see #createCellElement(int, int)
*/
protected abstract String getCellElementTagName();
@Override
public EscalatorUpdater getEscalatorUpdater() {
return updater;
}
/**
* {@inheritDoc}
*
* Implementation detail: This method does no DOM modifications
* (i.e. is very cheap to call) if there is no data for rows or columns
* when this method is called.
*
* @see #hasColumnAndRowData()
*/
@Override
public void setEscalatorUpdater(
final EscalatorUpdater escalatorUpdater) {
if (escalatorUpdater == null) {
throw new IllegalArgumentException(
"escalator updater cannot be null");
}
updater = escalatorUpdater;
if (hasColumnAndRowData() && getRowCount() > 0) {
refreshRows(0, getRowCount());
}
}
/**
* {@inheritDoc}
*
* Implementation detail: This method does no DOM modifications
* (i.e. is very cheap to call) if there are no rows in the DOM when
* this method is called.
*
* @see #hasSomethingInDom()
*/
@Override
public void removeRows(final int index, final int numberOfRows) {
assertArgumentsAreValidAndWithinRange(index, numberOfRows);
rows -= numberOfRows;
if (heightMode == HeightMode.UNDEFINED) {
heightByRows = rows;
}
if (!isAttached()) {
return;
}
if (hasSomethingInDom()) {
paintRemoveRows(index, numberOfRows);
}
}
/**
* Removes those row elements from the DOM that correspond to the given
* range of logical indices. This may be fewer than {@code numberOfRows}
* , even zero, if not all the removed rows are actually visible.
*
* The implementation must call {@link #paintRemoveRow(Element, int)}
* for each row that is removed from the DOM.
*
* @param index
* the logical index of the first removed row
* @param numberOfRows
* number of logical rows to remove
*/
protected abstract void paintRemoveRows(final int index,
final int numberOfRows);
/**
* Removes a row element from the DOM, invoking
* {@link #getEscalatorUpdater()}
* {@link EscalatorUpdater#preDetach(Row, Iterable) preDetach} and
* {@link EscalatorUpdater#postDetach(Row, Iterable) postDetach} before
* and after removing the row, respectively.
*
* This method must be called for each removed DOM row by any
* {@link #paintRemoveRows(int, int)} implementation.
*
* @param tr
* the row element to remove.
*/
protected void paintRemoveRow(final TableRowElement tr,
final int logicalRowIndex) {
flyweightRow.setup(tr, logicalRowIndex,
columnConfiguration.getCalculatedColumnWidths());
getEscalatorUpdater().preDetach(flyweightRow,
flyweightRow.getCells());
tr.removeFromParent();
getEscalatorUpdater().postDetach(flyweightRow,
flyweightRow.getCells());
/*
* the "assert" guarantees that this code is run only during
* development/debugging.
*/
assert flyweightRow.teardown();
}
protected void assertArgumentsAreValidAndWithinRange(final int index,
final int numberOfRows)
throws IllegalArgumentException, IndexOutOfBoundsException {
if (numberOfRows < 1) {
throw new IllegalArgumentException(
"Number of rows must be 1 or greater (was "
+ numberOfRows + ")");
}
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;
}
/**
* This method calculates the current row count directly from the DOM.
*
* While Escalator is stable, this value should equal to
* {@link #getRowCount()}, but while row counts are being updated, these
* two values might differ for a short while.
*
* Any extra content, such as spacers for the body, should not be
* included in this count.
*
* @since 7.5.0
*
* @return the actual DOM count of rows
*/
public abstract int getDomRowCount();
/**
* {@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;
if (heightMode == HeightMode.UNDEFINED) {
heightByRows = rows;
}
/*
* 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);
if (rows == numberOfRows) {
/*
* We are inserting the first rows in this container. We
* potentially need to set the widths for the cells for the
* first time.
*/
Map
* 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
// overridden because of JavaDoc
public void refreshRows(final int index, final int numberOfRows) {
Range rowRange = Range.withLength(index, numberOfRows);
Range colRange = Range.withLength(0,
getColumnConfiguration().getColumnCount());
refreshCells(rowRange, colRange);
}
protected abstract void refreshCells(Range logicalRowRange,
Range colRange);
void refreshRow(TableRowElement tr, int logicalRowIndex) {
refreshRow(tr, logicalRowIndex, Range.withLength(0,
getColumnConfiguration().getColumnCount()));
}
void refreshRow(final TableRowElement tr, final int logicalRowIndex,
Range colRange) {
flyweightRow.setup(tr, logicalRowIndex,
columnConfiguration.getCalculatedColumnWidths());
Iterable
* Precondition: The row must be already attached to the DOM and the
* FlyweightCell instances corresponding to the new columns added to
* {@code flyweightRow}.
*
* @param tr
* the row in which to insert the cells
* @param logicalRowIndex
* the index of the row
* @param offset
* the index of the first cell
* @param numberOfCells
* the number of cells to insert
*/
private void paintInsertCells(final TableRowElement tr,
int logicalRowIndex, final int offset,
final int numberOfCells) {
assert root.isOrHasChild(
tr) : "The row must be attached to the document";
flyweightRow.setup(tr, logicalRowIndex,
columnConfiguration.getCalculatedColumnWidths());
Iterable
* In practice, this applies for all header and footer rows. For body
* rows, it applies for all rows except spacer rows.
*
* @since 7.5.0
*
* @param tr
* the row element to check for if it is or has elements that
* can be frozen
* @return
* Note: In contrast to {@link #reapplyColumnWidths()}, this
* method only modifies the width of the {@code
*
* Make sure that the displayed rows with a default height are updated
* in height and top position.
*
* Note:This implementation should not call
* {@link Escalator#recalculateElementSizes()} - it is done by the
* discretion of the caller of this method.
*/
protected abstract void reapplyDefaultRowHeights();
protected void reapplyRowHeight(final TableRowElement tr,
final double heightPx) {
assert heightPx >= 0 : "Height must not be negative";
Element cellElem = tr.getFirstChildElement();
while (cellElem != null) {
cellElem.getStyle().setHeight(heightPx, Unit.PX);
cellElem = cellElem.getNextSiblingElement();
}
/*
* no need to apply height to tr-element, it'll be resized
* implicitly.
*/
}
protected void setRowPosition(final TableRowElement tr, final int x,
final double y) {
positions.set(tr, x, y);
}
/**
* Returns the assigned top position for the given element.
*
* Note: This method does not calculate what a row's top
* position should be. It just returns an assigned value, correct or
* not.
*
* @param tr
* the table row element to measure
* @return the current top position for {@code tr}
* @see BodyRowContainerImpl#getRowTop(int)
*/
protected double getRowTop(final TableRowElement tr) {
return positions.getTop(tr);
}
protected void removeRowPosition(TableRowElement tr) {
positions.remove(tr);
}
public void autodetectRowHeightLater() {
Scheduler.get().scheduleFinally(new Scheduler.ScheduledCommand() {
@Override
public void execute() {
if (defaultRowHeightShouldBeAutodetected && isAttached()) {
autodetectRowHeightNow();
defaultRowHeightShouldBeAutodetected = false;
}
}
});
}
private void fireRowHeightChangedEventFinally() {
if (!rowHeightChangedEventFired) {
rowHeightChangedEventFired = true;
Scheduler.get().scheduleFinally(new ScheduledCommand() {
@Override
public void execute() {
fireEvent(new RowHeightChangedEvent());
rowHeightChangedEventFired = false;
}
});
}
}
public void autodetectRowHeightNow() {
if (!isAttached()) {
// Run again when attached
defaultRowHeightShouldBeAutodetected = true;
return;
}
final double oldRowHeight = defaultRowHeight;
final Element detectionTr = DOM.createTR();
detectionTr.setClassName(getStylePrimaryName() + "-row");
final Element cellElem = DOM.createElement(getCellElementTagName());
cellElem.setClassName(getStylePrimaryName() + "-cell");
cellElem.setInnerText("Ij");
detectionTr.appendChild(cellElem);
root.appendChild(detectionTr);
double boundingHeight = WidgetUtil
.getRequiredHeightBoundingClientRectDouble(cellElem);
defaultRowHeight = Math.max(1.0d, boundingHeight);
root.removeChild(detectionTr);
if (root.hasChildNodes()) {
reapplyDefaultRowHeights();
applyHeightByRows();
}
if (oldRowHeight != defaultRowHeight) {
fireRowHeightChangedEventFinally();
}
}
@Override
public Cell getCell(final Element element) {
if (element == null) {
throw new IllegalArgumentException("Element cannot be null");
}
/*
* Ensure that element is not root nor the direct descendant of root
* (a row) and ensure the element is inside the dom hierarchy of the
* root element. If not, return.
*/
if (root == element || element.getParentElement() == root
|| !root.isOrHasChild(element)) {
return null;
}
/*
* Ensure element is the cell element by iterating up the DOM
* hierarchy until reaching cell element.
*/
Element cellElementCandidate = element;
while (cellElementCandidate.getParentElement()
.getParentElement() != root) {
cellElementCandidate = cellElementCandidate.getParentElement();
}
final TableCellElement cellElement = TableCellElement
.as(cellElementCandidate);
// Find dom column
int domColumnIndex = -1;
for (Element e = cellElement; e != null; e = e
.getPreviousSiblingElement()) {
domColumnIndex++;
}
// Find dom row
int domRowIndex = -1;
for (Element e = cellElement.getParentElement(); e != null; e = e
.getPreviousSiblingElement()) {
domRowIndex++;
}
return new Cell(domRowIndex, domColumnIndex, cellElement);
}
double measureCellWidth(TableCellElement cell, boolean withContent) {
/*
* To get the actual width of the contents, we need to get the cell
* content without any hardcoded height or width.
*
* But we don't want to modify the existing column, because that
* might trigger some unnecessary listeners and whatnot. So,
* instead, we make a deep clone of that cell, but without any
* explicit dimensions, and measure that instead.
*/
TableCellElement cellClone = TableCellElement
.as((Element) cell.cloneNode(withContent));
cellClone.getStyle().clearHeight();
cellClone.getStyle().clearWidth();
cell.getParentElement().insertBefore(cellClone, cell);
double requiredWidth = WidgetUtil
.getRequiredWidthBoundingClientRectDouble(cellClone);
if (BrowserInfo.get().isIE()) {
/*
* IE browsers have some issues with subpixels. Occasionally
* content is overflown even if not necessary. Increase the
* counted required size by 0.01 just to be on the safe side.
*/
requiredWidth += 0.01;
}
cellClone.removeFromParent();
return requiredWidth;
}
/**
* Gets the minimum width needed to display the cell properly.
*
* @param colIndex
* index of column to measure
* @param withContent
*
* Note that {@link Escalator#getBody() the body} will calculate its
* height, while the others will return a precomputed value.
*
* @since 7.5.0
*
* @return the height of this table section
*/
protected abstract double getHeightOfSection();
protected int getLogicalRowIndex(final TableRowElement tr) {
return tr.getSectionRowIndex();
};
}
private abstract class AbstractStaticRowContainer
extends AbstractRowContainer {
/** The height of the combined rows in the DOM. Never negative. */
private double heightOfSection = 0;
public AbstractStaticRowContainer(
final TableSectionElement headElement) {
super(headElement);
}
@Override
public int getDomRowCount() {
return root.getChildCount();
}
@Override
protected void paintRemoveRows(final int index,
final int numberOfRows) {
for (int i = index; i < index + numberOfRows; i++) {
final TableRowElement tr = root.getRows().getItem(index);
paintRemoveRow(tr, index);
}
recalculateSectionHeight();
}
@Override
protected TableRowElement getTrByVisualIndex(final int index)
throws IndexOutOfBoundsException {
if (index >= 0 && index < root.getChildCount()) {
return root.getRows().getItem(index);
} else {
throw new IndexOutOfBoundsException(
"No such visual index: " + index);
}
}
@Override
public void insertRows(int index, int numberOfRows) {
super.insertRows(index, numberOfRows);
recalculateElementSizes();
applyHeightByRows();
}
@Override
public void removeRows(int index, int numberOfRows) {
/*
* While the rows in a static section are removed, the scrollbar is
* temporarily shrunk and then re-expanded. This leads to the fact
* that the scroll position is scooted up a bit. This means that we
* need to reset the position here.
*
* If Escalator, at some point, gets a JIT evaluation functionality,
* this re-setting is a strong candidate for removal.
*/
double oldScrollPos = verticalScrollbar.getScrollPos();
super.removeRows(index, numberOfRows);
recalculateElementSizes();
applyHeightByRows();
verticalScrollbar.setScrollPos(oldScrollPos);
}
@Override
protected void reapplyDefaultRowHeights() {
if (root.getChildCount() == 0) {
return;
}
Profiler.enter(
"Escalator.AbstractStaticRowContainer.reapplyDefaultRowHeights");
Element tr = root.getRows().getItem(0);
while (tr != null) {
reapplyRowHeight(TableRowElement.as(tr), getDefaultRowHeight());
tr = tr.getNextSiblingElement();
}
/*
* Because all rows are immediately displayed in the static row
* containers, the section's overall height has most probably
* changed.
*/
recalculateSectionHeight();
Profiler.leave(
"Escalator.AbstractStaticRowContainer.reapplyDefaultRowHeights");
}
@Override
protected void recalculateSectionHeight() {
Profiler.enter(
"Escalator.AbstractStaticRowContainer.recalculateSectionHeight");
double newHeight = calculateTotalRowHeight();
if (newHeight != heightOfSection) {
heightOfSection = newHeight;
sectionHeightCalculated();
/*
* We need to update the scrollbar dimension at this point. If
* we are scrolled too far down and the static section shrinks,
* the body will try to render rows that don't exist during
* body.verifyEscalatorCount. This is because the logical row
* indices are calculated from the scrollbar position.
*/
verticalScrollbar.setOffsetSize(
heightOfEscalator - header.getHeightOfSection()
- footer.getHeightOfSection());
body.verifyEscalatorCount();
body.spacerContainer.updateSpacerDecosVisibility();
}
Profiler.leave(
"Escalator.AbstractStaticRowContainer.recalculateSectionHeight");
}
/**
* Informs the row container that the height of its respective table
* section has changed.
*
* These calculations might affect some layouting logic, such as the
* body is being offset by the footer, the footer needs to be readjusted
* according to its height, and so on.
*
* A table section is either header, body or footer.
*/
protected abstract void sectionHeightCalculated();
@Override
protected void refreshCells(Range logicalRowRange, Range colRange) {
assertArgumentsAreValidAndWithinRange(logicalRowRange.getStart(),
logicalRowRange.length());
if (!isAttached()) {
return;
}
Profiler.enter("Escalator.AbstractStaticRowContainer.refreshCells");
if (hasColumnAndRowData()) {
for (int row = logicalRowRange.getStart(); row < logicalRowRange
.getEnd(); row++) {
final TableRowElement tr = getTrByVisualIndex(row);
refreshRow(tr, row, colRange);
}
}
Profiler.leave("Escalator.AbstractStaticRowContainer.refreshCells");
}
@Override
protected void paintInsertRows(int visualIndex, int numberOfRows) {
paintInsertStaticRows(visualIndex, numberOfRows);
}
@Override
protected boolean rowCanBeFrozen(TableRowElement tr) {
assert root.isOrHasChild(
tr) : "Row does not belong to this table section";
return true;
}
@Override
protected double getHeightOfSection() {
return Math.max(0, heightOfSection);
}
}
private class HeaderRowContainer extends AbstractStaticRowContainer {
public HeaderRowContainer(final TableSectionElement headElement) {
super(headElement);
}
@Override
protected void sectionHeightCalculated() {
double heightOfSection = getHeightOfSection();
bodyElem.getStyle().setMarginTop(heightOfSection, Unit.PX);
spacerDecoContainer.getStyle().setMarginTop(heightOfSection,
Unit.PX);
verticalScrollbar.getElement().getStyle().setTop(heightOfSection,
Unit.PX);
headerDeco.getStyle().setHeight(heightOfSection, Unit.PX);
}
@Override
protected String getCellElementTagName() {
return "th";
}
@Override
public void setStylePrimaryName(String primaryStyleName) {
super.setStylePrimaryName(primaryStyleName);
UIObject.setStylePrimaryName(root, primaryStyleName + "-header");
}
}
private class FooterRowContainer extends AbstractStaticRowContainer {
public FooterRowContainer(final TableSectionElement footElement) {
super(footElement);
}
@Override
public void setStylePrimaryName(String primaryStyleName) {
super.setStylePrimaryName(primaryStyleName);
UIObject.setStylePrimaryName(root, primaryStyleName + "-footer");
}
@Override
protected String getCellElementTagName() {
return "td";
}
@Override
protected void sectionHeightCalculated() {
double headerHeight = header.getHeightOfSection();
double footerHeight = footer.getHeightOfSection();
int vscrollHeight = (int) Math
.floor(heightOfEscalator - headerHeight - footerHeight);
final boolean horizontalScrollbarNeeded = columnConfiguration
.calculateRowWidth() > widthOfEscalator;
if (horizontalScrollbarNeeded) {
vscrollHeight -= horizontalScrollbar.getScrollbarThickness();
}
footerDeco.getStyle().setHeight(footer.getHeightOfSection(),
Unit.PX);
verticalScrollbar.setOffsetSize(vscrollHeight);
}
}
private class BodyRowContainerImpl extends AbstractRowContainer
implements BodyRowContainer {
/*
* 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.
*
* @see #sortDomElements()
*/
private final LinkedList
* The difference between using this method and simply scrolling is that
* this method "takes the rows and spacers with it" and renders them
* appropriately. The viewport may be scrolled any arbitrary amount, and
* the contents are moved appropriately, but always snapped into a
* plausible place.
*
*
* If Escalator already is at (or beyond) max capacity, this method does
* nothing to the DOM.
*
* @param index
* the index at which to add new escalator rows.
* Note:It is assumed that the index is both the
* visual index and the logical index.
* @param numberOfRows
* the number of rows to add at
* It converts a logical range of rows index to the matching visual
* range, truncating the resulting range with the viewport.
*
*
* This method should be called when e.g. the height of the Escalator
* changes.
*
* Note: This method will make sure that the escalator rows are
* placed in the proper places. By default new rows are added below, but
* if the content is scrolled down, the rows are populated on top
* instead.
*/
public void verifyEscalatorCount() {
/*
* This method indeed has a smell very similar to paintRemoveRows
* and paintInsertRows.
*
* Unfortunately, those the code can't trivially be shared, since
* there are some slight differences in the respective
* responsibilities. The "paint" methods fake the addition and
* removal of rows, and make sure to either push existing data out
* of view, or draw new data into view. Only in some special cases
* will the DOM element count change.
*
* This method, however, has the explicit responsibility to verify
* that when "something" happens, we still have the correct amount
* of escalator rows in the DOM, and if not, we make sure to modify
* that count. Only in some special cases do we need to take into
* account other things than simply modifying the DOM element count.
*/
Profiler.enter("Escalator.BodyRowContainer.verifyEscalatorCount");
if (!isAttached()) {
return;
}
final int maxEscalatorRows = getMaxEscalatorRowCapacity();
final int neededEscalatorRows = Math.min(maxEscalatorRows,
body.getRowCount());
final int neededEscalatorRowsDiff = neededEscalatorRows
- visualRowOrder.size();
if (neededEscalatorRowsDiff > 0) {
// needs more
/*
* This is a workaround for the issue where we might be scrolled
* to the bottom, and the widget expands beyond the content
* range
*/
final int index = visualRowOrder.size();
final int nextLastLogicalIndex;
if (!visualRowOrder.isEmpty()) {
nextLastLogicalIndex = getLogicalRowIndex(
visualRowOrder.getLast()) + 1;
} else {
nextLastLogicalIndex = 0;
}
final boolean contentWillFit = nextLastLogicalIndex < getRowCount()
- neededEscalatorRowsDiff;
if (contentWillFit) {
final List
* A correct result requires that both {@link #getDefaultRowHeight()} is
* consistent, and the placement and height of all spacers above the
* given logical index are consistent.
*
* @param logicalIndex
* the logical index of the row for which to calculate the
* top position
* @return the position at which to place a row in {@code logicalIndex}
* @see #getRowTop(TableRowElement)
*/
private double getRowTop(int logicalIndex) {
double top = spacerContainer
.getSpacerHeightsSumUntilIndex(logicalIndex);
return top + (logicalIndex * getDefaultRowHeight());
}
public void shiftRowPositions(int row, double diff) {
for (TableRowElement tr : getVisibleRowsAfter(row)) {
setRowPosition(tr, 0, getRowTop(tr) + diff);
}
}
private List
* Called by {@link Escalator#onLoad()}.
*/
public boolean measureAndSetWidthIfNeeded() {
assert isAttached() : "Column.measureAndSetWidthIfNeeded() was called even though Escalator was not attached!";
if (measuringRequested) {
measuringRequested = false;
setWidth(definedWidth);
return true;
}
return false;
}
private void calculateWidth() {
calculatedWidth = getMaxCellWidth(columns.indexOf(this));
}
}
private final List
* Implementation detail: This method does no DOM modifications
* (i.e. is very cheap to call) if there are no rows in the DOM when
* this method is called.
*
* @see #hasSomethingInDom()
*/
@Override
public void removeColumns(final int index, final int numberOfColumns) {
// Validate
assertArgumentsAreValidAndWithinRange(index, numberOfColumns);
// Move the horizontal scrollbar to the left, if removed columns are
// to the left of the viewport
removeColumnsAdjustScrollbar(index, numberOfColumns);
// Remove from DOM
header.paintRemoveColumns(index, numberOfColumns);
body.paintRemoveColumns(index, numberOfColumns);
footer.paintRemoveColumns(index, numberOfColumns);
// Remove from bookkeeping
flyweightRow.removeCells(index, numberOfColumns);
columns.subList(index, index + numberOfColumns).clear();
// Adjust frozen columns
if (index < getFrozenColumnCount()) {
if (index + numberOfColumns < frozenColumns) {
/*
* Last removed column was frozen, meaning that all removed
* columns were frozen. Just decrement the number of frozen
* columns accordingly.
*/
frozenColumns -= numberOfColumns;
} else {
/*
* If last removed column was not frozen, we have removed
* columns beyond the frozen range, so all remaining frozen
* columns are to the left of the removed columns.
*/
frozenColumns = index;
}
}
scroller.recalculateScrollbarsForVirtualViewport();
body.verifyEscalatorCount();
if (getColumnConfiguration().getColumnCount() > 0) {
reapplyRowWidths(header);
reapplyRowWidths(body);
reapplyRowWidths(footer);
}
/*
* Colspans make any kind of automatic clever content re-rendering
* impossible: As soon as anything has colspans, removing one might
* reveal further colspans, modifying the DOM structure once again,
* ending in a cascade of updates. Because we don't know how the
* data is updated.
*
* So, instead, we don't do anything. The client code is responsible
* for re-rendering the content (if so desired). Everything Just
* Works (TM) if colspans aren't used.
*/
}
private void reapplyRowWidths(AbstractRowContainer container) {
if (container.getRowCount() > 0) {
container.reapplyRowWidths();
}
}
private void removeColumnsAdjustScrollbar(int index,
int numberOfColumns) {
if (horizontalScrollbar.getOffsetSize() >= horizontalScrollbar
.getScrollSize()) {
return;
}
double leftPosOfFirstColumnToRemove = getCalculatedColumnsWidth(
Range.between(0, index));
double widthOfColumnsToRemove = getCalculatedColumnsWidth(
Range.withLength(index, numberOfColumns));
double scrollLeft = horizontalScrollbar.getScrollPos();
if (scrollLeft <= leftPosOfFirstColumnToRemove) {
/*
* viewport is scrolled to the left of the first removed column,
* so there's no need to adjust anything
*/
return;
}
double adjustedScrollLeft = Math.max(leftPosOfFirstColumnToRemove,
scrollLeft - widthOfColumnsToRemove);
horizontalScrollbar.setScrollPos(adjustedScrollLeft);
}
/**
* Calculate the width of a row, as the sum of columns' widths.
*
* @return the width of a row, in pixels
*/
public double calculateRowWidth() {
return getCalculatedColumnsWidth(
Range.between(0, getColumnCount()));
}
private void assertArgumentsAreValidAndWithinRange(final int index,
final int numberOfColumns) {
if (numberOfColumns < 1) {
throw new IllegalArgumentException(
"Number of columns can't be less than 1 (was "
+ numberOfColumns + ")");
}
if (index < 0 || index + numberOfColumns > getColumnCount()) {
throw new IndexOutOfBoundsException("The given "
+ "column range (" + index + ".."
+ (index + numberOfColumns)
+ ") was outside of the current "
+ "number of columns (" + getColumnCount() + ")");
}
}
/**
* {@inheritDoc}
*
* Implementation detail: This method does no DOM modifications
* (i.e. is very cheap to call) if there is no data for rows when this
* method is called.
*
* @see #hasColumnAndRowData()
*/
@Override
public void insertColumns(final int index, final int numberOfColumns) {
// Validate
if (index < 0 || index > getColumnCount()) {
throw new IndexOutOfBoundsException("The given index(" + index
+ ") was outside of the current number of columns (0.."
+ getColumnCount() + ")");
}
if (numberOfColumns < 1) {
throw new IllegalArgumentException(
"Number of columns must be 1 or greater (was "
+ numberOfColumns);
}
// Add to bookkeeping
flyweightRow.addCells(index, numberOfColumns);
for (int i = 0; i < numberOfColumns; i++) {
columns.add(index, new Column());
}
// Adjust frozen columns
boolean frozen = index < frozenColumns;
if (frozen) {
frozenColumns += numberOfColumns;
}
// Add to DOM
header.paintInsertColumns(index, numberOfColumns, frozen);
body.paintInsertColumns(index, numberOfColumns, frozen);
footer.paintInsertColumns(index, numberOfColumns, frozen);
// this needs to be before the scrollbar adjustment.
boolean scrollbarWasNeeded = horizontalScrollbar
.getOffsetSize() < horizontalScrollbar.getScrollSize();
scroller.recalculateScrollbarsForVirtualViewport();
boolean scrollbarIsNowNeeded = horizontalScrollbar
.getOffsetSize() < horizontalScrollbar.getScrollSize();
if (!scrollbarWasNeeded && scrollbarIsNowNeeded) {
// This might as a side effect move rows around (when scrolled
// all the way down) and require the DOM to be up to date, i.e.
// the column to be added
body.verifyEscalatorCount();
}
// fix initial width
if (header.getRowCount() > 0 || body.getRowCount() > 0
|| footer.getRowCount() > 0) {
Map
* The meaning of each value may differ depending on the context it is being
* used in. Check that particular method's JavaDoc.
*/
private enum SpacerInclusionStrategy {
/** A representation of "the entire spacer". */
COMPLETE,
/** A representation of "a partial spacer". */
PARTIAL,
/** A representation of "no spacer at all". */
NONE
}
private class SpacerContainer {
/** This is used mainly for testing purposes */
private static final String SPACER_LOGICAL_ROW_PROPERTY = "vLogicalRow";
private final class SpacerImpl implements Spacer {
private TableCellElement spacerElement;
private TableRowElement root;
private DivElement deco;
private int rowIndex;
private double height = -1;
private boolean domHasBeenSetup = false;
private double decoHeight;
private double defaultCellBorderBottomSize = -1;
public SpacerImpl(int rowIndex) {
this.rowIndex = rowIndex;
root = TableRowElement.as(DOM.createTR());
spacerElement = TableCellElement.as(DOM.createTD());
root.appendChild(spacerElement);
root.setPropertyInt(SPACER_LOGICAL_ROW_PROPERTY, rowIndex);
deco = DivElement.as(DOM.createDiv());
}
public void setPositionDiff(double x, double y) {
setPosition(getLeft() + x, getTop() + y);
}
public void setupDom(double height) {
assert !domHasBeenSetup : "DOM can't be set up twice.";
assert RootPanel.get().getElement().isOrHasChild(
root) : "Root element should've been attached to the DOM by now.";
domHasBeenSetup = true;
getRootElement().getStyle().setWidth(getInnerWidth(), Unit.PX);
setHeight(height);
spacerElement
.setColSpan(getColumnConfiguration().getColumnCount());
setStylePrimaryName(getStylePrimaryName());
}
public TableRowElement getRootElement() {
return root;
}
@Override
public Element getDecoElement() {
return deco;
}
public void setPosition(double x, double y) {
positions.set(getRootElement(), x, y);
positions.set(getDecoElement(), 0,
y - getSpacerDecoTopOffset());
}
private double getSpacerDecoTopOffset() {
return getBody().getDefaultRowHeight();
}
public void setStylePrimaryName(String style) {
UIObject.setStylePrimaryName(root, style + "-spacer");
UIObject.setStylePrimaryName(deco, style + "-spacer-deco");
}
public void setHeight(double height) {
assert height >= 0 : "Height must be more >= 0 (was " + height
+ ")";
final double heightDiff = height - Math.max(0, this.height);
final double oldHeight = this.height;
this.height = height;
// since the spacer might be rendered on top of the previous
// rows border (done with css), need to increase height the
// amount of the border thickness
if (defaultCellBorderBottomSize < 0) {
defaultCellBorderBottomSize = WidgetUtil
.getBorderBottomThickness(body
.getRowElement(
getVisibleRowRange().getStart())
.getFirstChildElement());
}
root.getStyle().setHeight(height + defaultCellBorderBottomSize,
Unit.PX);
// move the visible spacers getRow row onwards.
shiftSpacerPositionsAfterRow(getRow(), heightDiff);
/*
* If we're growing, we'll adjust the scroll size first, then
* adjust scrolling. If we're shrinking, we do it after the
* second if-clause.
*/
boolean spacerIsGrowing = heightDiff > 0;
if (spacerIsGrowing) {
verticalScrollbar.setScrollSize(
verticalScrollbar.getScrollSize() + heightDiff);
}
/*
* Don't modify the scrollbars if we're expanding the -1 spacer
* while we're scrolled to the top.
*/
boolean minusOneSpacerException = spacerIsGrowing
&& getRow() == -1 && body.getTopRowLogicalIndex() == 0;
boolean viewportNeedsScrolling = getRow() < body
.getTopRowLogicalIndex() && !minusOneSpacerException;
if (viewportNeedsScrolling) {
/*
* We can't use adjustScrollPos here, probably because of a
* bookkeeping-related race condition.
*
* This particular situation is easier, however, since we
* know exactly how many pixels we need to move (heightDiff)
* and all elements below the spacer always need to move
* that pixel amount.
*/
for (TableRowElement row : body.visualRowOrder) {
body.setRowPosition(row, 0,
body.getRowTop(row) + heightDiff);
}
double top = getTop();
double bottom = top + oldHeight;
double scrollTop = verticalScrollbar.getScrollPos();
boolean viewportTopIsAtMidSpacer = top < scrollTop
&& scrollTop < bottom;
final double moveDiff;
if (viewportTopIsAtMidSpacer && !spacerIsGrowing) {
/*
* If the scroll top is in the middle of the modified
* spacer, we want to scroll the viewport up as usual,
* but we don't want to scroll past the top of it.
*
* Math.max ensures this (remember: the result is going
* to be negative).
*/
moveDiff = Math.max(heightDiff, top - scrollTop);
} else {
moveDiff = heightDiff;
}
body.setBodyScrollPosition(tBodyScrollLeft,
tBodyScrollTop + moveDiff);
verticalScrollbar.setScrollPosByDelta(moveDiff);
} else {
body.shiftRowPositions(getRow(), heightDiff);
}
if (!spacerIsGrowing) {
verticalScrollbar.setScrollSize(
verticalScrollbar.getScrollSize() + heightDiff);
}
updateDecoratorGeometry(height);
}
/** Resizes and places the decorator. */
private void updateDecoratorGeometry(double detailsHeight) {
Style style = deco.getStyle();
decoHeight = detailsHeight + getBody().getDefaultRowHeight();
style.setHeight(decoHeight, Unit.PX);
}
@Override
public Element getElement() {
return spacerElement;
}
@Override
public int getRow() {
return rowIndex;
}
public double getHeight() {
assert height >= 0 : "Height was not previously set by setHeight.";
return height;
}
public double getTop() {
return positions.getTop(getRootElement());
}
public double getLeft() {
return positions.getLeft(getRootElement());
}
/**
* Sets a new row index for this spacer. Also updates the bookeeping
* at {@link SpacerContainer#rowIndexToSpacer}.
*/
@SuppressWarnings("boxing")
public void setRowIndex(int rowIndex) {
SpacerImpl spacer = rowIndexToSpacer.remove(this.rowIndex);
assert this == spacer : "trying to move an unexpected spacer.";
this.rowIndex = rowIndex;
root.setPropertyInt(SPACER_LOGICAL_ROW_PROPERTY, rowIndex);
rowIndexToSpacer.put(this.rowIndex, this);
}
/**
* Updates the spacer's visibility parameters, based on whether it
* is being currently visible or not.
*/
public void updateVisibility() {
if (isInViewport()) {
show();
} else {
hide();
}
}
private boolean isInViewport() {
int top = (int) Math.ceil(getTop());
int height = (int) Math.floor(getHeight());
Range location = Range.withLength(top, height);
return getViewportPixels().intersects(location);
}
public void show() {
getRootElement().getStyle().clearDisplay();
getDecoElement().getStyle().clearDisplay();
}
public void hide() {
getRootElement().getStyle().setDisplay(Display.NONE);
getDecoElement().getStyle().setDisplay(Display.NONE);
}
/**
* Crop the decorator element so that it doesn't overlap the header
* and footer sections.
*
* @param bodyTop
* the top cordinate of the escalator body
* @param bodyBottom
* the bottom cordinate of the escalator body
* @param decoWidth
* width of the deco
*/
private void updateDecoClip(final double bodyTop,
final double bodyBottom, final double decoWidth) {
final int top = deco.getAbsoluteTop();
final int bottom = deco.getAbsoluteBottom();
/*
* FIXME
*
* Height and its use is a workaround for the issue where
* coordinates of the deco are not calculated yet. This will
* prevent a deco from being displayed when it's added to DOM
*/
final int height = bottom - top;
if (top < bodyTop || bottom > bodyBottom) {
final double topClip = Math.max(0.0D, bodyTop - top);
final double bottomClip = height
- Math.max(0.0D, bottom - bodyBottom);
// TODO [optimize] not sure how GWT compiles this
final String clip = new StringBuilder("rect(")
.append(topClip).append("px,").append(decoWidth)
.append("px,").append(bottomClip).append("px,0)")
.toString();
deco.getStyle().setProperty("clip", clip);
} else {
deco.getStyle().setProperty("clip", "auto");
}
}
}
private final TreeMap
*
* In this method, the {@link SpacerInclusionStrategy} has the following
* meaning when a spacer lies in the middle of either pixel argument:
*
* In this method, the {@link SpacerInclusionStrategy} has the following
* meaning when a spacer lies in the middle of either pixel argument:
*
* This moves both their associated row index and also their visual
* placement.
*
* Note: This method does not check for the validity of any
* arguments.
*
* @param index
* the index of first row to move
* @param numberOfRows
* the number of rows to shift the spacers with. A positive
* value is downwards, a negative value is upwards.
*/
public void shiftSpacersByRows(int index, int numberOfRows) {
final double pxDiff = numberOfRows * body.getDefaultRowHeight();
for (SpacerContainer.SpacerImpl spacer : getSpacersForRowAndAfter(
index)) {
spacer.setPositionDiff(0, pxDiff);
spacer.setRowIndex(spacer.getRow() + numberOfRows);
}
}
private void updateSpacerDecosVisibility() {
final Range visibleRowRange = getVisibleRowRange();
Collection
* This constant is placed in the Escalator class, instead of an inner
* class, since even mathematical expressions aren't allowed in non-static
* inner classes for constants.
*/
private static final double RATIO_OF_30_DEGREES = 1 / Math.sqrt(3);
/**
* The solution to
*
* This constant is placed in the Escalator class, instead of an inner
* class, since even mathematical expressions aren't allowed in non-static
* inner classes for constants.
*/
private static final double RATIO_OF_40_DEGREES = Math.tan(2 * Math.PI / 9);
private static final String DEFAULT_WIDTH = "500.0px";
private static final String DEFAULT_HEIGHT = "400.0px";
private FlyweightRow flyweightRow = new FlyweightRow();
/** The {@code } tag. */
private final TableSectionElement headElem = TableSectionElement
.as(DOM.createTHead());
/** The {@code
* If Escalator is currently not in {@link HeightMode#CSS}, the given value
* is remembered, and applied once the mode is applied.
*
* @see #setHeightMode(HeightMode)
*/
@Override
public void setHeight(String height) {
/*
* TODO remove method once RequiresResize and the Vaadin layoutmanager
* listening mechanisms are implemented
*/
if (height != null && !height.isEmpty()) {
heightByCss = height;
} else {
if (getHeightMode() == HeightMode.UNDEFINED) {
heightByRows = body.getRowCount();
applyHeightByRows();
return;
} else {
heightByCss = DEFAULT_HEIGHT;
}
}
if (getHeightMode() == HeightMode.CSS) {
setHeightInternal(height);
}
}
private void setHeightInternal(final String height) {
final int escalatorRowsBefore = body.visualRowOrder.size();
if (height != null && !height.isEmpty()) {
super.setHeight(height);
} else {
if (getHeightMode() == HeightMode.UNDEFINED) {
int newHeightByRows = body.getRowCount();
if (heightByRows != newHeightByRows) {
heightByRows = newHeightByRows;
applyHeightByRows();
}
return;
} else {
super.setHeight(DEFAULT_HEIGHT);
}
}
recalculateElementSizes();
if (escalatorRowsBefore != body.visualRowOrder.size()) {
fireRowVisibilityChangeEvent();
}
}
/**
* Returns the vertical scroll offset. Note that this is not necessarily the
* same as the {@code scrollTop} attribute in the DOM.
*
* @return the logical vertical scroll offset
*/
public double getScrollTop() {
return verticalScrollbar.getScrollPos();
}
/**
* Sets the vertical scroll offset. Note that this will not necessarily
* become the same as the {@code scrollTop} attribute in the DOM.
*
* @param scrollTop
* the number of pixels to scroll vertically
*/
public void setScrollTop(final double scrollTop) {
verticalScrollbar.setScrollPos(scrollTop);
}
/**
* Returns the logical horizontal scroll offset. Note that this is not
* necessarily the same as the {@code scrollLeft} attribute in the DOM.
*
* @return the logical horizontal scroll offset
*/
public double getScrollLeft() {
return horizontalScrollbar.getScrollPos();
}
/**
* Sets the logical horizontal scroll offset. Note that will not necessarily
* become the same as the {@code scrollLeft} attribute in the DOM.
*
* @param scrollLeft
* the number of pixels to scroll horizontally
*/
public void setScrollLeft(final double scrollLeft) {
horizontalScrollbar.setScrollPos(scrollLeft);
}
/**
* Returns the scroll width for the escalator. Note that this is not
* necessary the same as {@code Element.scrollWidth} in the DOM.
*
* @since 7.5.0
* @return the scroll width in pixels
*/
public double getScrollWidth() {
return horizontalScrollbar.getScrollSize();
}
/**
* Returns the scroll height for the escalator. Note that this is not
* necessary the same as {@code Element.scrollHeight} in the DOM.
*
* @since 7.5.0
* @return the scroll height in pixels
*/
public double getScrollHeight() {
return verticalScrollbar.getScrollSize();
}
/**
* Scrolls the body horizontally so that the column at the given index is
* visible and there is at least {@code padding} pixels in the direction of
* 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}
* and padding is nonzero; or if the indicated column is frozen;
* or if {@code destination == null}
*/
public void scrollToColumn(final int columnIndex,
final ScrollDestination destination, final int padding)
throws IndexOutOfBoundsException, IllegalArgumentException {
validateScrollDestination(destination, padding);
verifyValidColumnIndex(columnIndex);
if (columnIndex < columnConfiguration.frozenColumns) {
throw new IllegalArgumentException(
"The given column index " + columnIndex + " is frozen.");
}
scroller.scrollToColumn(columnIndex, destination, padding);
}
private void verifyValidColumnIndex(final int columnIndex)
throws IndexOutOfBoundsException {
if (columnIndex < 0
|| columnIndex >= columnConfiguration.getColumnCount()) {
throw new IndexOutOfBoundsException("The given column index "
+ columnIndex + " does not exist.");
}
}
/**
* 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}
* and padding is nonzero; or if {@code destination == null}
* @see #scrollToRowAndSpacer(int, ScrollDestination, int)
* @see #scrollToSpacer(int, ScrollDestination, int)
*/
public void scrollToRow(final int rowIndex,
final ScrollDestination destination, final int padding)
throws IndexOutOfBoundsException, IllegalArgumentException {
Scheduler.get().scheduleFinally(new ScheduledCommand() {
@Override
public void execute() {
validateScrollDestination(destination, padding);
verifyValidRowIndex(rowIndex);
scroller.scrollToRow(rowIndex, destination, padding);
}
});
}
private void verifyValidRowIndex(final int rowIndex) {
if (rowIndex < 0 || rowIndex >= body.getRowCount()) {
throw new IndexOutOfBoundsException(
"The given row index " + rowIndex + " does not exist.");
}
}
/**
* Scrolls the body vertically so that the spacer at the given row index is
* visible and there is at least {@literal padding} pixesl to the given
* scroll destination.
*
* @since 7.5.0
* @param spacerIndex
* the row index of the spacer to scroll to
* @param destination
* where the spacer should be aligned visually after scrolling
* @param padding
* the number of pixels to place between the scrolled-to spacer
* and the viewport edge
* @throws IllegalArgumentException
* if {@code spacerIndex} is not an opened spacer; or if
* {@code destination} is {@link ScrollDestination#MIDDLE} and
* padding is nonzero; or if {@code destination == null}
* @see #scrollToRow(int, ScrollDestination, int)
* @see #scrollToRowAndSpacer(int, ScrollDestination, int)
*/
public void scrollToSpacer(final int spacerIndex,
ScrollDestination destination, final int padding)
throws IllegalArgumentException {
validateScrollDestination(destination, padding);
body.scrollToSpacer(spacerIndex, destination, padding);
}
/**
* Scrolls vertically to a row and the spacer below it.
*
* If a spacer is not open at that index, this method behaves like
* {@link #scrollToRow(int, ScrollDestination, int)}
*
* @since 7.5.0
* @param rowIndex
* the index of the logical row to scroll to. -1 takes the
* topmost spacer into account as well.
* @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.
* @see #scrollToRow(int, ScrollDestination, int)
* @see #scrollToSpacer(int, ScrollDestination, int)
* @throws IllegalArgumentException
* if {@code destination} is {@link ScrollDestination#MIDDLE}
* and {@code padding} is not zero; or if {@code rowIndex} is
* not a valid row index, or -1; or if
* {@code destination == null}; or if {@code rowIndex == -1} and
* there is no spacer open at that index.
*/
public void scrollToRowAndSpacer(final int rowIndex,
final ScrollDestination destination, final int padding)
throws IllegalArgumentException {
Scheduler.get().scheduleFinally(new ScheduledCommand() {
@Override
public void execute() {
validateScrollDestination(destination, padding);
if (rowIndex != -1) {
verifyValidRowIndex(rowIndex);
}
// row range
final Range rowRange;
if (rowIndex != -1) {
int rowTop = (int) Math.floor(body.getRowTop(rowIndex));
int rowHeight = (int) Math.ceil(body.getDefaultRowHeight());
rowRange = Range.withLength(rowTop, rowHeight);
} else {
rowRange = Range.withLength(0, 0);
}
// get spacer
final SpacerContainer.SpacerImpl spacer = body.spacerContainer
.getSpacer(rowIndex);
if (rowIndex == -1 && spacer == null) {
throw new IllegalArgumentException(
"Cannot scroll to row index "
+ "-1, as there is no spacer open at that index.");
}
// make into target range
final Range targetRange;
if (spacer != null) {
final int spacerTop = (int) Math.floor(spacer.getTop());
final int spacerHeight = (int) Math
.ceil(spacer.getHeight());
Range spacerRange = Range.withLength(spacerTop,
spacerHeight);
targetRange = rowRange.combineWith(spacerRange);
} else {
targetRange = rowRange;
}
// get params
int targetStart = targetRange.getStart();
int targetEnd = targetRange.getEnd();
double viewportStart = getScrollTop();
double viewportEnd = viewportStart + body.getHeightOfSection();
double scrollPos = getScrollPos(destination, targetStart,
targetEnd, viewportStart, viewportEnd, padding);
setScrollTop(scrollPos);
}
});
}
private static void validateScrollDestination(
final ScrollDestination destination, final int padding) {
if (destination == null) {
throw new IllegalArgumentException("Destination cannot be null");
}
if (destination == ScrollDestination.MIDDLE && padding != 0) {
throw new IllegalArgumentException(
"You cannot have a padding with a MIDDLE destination");
}
}
/**
* Recalculates the dimensions for all elements that require manual
* calculations. Also updates the dimension caches.
*
* Note: This method has the side-effect
* automatically makes sure that an appropriate amount of escalator rows are
* present. So, if the body area grows, more escalator rows might be
* inserted. Conversely, if the body area shrinks,
* escalator rows might be removed.
*/
private void recalculateElementSizes() {
if (!isAttached()) {
return;
}
Profiler.enter("Escalator.recalculateElementSizes");
widthOfEscalator = Math.max(0, WidgetUtil
.getRequiredWidthBoundingClientRectDouble(getElement()));
heightOfEscalator = Math.max(0, WidgetUtil
.getRequiredHeightBoundingClientRectDouble(getElement()));
header.recalculateSectionHeight();
body.recalculateSectionHeight();
footer.recalculateSectionHeight();
scroller.recalculateScrollbarsForVirtualViewport();
body.verifyEscalatorCount();
body.reapplySpacerWidths();
Profiler.leave("Escalator.recalculateElementSizes");
}
/**
* Snap deltas of x and y to the major four axes (up, down, left, right)
* with a threshold of a number of degrees from those axes.
*
* @param deltaX
* the delta in the x axis
* @param deltaY
* the delta in the y axis
* @param thresholdRatio
* the threshold in ratio (0..1) between x and y for when to snap
* @return a two-element array:
* If Escalator is currently not in {@link HeightMode#ROW}, the given value
* is remembered, and applied once the mode is applied.
*
* @param rows
* the number of rows that should be visible in Escalator's body
* @throws IllegalArgumentException
* if {@code rows} is ≤ 0, {@link Double#isInifinite(double)
* infinite} or {@link Double#isNaN(double) NaN}.
* @see #setHeightMode(HeightMode)
*/
public void setHeightByRows(double rows) throws IllegalArgumentException {
if (rows <= 0) {
throw new IllegalArgumentException(
"The number of rows must be a positive number.");
} else if (Double.isInfinite(rows)) {
throw new IllegalArgumentException(
"The number of rows must be finite.");
} else if (Double.isNaN(rows)) {
throw new IllegalArgumentException("The number must not be NaN.");
}
heightByRows = rows;
applyHeightByRows();
}
/**
* Gets the amount of rows in Escalator's body that are shown, while
* {@link #getHeightMode()} is {@link HeightMode#ROW}.
*
* By default, it is 10.
*
* @return the amount of rows that are being shown in Escalator's body
* @see #setHeightByRows(double)
*/
public double getHeightByRows() {
return heightByRows;
}
/**
* Reapplies the row-based height of the Grid, if Grid currently should
* define its height that way.
*/
private void applyHeightByRows() {
if (heightMode != HeightMode.ROW
&& heightMode != HeightMode.UNDEFINED) {
return;
}
double headerHeight = header.getHeightOfSection();
double footerHeight = footer.getHeightOfSection();
double bodyHeight = body.getDefaultRowHeight() * heightByRows;
double scrollbar = horizontalScrollbar.showsScrollHandle()
? horizontalScrollbar.getScrollbarThickness() : 0;
double spacerHeight = 0; // ignored if HeightMode.ROW
if (heightMode == HeightMode.UNDEFINED) {
spacerHeight = body.spacerContainer.getSpacerHeightsSum();
}
double totalHeight = headerHeight + bodyHeight + spacerHeight
+ scrollbar + footerHeight;
setHeightInternal(totalHeight + "px");
}
/**
* Defines the mode in which the Escalator widget's height is calculated.
*
* If {@link HeightMode#CSS} is given, Escalator will respect the values
* given via {@link #setHeight(String)}, and behave as a traditional Widget.
*
* If {@link HeightMode#ROW} is given, Escalator will make sure that the
* {@link #getBody() body} will display as many rows as
* {@link #getHeightByRows()} defines. Note: If headers/footers are
* inserted or removed, the widget will resize itself to still display the
* required amount of rows in its body. It also takes the horizontal
* scrollbar into account.
*
* @param heightMode
* the mode in to which Escalator should be set
*/
public void setHeightMode(HeightMode heightMode) {
/*
* This method is a workaround for the fact that Vaadin re-applies
* widget dimensions (height/width) on each state change event. The
* original design was to have setHeight an setHeightByRow be equals,
* and whichever was called the latest was considered in effect.
*
* But, because of Vaadin always calling setHeight on the widget, this
* approach doesn't work.
*/
if (heightMode != this.heightMode) {
this.heightMode = heightMode;
switch (this.heightMode) {
case CSS:
setHeight(heightByCss);
break;
case ROW:
setHeightByRows(heightByRows);
break;
case UNDEFINED:
setHeightByRows(body.getRowCount());
break;
default:
throw new IllegalStateException("Unimplemented feature "
+ "- unknown HeightMode: " + this.heightMode);
}
}
}
/**
* Returns the current {@link HeightMode} the Escalator is in.
*
* Defaults to {@link HeightMode#CSS}.
*
* @return the current HeightMode
*/
public HeightMode getHeightMode() {
return heightMode;
}
/**
* Returns the {@link RowContainer} which contains the element.
*
* @param element
* the element to check for
* @return the container the element is in or
* If a direction is locked, the escalator will refuse to scroll in that
* direction.
*
* @param direction
* the orientation of the scroll to set the lock status
* @param locked
*
* } tags) are contained in.
*/
protected final TableSectionElement root;
/**
* The primary style name of the escalator. Most commonly provided by
* Escalator as "v-escalator".
*/
private String primaryStyleName = null;
private boolean defaultRowHeightShouldBeAutodetected = true;
private double defaultRowHeight = INITIAL_DEFAULT_ROW_HEIGHT;
public AbstractRowContainer(
final TableSectionElement rowContainerElement) {
root = rowContainerElement;
}
@Override
public TableSectionElement getElement() {
return root;
}
/**
* Gets the tag name of an element to represent a cell in a row.
* true
iff this the given element, or any of its
* descendants, can be frozen
*/
abstract protected boolean rowCanBeFrozen(TableRowElement tr);
/**
* Iterates through all the cells in a column and returns the width of
* the widest element in this RowContainer.
*
* @param index
* the index of the column to inspect
* @return the pixel width of the widest element in the indicated column
*/
public double calculateMaxColWidth(int index) {
TableRowElement row = TableRowElement
.as(root.getFirstChildElement());
double maxWidth = 0;
while (row != null) {
final TableCellElement cell = row.getCells().getItem(index);
final boolean isVisible = !cell.getStyle().getDisplay()
.equals(Display.NONE.getCssName());
if (isVisible) {
maxWidth = Math.max(maxWidth, WidgetUtil
.getRequiredWidthBoundingClientRectDouble(cell));
}
row = TableRowElement.as(row.getNextSiblingElement());
}
return maxWidth;
}
/**
* Reapplies all the cells' widths according to the calculated widths in
* the column configuration.
*/
public void reapplyColumnWidths() {
Element row = root.getFirstChildElement();
while (row != null) {
// Only handle non-spacer rows
if (!body.spacerContainer.isSpacer(row)) {
Element cell = row.getFirstChildElement();
int columnIndex = 0;
while (cell != null) {
final double width = getCalculatedColumnWidthWithColspan(
cell, columnIndex);
/*
* TODO Should Escalator implement ProvidesResize at
* some point, this is where we need to do that.
*/
cell.getStyle().setWidth(width, Unit.PX);
cell = cell.getNextSiblingElement();
columnIndex++;
}
}
row = row.getNextSiblingElement();
}
reapplyRowWidths();
}
private double getCalculatedColumnWidthWithColspan(final Element cell,
final int columnIndex) {
final int colspan = cell.getPropertyInt(FlyweightCell.COLSPAN_ATTR);
Range spannedColumns = Range.withLength(columnIndex, colspan);
/*
* Since browsers don't explode with overflowing colspans, escalator
* shouldn't either.
*/
if (spannedColumns.getEnd() > columnConfiguration
.getColumnCount()) {
spannedColumns = Range.between(columnIndex,
columnConfiguration.getColumnCount());
}
return columnConfiguration
.getCalculatedColumnsWidth(spannedColumns);
}
/**
* Applies the total length of the columns to each row element.
*
* } element, not the cells within.
*/
protected void reapplyRowWidths() {
double rowWidth = columnConfiguration.calculateRowWidth();
if (rowWidth < 0) {
return;
}
Element row = root.getFirstChildElement();
while (row != null) {
// IF there is a rounding error when summing the columns, we
// need to round the tr width up to ensure that columns fit and
// do not wrap
// E.g.122.95+123.25+103.75+209.25+83.52+88.57+263.45+131.21+126.85+113.13=1365.9299999999998
// For this we must set 1365.93 or the last column will wrap
row.getStyle().setWidth(WidgetUtil.roundSizeUp(rowWidth),
Unit.PX);
row = row.getNextSiblingElement();
}
}
/**
* The primary style name for the container.
*
* @param primaryStyleName
* the style name to use as prefix for all row and cell style
* names.
*/
protected void setStylePrimaryName(String primaryStyleName) {
String oldStyle = getStylePrimaryName();
if (SharedUtil.equals(oldStyle, primaryStyleName)) {
return;
}
this.primaryStyleName = primaryStyleName;
// Update already rendered rows and cells
Element row = root.getRows().getItem(0);
while (row != null) {
UIObject.setStylePrimaryName(row, primaryStyleName + "-row");
Element cell = TableRowElement.as(row).getCells().getItem(0);
while (cell != null) {
assert TableCellElement.is(cell);
UIObject.setStylePrimaryName(cell,
primaryStyleName + "-cell");
cell = cell.getNextSiblingElement();
}
row = row.getNextSiblingElement();
}
}
/**
* Returns the primary style name of the container.
*
* @return The primary style name or null
if not set.
*/
protected String getStylePrimaryName() {
return primaryStyleName;
}
@Override
public void setDefaultRowHeight(double px)
throws IllegalArgumentException {
if (px < 1) {
throw new IllegalArgumentException(
"Height must be positive. " + px + " was given.");
}
defaultRowHeightShouldBeAutodetected = false;
defaultRowHeight = px;
reapplyDefaultRowHeights();
}
@Override
public double getDefaultRowHeight() {
return defaultRowHeight;
}
/**
* The default height of rows has (most probably) changed.
* true
if content is taken into account,
* false
if not
* @return cell width needed for displaying correctly
*/
double measureMinCellWidth(int colIndex, boolean withContent) {
assert isAttached() : "Can't measure max width of cell, since Escalator is not attached to the DOM.";
double minCellWidth = -1;
NodeListtrue
if a sort is scheduled */
public boolean waiting = false;
public void reschedule() {
waiting = true;
resetConditions();
animationHandle = AnimationScheduler.get()
.requestAnimationFrame(frameCounter);
}
private boolean sortIfConditionsMet() {
boolean enoughFramesHavePassed = framesPassed >= REQUIRED_FRAMES_PASSED;
boolean enoughTimeHasPassed = (Duration.currentTimeMillis()
- startTime) >= SORT_DELAY_MILLIS;
boolean notTouchActivity = !scroller.touchHandlerBundle.touching;
boolean conditionsMet = enoughFramesHavePassed
&& enoughTimeHasPassed && notTouchActivity;
if (conditionsMet) {
resetConditions();
sortDomElements();
}
return conditionsMet;
}
private void resetConditions() {
if (animationHandle != null) {
animationHandle.cancel();
animationHandle = null;
}
startTime = Duration.currentTimeMillis();
framesPassed = 0;
}
}
private DeferredDomSorter domSorter = new DeferredDomSorter();
private final SpacerContainer spacerContainer = new SpacerContainer();
public BodyRowContainerImpl(final TableSectionElement bodyElement) {
super(bodyElement);
}
@Override
public void setStylePrimaryName(String primaryStyleName) {
super.setStylePrimaryName(primaryStyleName);
UIObject.setStylePrimaryName(root, primaryStyleName + "-body");
spacerContainer.setStylePrimaryName(primaryStyleName);
}
public void updateEscalatorRowsOnScroll() {
if (visualRowOrder.isEmpty()) {
return;
}
boolean rowsWereMoved = false;
final double topElementPosition;
final double nextRowBottomOffset;
SpacerContainer.SpacerImpl topSpacer = spacerContainer
.getSpacer(getTopRowLogicalIndex() - 1);
if (topSpacer != null) {
topElementPosition = topSpacer.getTop();
nextRowBottomOffset = topSpacer.getHeight()
+ getDefaultRowHeight();
} else {
topElementPosition = getRowTop(visualRowOrder.getFirst());
nextRowBottomOffset = getDefaultRowHeight();
}
// TODO [[mpixscroll]]
final double scrollTop = tBodyScrollTop;
final double viewportOffset = topElementPosition - 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
double rowPx = getRowHeightsSumBetweenPx(scrollTop,
topElementPosition);
int originalRowsToMove = (int) Math
.ceil(rowPx / getDefaultRowHeight());
int rowsToMove = Math.min(originalRowsToMove,
visualRowOrder.size());
final int end = visualRowOrder.size();
final int start = end - rowsToMove;
final int logicalRowIndex = getLogicalRowIndex(scrollTop);
moveAndUpdateEscalatorRows(Range.between(start, end), 0,
logicalRowIndex);
setTopRowLogicalIndex(logicalRowIndex);
rowsWereMoved = true;
}
else if (viewportOffset + nextRowBottomOffset <= 0) {
/*
* the viewport has been scrolled more than the topmost visual
* row.
*/
double rowPx = getRowHeightsSumBetweenPx(topElementPosition,
scrollTop);
int originalRowsToMove = (int) (rowPx / getDefaultRowHeight());
int rowsToMove = Math.min(originalRowsToMove,
visualRowOrder.size());
int logicalRowIndex;
if (rowsToMove < visualRowOrder.size()) {
/*
* 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 = getLogicalRowIndex(scrollTop);
}
/*
* 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 = visualRowOrder.size();
// make sure that we don't move rows over the data boundary
boolean aRowWasLeftBehind = false;
if (logicalRowIndex + rowsToMove > getRowCount()) {
/*
* 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
* varying.
*/
rowsToMove--;
aRowWasLeftBehind = true;
}
/*
* Make sure we don't scroll beyond the row content. This can
* happen if we have spacers for the last rows.
*/
rowsToMove = Math.max(0,
Math.min(rowsToMove, getRowCount() - logicalRowIndex));
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);
/*
* We cannot trust getLogicalRowIndex, because it hasn't yet
* been updated. But since we're leaving rows behind, it
* means we've scrolled to the bottom. So, instead, we
* simply count backwards from the end.
*/
final int topLogicalIndex = getRowCount()
- visualRowOrder.size();
moveAndUpdateEscalatorRows(strayRow, 0, topLogicalIndex);
}
final int naiveNewLogicalIndex = getTopRowLogicalIndex()
+ originalRowsToMove;
final int maxLogicalIndex = getRowCount()
- visualRowOrder.size();
setTopRowLogicalIndex(
Math.min(naiveNewLogicalIndex, maxLogicalIndex));
rowsWereMoved = true;
}
if (rowsWereMoved) {
fireRowVisibilityChangeEvent();
domSorter.reschedule();
}
}
private double getRowHeightsSumBetweenPx(double y1, double y2) {
assert y1 < y2 : "y1 must be smaller than y2";
double viewportPx = y2 - y1;
double spacerPx = spacerContainer.getSpacerHeightsSumBetweenPx(y1,
SpacerInclusionStrategy.PARTIAL, y2,
SpacerInclusionStrategy.PARTIAL);
return viewportPx - spacerPx;
}
private int getLogicalRowIndex(final double px) {
double rowPx = px - spacerContainer.getSpacerHeightsSumUntilPx(px);
return (int) (rowPx / getDefaultRowHeight());
}
@Override
protected void paintInsertRows(final int index,
final int numberOfRows) {
if (numberOfRows == 0) {
return;
}
spacerContainer.shiftSpacersByRows(index, numberOfRows);
/*
* 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
*
*
* @param yDelta
* the delta of pixels by which to move the viewport and
* content. A positive value moves everything downwards,
* while a negative value moves everything upwards
*/
public void moveViewportAndContent(final double yDelta) {
if (yDelta == 0) {
return;
}
double newTop = tBodyScrollTop + yDelta;
verticalScrollbar.setScrollPos(newTop);
final double defaultRowHeight = getDefaultRowHeight();
double rowPxDelta = yDelta - (yDelta % defaultRowHeight);
int rowIndexDelta = (int) (yDelta / defaultRowHeight);
if (!WidgetUtil.pixelValuesEqual(rowPxDelta, 0)) {
Collectionindex
* @return a list of the added rows
*/
private List
*
*
* @return a logical range converted to a visual range, truncated to the
* current viewport. The first visual row has the index 0.
*/
private Range convertToVisual(final Range logicalRange) {
if (logicalRange.isEmpty()) {
return logicalRange;
} else if (visualRowOrder.isEmpty()) {
// empty range
return Range.withLength(0, 0);
}
/*
* TODO [[spacer]]: these assumptions will be totally broken with
* spacers.
*/
final int maxEscalatorRows = getMaxEscalatorRowCapacity();
final int currentTopRowIndex = getLogicalRowIndex(
visualRowOrder.getFirst());
final Range[] partitions = logicalRange.partitionWith(
Range.withLength(currentTopRowIndex, maxEscalatorRows));
final Range insideRange = partitions[1];
return insideRange.offsetBy(-currentTopRowIndex);
}
@Override
protected String getCellElementTagName() {
return "td";
}
@Override
protected double getHeightOfSection() {
final int tableHeight = tableWrapper.getOffsetHeight();
final double footerHeight = footer.getHeightOfSection();
final double headerHeight = header.getHeightOfSection();
double heightOfSection = tableHeight - footerHeight - headerHeight;
return Math.max(0, heightOfSection);
}
@Override
protected void refreshCells(Range logicalRowRange, Range colRange) {
Profiler.enter("Escalator.BodyRowContainer.refreshRows");
final Range visualRange = convertToVisual(logicalRowRange);
if (!visualRange.isEmpty()) {
final int firstLogicalRowIndex = getLogicalRowIndex(
visualRowOrder.getFirst());
for (int rowNumber = visualRange
.getStart(); rowNumber < visualRange
.getEnd(); rowNumber++) {
refreshRow(visualRowOrder.get(rowNumber),
firstLogicalRowIndex + rowNumber, colRange);
}
}
Profiler.leave("Escalator.BodyRowContainer.refreshRows");
}
@Override
protected TableRowElement 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);
}
}
@Override
public TableRowElement getRowElement(int index) {
if (index < 0 || index >= getRowCount()) {
throw new IndexOutOfBoundsException(
"No such logical index: " + index);
}
int visualIndex = index
- getLogicalRowIndex(visualRowOrder.getFirst());
if (visualIndex >= 0 && visualIndex < visualRowOrder.size()) {
return super.getRowElement(visualIndex);
} else {
throw new IllegalStateException("Row with logical index "
+ index + " is currently not available in the DOM");
}
}
private void setBodyScrollPosition(final double scrollLeft,
final double scrollTop) {
tBodyScrollLeft = scrollLeft;
tBodyScrollTop = scrollTop;
position.set(bodyElem, -tBodyScrollLeft, -tBodyScrollTop);
position.set(spacerDecoContainer, 0, -tBodyScrollTop);
}
/**
* Make sure that there is a correct amount of escalator rows: Add more
* if needed, or remove any superfluous ones.
* null
if focus is outside of a body
* row.
*/
private TableRowElement getRowWithFocus() {
TableRowElement rowContainingFocus = null;
final Element focusedElement = WidgetUtil.getFocusedElement();
if (focusedElement != null && root.isOrHasChild(focusedElement)) {
Element e = focusedElement;
while (e != null && e != root) {
/*
* You never know if there's several tables embedded in a
* cell... We'll take the deepest one.
*/
if (TableRowElement.is(e)) {
rowContainingFocus = TableRowElement.as(e);
}
e = e.getParentElement();
}
}
return rowContainingFocus;
}
@Override
public Cell getCell(Element element) {
Cell cell = super.getCell(element);
if (cell == null) {
return null;
}
// Convert DOM coordinates to logical coordinates for rows
TableRowElement rowElement = (TableRowElement) cell.getElement()
.getParentElement();
return new Cell(getLogicalRowIndex(rowElement), cell.getColumn(),
cell.getElement());
}
@Override
public void setSpacer(int rowIndex, double height)
throws IllegalArgumentException {
spacerContainer.setSpacer(rowIndex, height);
}
@Override
public void setSpacerUpdater(SpacerUpdater spacerUpdater)
throws IllegalArgumentException {
spacerContainer.setSpacerUpdater(spacerUpdater);
}
@Override
public SpacerUpdater getSpacerUpdater() {
return spacerContainer.getSpacerUpdater();
}
/**
* Calculates the correct top position of a row at a logical
* index, regardless if there is one there or not.
* columns
*/
double getCalculatedColumnsWidth(final Range columns) {
/*
* This is an assert instead of an exception, since this is an
* internal method.
*/
assert columns
.isSubsetOf(Range.between(0, getColumnCount())) : "Range "
+ "was outside of current column range (i.e.: "
+ Range.between(0, getColumnCount())
+ ", but was given :" + columns;
double sum = 0;
for (int i = columns.getStart(); i < columns.getEnd(); i++) {
double columnWidthActual = getColumnWidthActual(i);
sum += columnWidthActual;
}
return sum;
}
double[] getCalculatedColumnWidths() {
if (widthsArray == null || widthsArray.length != getColumnCount()) {
widthsArray = new double[getColumnCount()];
for (int i = 0; i < columns.size(); i++) {
widthsArray[i] = columns.get(i).getCalculatedWidth();
}
}
return widthsArray;
}
@Override
public void refreshColumns(int index, int numberOfColumns)
throws IndexOutOfBoundsException, IllegalArgumentException {
if (numberOfColumns < 1) {
throw new IllegalArgumentException(
"Number of columns must be 1 or greater (was "
+ numberOfColumns + ")");
}
if (index < 0 || index + numberOfColumns > getColumnCount()) {
throw new IndexOutOfBoundsException(
"The given " + "column range (" + index + ".."
+ (index + numberOfColumns)
+ ") was outside of the current number of columns ("
+ getColumnCount() + ")");
}
header.refreshColumns(index, numberOfColumns);
body.refreshColumns(index, numberOfColumns);
footer.refreshColumns(index, numberOfColumns);
}
}
/**
* A decision on how to measure a spacer when it is partially within a
* designated range.
*
*
*
* @param px
* the pixel point after which to return all spacers
* @param strategy
* the inclusion strategy regarding the {@code px}
* @return a collection of the spacers that exist after {@code px}
*/
public Collection
*
*
* @param rangeTop
* the top pixel point
* @param topInclusion
* the inclusion strategy regarding {@code rangeTop}.
* @param rangeBottom
* the bottom pixel point
* @param bottomInclusion
* the inclusion strategy regarding {@code rangeBottom}.
* @return the pixels occupied by spacers between {@code rangeTop} and
* {@code rangeBottom}
*/
public double getSpacerHeightsSumBetweenPx(double rangeTop,
SpacerInclusionStrategy topInclusion, double rangeBottom,
SpacerInclusionStrategy bottomInclusion) {
assert rangeTop <= rangeBottom : "rangeTop must be less than rangeBottom";
double heights = 0;
/*
* TODO [[optimize]]: this might be somewhat inefficient (due to
* iterator-based scanning, instead of using the treemap's search
* functionalities). But it should be easy to write, read, verify
* and maintain.
*/
for (SpacerImpl spacer : rowIndexToSpacer.values()) {
double top = spacer.getTop();
double height = spacer.getHeight();
double bottom = top + height;
/*
* If we happen to implement a DoubleRange (in addition to the
* int-based Range) at some point, the following logic should
* probably be converted into using the
* Range.partitionWith-equivalent.
*/
boolean topIsAboveRange = top < rangeTop;
boolean topIsInRange = rangeTop <= top && top <= rangeBottom;
boolean topIsBelowRange = rangeBottom < top;
boolean bottomIsAboveRange = bottom < rangeTop;
boolean bottomIsInRange = rangeTop <= bottom
&& bottom <= rangeBottom;
boolean bottomIsBelowRange = rangeBottom < bottom;
assert topIsAboveRange ^ topIsBelowRange
^ topIsInRange : "Bad top logic";
assert bottomIsAboveRange ^ bottomIsBelowRange
^ bottomIsInRange : "Bad bottom logic";
if (bottomIsAboveRange) {
continue;
} else if (topIsBelowRange) {
return heights;
}
else if (topIsAboveRange && bottomIsInRange) {
switch (topInclusion) {
case PARTIAL:
heights += bottom - rangeTop;
break;
case COMPLETE:
heights += height;
break;
default:
break;
}
}
else if (topIsAboveRange && bottomIsBelowRange) {
/*
* Here we arbitrarily decide that the top inclusion will
* have the honor of overriding the bottom inclusion if
* happens to be a conflict of interests.
*/
switch (topInclusion) {
case NONE:
return 0;
case COMPLETE:
return height;
case PARTIAL:
return rangeBottom - rangeTop;
default:
throw new IllegalArgumentException(
"Unexpected inclusion state :" + topInclusion);
}
} else if (topIsInRange && bottomIsInRange) {
heights += height;
}
else if (topIsInRange && bottomIsBelowRange) {
switch (bottomInclusion) {
case PARTIAL:
heights += rangeBottom - top;
break;
case COMPLETE:
heights += height;
break;
default:
break;
}
return heights;
}
else {
assert false : "Unnaccounted-for situation";
}
}
return heights;
}
/**
* Gets the amount of pixels occupied by spacers from the top until a
* certain spot from the top of the body.
*
* @param px
* pixels counted from the top
* @return the pixels occupied by spacers up until {@code px}
*/
public double getSpacerHeightsSumUntilPx(double px) {
return getSpacerHeightsSumBetweenPx(0,
SpacerInclusionStrategy.PARTIAL, px,
SpacerInclusionStrategy.PARTIAL);
}
/**
* Gets the amount of pixels occupied by spacers until a logical row
* index.
*
* @param logicalIndex
* a logical row index
* @return the pixels occupied by spacers up until {@code logicalIndex}
*/
@SuppressWarnings("boxing")
public double getSpacerHeightsSumUntilIndex(int logicalIndex) {
return getHeights(
rowIndexToSpacer.headMap(logicalIndex, false).values());
}
private double getHeights(Collection|tan-1(x)|×(180/π) = 30
* .
* |tan-1(x)|×(180/π) = 40
* .
* true
iff header, body or footer has rows && there
* are columns
*/
private boolean hasColumnAndRowData() {
return (header.getRowCount() > 0 || body.getRowCount() > 0
|| footer.getRowCount() > 0)
&& columnConfiguration.getColumnCount() > 0;
}
/**
* Check whether there are any cells in the DOM.
*
* @return true
iff header, body or footer has any child
* elements
*/
private boolean hasSomethingInDom() {
return headElem.hasChildNodes() || bodyElem.hasChildNodes()
|| footElem.hasChildNodes();
}
/**
* Returns the row container for the header in this Escalator.
*
* @return the header. Never null
*/
public RowContainer getHeader() {
return header;
}
/**
* Returns the row container for the body in this Escalator.
*
* @return the body. Never null
*/
public BodyRowContainer getBody() {
return body;
}
/**
* Returns the row container for the footer in this Escalator.
*
* @return the footer. Never null
*/
public RowContainer getFooter() {
return footer;
}
/**
* Returns the configuration object for the columns in this Escalator.
*
* @return the configuration object for the columns in this Escalator. Never
* null
*/
public ColumnConfiguration getColumnConfiguration() {
return columnConfiguration;
}
@Override
public void setWidth(final String width) {
if (width != null && !width.isEmpty()) {
super.setWidth(width);
} else {
super.setWidth(DEFAULT_WIDTH);
}
recalculateElementSizes();
}
/**
* {@inheritDoc}
* [snappedX, snappedY]
*/
private static double[] snapDeltas(final double deltaX, final double deltaY,
final double thresholdRatio) {
final double[] array = new double[2];
if (deltaX != 0 && deltaY != 0) {
final double aDeltaX = Math.abs(deltaX);
final double aDeltaY = Math.abs(deltaY);
final double yRatio = aDeltaY / aDeltaX;
final double xRatio = aDeltaX / aDeltaY;
array[0] = (xRatio < thresholdRatio) ? 0 : deltaX;
array[1] = (yRatio < thresholdRatio) ? 0 : deltaY;
} else {
array[0] = deltaX;
array[1] = deltaY;
}
return array;
}
/**
* Adds an event handler that gets notified when the range of visible rows
* changes e.g. because of scrolling, row resizing or spacers
* appearing/disappearing.
*
* @param rowVisibilityChangeHandler
* the event handler
* @return a handler registration for the added handler
*/
public HandlerRegistration addRowVisibilityChangeHandler(
RowVisibilityChangeHandler rowVisibilityChangeHandler) {
return addHandler(rowVisibilityChangeHandler,
RowVisibilityChangeEvent.TYPE);
}
private void fireRowVisibilityChangeEvent() {
if (!body.visualRowOrder.isEmpty()) {
int visibleRangeStart = body
.getLogicalRowIndex(body.visualRowOrder.getFirst());
int visibleRangeEnd = body
.getLogicalRowIndex(body.visualRowOrder.getLast()) + 1;
int visibleRowCount = visibleRangeEnd - visibleRangeStart;
fireEvent(new RowVisibilityChangeEvent(visibleRangeStart,
visibleRowCount));
} else {
fireEvent(new RowVisibilityChangeEvent(0, 0));
}
}
/**
* Gets the logical index range of currently visible rows.
*
* @return logical index range of visible rows
*/
public Range getVisibleRowRange() {
if (!body.visualRowOrder.isEmpty()) {
return Range.withLength(body.getTopRowLogicalIndex(),
body.visualRowOrder.size());
} else {
return Range.withLength(0, 0);
}
}
/**
* Returns the widget from a cell node or null
if there is no
* widget in the cell
*
* @param cellNode
* The cell node
*/
static Widget getWidgetFromCell(Node cellNode) {
Node possibleWidgetNode = cellNode.getFirstChild();
if (possibleWidgetNode != null
&& possibleWidgetNode.getNodeType() == Node.ELEMENT_NODE) {
@SuppressWarnings("deprecation")
com.google.gwt.user.client.Element castElement = (com.google.gwt.user.client.Element) possibleWidgetNode
.cast();
Widget w = WidgetUtil.findWidget(castElement, null);
// Ensure findWidget did not traverse past the cell element in the
// DOM hierarchy
if (cellNode.isOrHasChild(w.getElement())) {
return w;
}
}
return null;
}
@Override
public void setStylePrimaryName(String style) {
super.setStylePrimaryName(style);
verticalScrollbar.setStylePrimaryName(style);
horizontalScrollbar.setStylePrimaryName(style);
UIObject.setStylePrimaryName(tableWrapper, style + "-tablewrapper");
UIObject.setStylePrimaryName(headerDeco, style + "-header-deco");
UIObject.setStylePrimaryName(footerDeco, style + "-footer-deco");
UIObject.setStylePrimaryName(horizontalScrollbarDeco,
style + "-horizontal-scrollbar-deco");
UIObject.setStylePrimaryName(spacerDecoContainer,
style + "-spacer-deco-container");
header.setStylePrimaryName(style);
body.setStylePrimaryName(style);
footer.setStylePrimaryName(style);
}
/**
* Sets the number of rows that should be visible in Escalator's body, while
* {@link #getHeightMode()} is {@link HeightMode#ROW}.
* null
if element
* is not present in any container.
*/
public RowContainer findRowContainer(Element element) {
if (getHeader().getElement() != element
&& getHeader().getElement().isOrHasChild(element)) {
return getHeader();
} else if (getBody().getElement() != element
&& getBody().getElement().isOrHasChild(element)) {
return getBody();
} else if (getFooter().getElement() != element
&& getFooter().getElement().isOrHasChild(element)) {
return getFooter();
}
return null;
}
/**
* Sets whether a scroll direction is locked or not.
* true
to lock, false
to unlock
*/
public void setScrollLocked(ScrollbarBundle.Direction direction,
boolean locked) {
switch (direction) {
case HORIZONTAL:
horizontalScrollbar.setLocked(locked);
break;
case VERTICAL:
verticalScrollbar.setLocked(locked);
break;
default:
throw new UnsupportedOperationException(
"Unexpected value: " + direction);
}
}
/**
* Checks whether or not an direction is locked for scrolling.
*
* @param direction
* the direction of the scroll of which to check the lock status
* @return true
iff the direction is locked
*/
public boolean isScrollLocked(ScrollbarBundle.Direction direction) {
switch (direction) {
case HORIZONTAL:
return horizontalScrollbar.isLocked();
case VERTICAL:
return verticalScrollbar.isLocked();
default:
throw new UnsupportedOperationException(
"Unexpected value: " + direction);
}
}
/**
* Adds a scroll handler to this escalator
*
* @param handler
* the scroll handler to add
* @return a handler registration for the registered scroll handler
*/
public HandlerRegistration addScrollHandler(ScrollHandler handler) {
return addHandler(handler, ScrollEvent.TYPE);
}
@Override
public boolean isWorkPending() {
return body.domSorter.waiting || verticalScrollbar.isWorkPending()
|| horizontalScrollbar.isWorkPending() || layoutIsScheduled;
}
@Override
public void onResize() {
if (isAttached() && !layoutIsScheduled) {
layoutIsScheduled = true;
Scheduler.get().scheduleFinally(layoutCommand);
}
}
/**
* Gets the maximum number of body rows that can be visible on the screen at
* once.
*
* @return the maximum capacity
*/
public int getMaxVisibleRowCount() {
return body.getMaxEscalatorRowCapacity();
}
/**
* Gets the escalator's inner width. This is the entire width in pixels,
* without the vertical scrollbar.
*
* @return escalator's inner width
*/
public double getInnerWidth() {
return WidgetUtil
.getRequiredWidthBoundingClientRectDouble(tableWrapper);
}
/**
* Resets all cached pixel sizes and reads new values from the DOM. This
* methods should be used e.g. when styles affecting the dimensions of
* elements in this escalator have been changed.
*/
public void resetSizesFromDom() {
header.autodetectRowHeightNow();
body.autodetectRowHeightNow();
footer.autodetectRowHeightNow();
for (int i = 0; i < columnConfiguration.getColumnCount(); i++) {
columnConfiguration.setColumnWidth(i,
columnConfiguration.getColumnWidth(i));
}
}
private Range getViewportPixels() {
int from = (int) Math.floor(verticalScrollbar.getScrollPos());
int to = (int) body.getHeightOfSection();
return Range.withLength(from, to);
}
@Override
@SuppressWarnings("deprecation")
public com.google.gwt.user.client.Element getSubPartElement(
String subPart) {
SubPartArguments args = SubPartArguments.create(subPart);
Element tableStructureElement = getSubPartElementTableStructure(args);
if (tableStructureElement != null) {
return DOM.asOld(tableStructureElement);
}
Element spacerElement = getSubPartElementSpacer(args);
if (spacerElement != null) {
return DOM.asOld(spacerElement);
}
return null;
}
private Element getSubPartElementTableStructure(SubPartArguments args) {
String type = args.getType();
int[] indices = args.getIndices();
// Get correct RowContainer for type from Escalator
RowContainer container = null;
if (type.equalsIgnoreCase("header")) {
container = getHeader();
} else if (type.equalsIgnoreCase("cell")) {
// If wanted row is not visible, we need to scroll there.
Range visibleRowRange = getVisibleRowRange();
if (indices.length > 0) {
// Contains a row number, ensure it is available and visible
boolean rowInCache = visibleRowRange.contains(indices[0]);
// Scrolling might be a no-op if row is already in the viewport
scrollToRow(indices[0], ScrollDestination.ANY, 0);
if (!rowInCache) {
// Row was not in cache, scrolling caused lazy loading and
// the caller needs to wait and call this method again to be
// able to get the requested element
return null;
}
}
container = getBody();
} else if (type.equalsIgnoreCase("footer")) {
container = getFooter();
}
if (null != container) {
if (indices.length == 0) {
// No indexing. Just return the wanted container element
return container.getElement();
} else {
try {
return getSubPart(container, indices);
} catch (Exception e) {
getLogger().log(Level.SEVERE, e.getMessage());
}
}
}
return null;
}
private Element getSubPart(RowContainer container, int[] indices) {
Element targetElement = container.getRowElement(indices[0]);
// Scroll wanted column to view if able
if (indices.length > 1 && targetElement != null) {
if (getColumnConfiguration().getFrozenColumnCount() <= indices[1]) {
scrollToColumn(indices[1], ScrollDestination.ANY, 0);
}
targetElement = getCellFromRow(TableRowElement.as(targetElement),
indices[1]);
for (int i = 2; i < indices.length && targetElement != null; ++i) {
targetElement = (Element) targetElement.getChild(indices[i]);
}
}
return targetElement;
}
private static Element getCellFromRow(TableRowElement rowElement,
int index) {
int childCount = rowElement.getCells().getLength();
if (index < 0 || index >= childCount) {
return null;
}
TableCellElement currentCell = null;
boolean indexInColspan = false;
int i = 0;
while (!indexInColspan) {
currentCell = rowElement.getCells().getItem(i);
// Calculate if this is the cell we are looking for
int colSpan = currentCell.getColSpan();
indexInColspan = index < colSpan + i;
// Increment by colspan to skip over hidden cells
i += colSpan;
}
return currentCell;
}
private Element getSubPartElementSpacer(SubPartArguments args) {
if ("spacer".equals(args.getType()) && args.getIndicesLength() == 1) {
return body.spacerContainer.getSubPartElement(args.getIndex(0));
} else {
return null;
}
}
@Override
@SuppressWarnings("deprecation")
public String getSubPartName(
com.google.gwt.user.client.Element subElement) {
/*
* The spacer check needs to be before table structure check, because
* (for now) the table structure will take spacer elements into account
* as well, when it shouldn't.
*/
String spacer = getSubPartNameSpacer(subElement);
if (spacer != null) {
return spacer;
}
String tableStructure = getSubPartNameTableStructure(subElement);
if (tableStructure != null) {
return tableStructure;
}
return null;
}
private String getSubPartNameTableStructure(Element subElement) {
List