summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--client/src/main/java/com/vaadin/client/connectors/grid/DetailsManagerConnector.java476
-rw-r--r--client/src/main/java/com/vaadin/client/connectors/grid/GridConnector.java91
-rw-r--r--client/src/main/java/com/vaadin/client/widget/escalator/RowContainer.java14
-rw-r--r--client/src/main/java/com/vaadin/client/widgets/Escalator.java2953
-rwxr-xr-xclient/src/main/java/com/vaadin/client/widgets/Grid.java44
-rw-r--r--server/src/main/java/com/vaadin/ui/Grid.java1
-rw-r--r--shared/src/main/java/com/vaadin/shared/ui/grid/DetailsManagerState.java8
-rw-r--r--uitest/src/main/java/com/vaadin/tests/components/treegrid/TreeGridBigDetailsManager.java37
-rw-r--r--uitest/src/main/java/com/vaadin/tests/widgetset/client/grid/EscalatorBasicClientFeaturesWidget.java68
-rw-r--r--uitest/src/main/java/com/vaadin/tests/widgetset/client/grid/EscalatorProxy.java5
-rw-r--r--uitest/src/test/java/com/vaadin/tests/components/grid/GridComponentsTest.java49
-rw-r--r--uitest/src/test/java/com/vaadin/tests/components/grid/GridScrolledToBottomTest.java13
-rw-r--r--uitest/src/test/java/com/vaadin/tests/components/grid/basicfeatures/GridColumnResizeModeTest.java8
-rw-r--r--uitest/src/test/java/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorBasicsTest.java22
-rw-r--r--uitest/src/test/java/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java52
-rw-r--r--uitest/src/test/java/com/vaadin/tests/components/treegrid/TreeGridBigDetailsManagerTest.java492
-rw-r--r--uitest/src/test/java/com/vaadin/tests/components/treegrid/TreeGridDetailsManagerTest.java22
17 files changed, 3277 insertions, 1078 deletions
diff --git a/client/src/main/java/com/vaadin/client/connectors/grid/DetailsManagerConnector.java b/client/src/main/java/com/vaadin/client/connectors/grid/DetailsManagerConnector.java
index cddaf66719..b3235952fb 100644
--- a/client/src/main/java/com/vaadin/client/connectors/grid/DetailsManagerConnector.java
+++ b/client/src/main/java/com/vaadin/client/connectors/grid/DetailsManagerConnector.java
@@ -29,6 +29,7 @@ import com.vaadin.client.ConnectorMap;
import com.vaadin.client.LayoutManager;
import com.vaadin.client.ServerConnector;
import com.vaadin.client.WidgetUtil;
+import com.vaadin.client.connectors.data.DataCommunicatorConnector;
import com.vaadin.client.data.DataChangeHandler;
import com.vaadin.client.extensions.AbstractExtensionConnector;
import com.vaadin.client.ui.layout.ElementResizeListener;
@@ -36,7 +37,9 @@ import com.vaadin.client.widget.escalator.events.SpacerIndexChangedEvent;
import com.vaadin.client.widget.escalator.events.SpacerIndexChangedHandler;
import com.vaadin.client.widget.grid.HeightAwareDetailsGenerator;
import com.vaadin.client.widgets.Grid;
+import com.vaadin.shared.Range;
import com.vaadin.shared.Registration;
+import com.vaadin.shared.data.DataCommunicatorClientRpc;
import com.vaadin.shared.ui.Connect;
import com.vaadin.shared.ui.grid.DetailsManagerState;
import com.vaadin.shared.ui.grid.GridState;
@@ -55,17 +58,12 @@ public class DetailsManagerConnector extends AbstractExtensionConnector {
/* Map for tracking which details are open on which row */
private TreeMap<Integer, String> indexToDetailConnectorId = new TreeMap<>();
- /* Boolean flag to avoid multiple refreshes */
- private boolean refreshing;
/* For listening data changes that originate from DataSource. */
private Registration dataChangeRegistration;
/* For listening spacer index changes that originate from Escalator. */
private HandlerRegistration spacerIndexChangedHandlerRegistration;
-
- /**
- * Handle for the spacer visibility change handler.
- */
- private HandlerRegistration spacerVisibilityChangeRegistration;
+ /* For listening when Escalator's visual range is changed. */
+ private HandlerRegistration rowVisibilityChangeHandlerRegistration;
private final Map<Element, ScheduledCommand> elementToResizeCommand = new HashMap<Element, Scheduler.ScheduledCommand>();
private final ElementResizeListener detailsRowResizeListener = event -> {
@@ -75,53 +73,199 @@ public class DetailsManagerConnector extends AbstractExtensionConnector {
}
};
+ /* for delayed alert if Grid needs to run or cancel pending operations */
+ private boolean delayedDetailsAddedOrUpdatedAlertTriggered = false;
+ private boolean delayedDetailsAddedOrUpdated = false;
+
+ /* for delayed re-positioning of Escalator contents to prevent gaps */
+ /* -1 is a possible spacer index in Escalator so can't be used as default */
+ private boolean delayedRepositioningTriggered = false;
+ private Integer delayedRepositioningStart = null;
+ private Integer delayedRepositioningEnd = null;
+
/* calculated when the first details row is opened */
private Double spacerCellBorderHeights = null;
+ private Range availableRowRange = Range.emptyRange();
+ private Range latestVisibleRowRange = Range.emptyRange();
+
/**
* DataChangeHandler for updating the visibility of detail widgets.
*/
private final class DetailsChangeHandler implements DataChangeHandler {
+
@Override
public void resetDataAndSize(int estimatedNewDataSize) {
- // Full clean up
- indexToDetailConnectorId.clear();
+ // No need to do anything, dataUpdated and dataAvailable take care
+ // of cleanup.
}
@Override
public void dataUpdated(int firstRowIndex, int numberOfRows) {
- for (int i = 0; i < numberOfRows; ++i) {
- int index = firstRowIndex + i;
- detachIfNeeded(index, getDetailsComponentConnectorId(index));
+ if (!getState().hasDetailsGenerator) {
+ markDetailsAddedOrUpdatedForDelayedAlertToGrid(false);
+ return;
+ }
+ Range updatedRange = Range.withLength(firstRowIndex, numberOfRows);
+
+ // NOTE: this relies on Escalator getting updated first
+ Range newVisibleRowRange = getWidget().getEscalator()
+ .getVisibleRowRange();
+
+ if (updatedRange.partitionWith(availableRowRange)[1]
+ .length() != updatedRange.length()
+ || availableRowRange.partitionWith(newVisibleRowRange)[1]
+ .length() != newVisibleRowRange.length()) {
+ // full visible range not available yet or full refresh coming
+ // up anyway, leave updating to dataAvailable
+ if (numberOfRows == 1
+ && latestVisibleRowRange.contains(firstRowIndex)) {
+ // A single details row has been opened or closed within
+ // visual range, trigger scrollTo after dataAvailable has
+ // done its thing. Do not attempt to scroll to details rows
+ // that are opened outside of the visual range.
+ Scheduler.get().scheduleFinally(() -> {
+ getParent().singleDetailsOpened(firstRowIndex);
+ // we don't know yet whether there are details or not,
+ // mark them added or updated just in case, so that
+ // the potential scrolling attempt gets triggered after
+ // another layout phase is finished
+ markDetailsAddedOrUpdatedForDelayedAlertToGrid(true);
+ });
+ }
+ return;
}
- if (numberOfRows == 1) {
+
+ // only trigger scrolling attempt if the single updated row is
+ // within existing visual range
+ boolean scrollToFirst = numberOfRows == 1
+ && latestVisibleRowRange.contains(firstRowIndex);
+
+ if (!newVisibleRowRange.equals(latestVisibleRowRange)) {
+ // update visible range
+ latestVisibleRowRange = newVisibleRowRange;
+
+ // do full refresh
+ detachOldAndRefreshCurrentDetails();
+ } else {
+ // refresh only the updated range
+ refreshDetailsVisibilityWithRange(updatedRange);
+
+ // the update may have affected details row contents and size,
+ // recalculation and triggering of any pending navigation
+ // confirmations etc. is needed
+ triggerDelayedRepositioning(firstRowIndex, numberOfRows);
+ }
+
+ if (scrollToFirst) {
+ // scroll to opened row (if it got closed instead, nothing
+ // happens)
getParent().singleDetailsOpened(firstRowIndex);
+ markDetailsAddedOrUpdatedForDelayedAlertToGrid(true);
}
- // Deferred opening of new ones.
- refreshDetails();
}
- /* The remaining methods will do a full refresh for now */
-
@Override
public void dataRemoved(int firstRowIndex, int numberOfRows) {
- refreshDetails();
+ if (!getState().hasDetailsGenerator) {
+ markDetailsAddedOrUpdatedForDelayedAlertToGrid(false);
+ return;
+ }
+ Range removing = Range.withLength(firstRowIndex, numberOfRows);
+
+ // update the handled range to only contain rows that fall before
+ // the removed range
+ latestVisibleRowRange = Range
+ .between(latestVisibleRowRange.getStart(),
+ Math.max(latestVisibleRowRange.getStart(),
+ Math.min(firstRowIndex,
+ latestVisibleRowRange.getEnd())));
+
+ // reduce the available range accordingly
+ Range[] partitions = availableRowRange.partitionWith(removing);
+ Range removedAbove = partitions[0];
+ Range removedAvailable = partitions[1];
+ availableRowRange = Range.withLength(
+ Math.max(0,
+ availableRowRange.getStart()
+ - removedAbove.length()),
+ Math.max(0, availableRowRange.length()
+ - removedAvailable.length()));
+
+ for (int i = 0; i < numberOfRows; ++i) {
+ int rowIndex = firstRowIndex + i;
+ if (indexToDetailConnectorId.containsKey(rowIndex)) {
+ String id = indexToDetailConnectorId.get(rowIndex);
+
+ ComponentConnector connector = (ComponentConnector) ConnectorMap
+ .get(getConnection()).getConnector(id);
+ if (connector != null) {
+ Element element = connector.getWidget().getElement();
+ elementToResizeCommand.remove(element);
+ getLayoutManager().removeElementResizeListener(element,
+ detailsRowResizeListener);
+ }
+ indexToDetailConnectorId.remove(rowIndex);
+ }
+ }
+ // Grid and Escalator take care of their own cleanup at removal, no
+ // need to clear details from those. Because this removal happens
+ // instantly any pending scroll to row or such should not need
+ // another attempt and unless something else causes such need the
+ // pending operations should be cleared out.
+ markDetailsAddedOrUpdatedForDelayedAlertToGrid(false);
}
@Override
public void dataAvailable(int firstRowIndex, int numberOfRows) {
- refreshDetails();
+ if (!getState().hasDetailsGenerator) {
+ markDetailsAddedOrUpdatedForDelayedAlertToGrid(false);
+ return;
+ }
+
+ // update available range
+ availableRowRange = Range.withLength(firstRowIndex, numberOfRows);
+
+ // NOTE: this relies on Escalator getting updated first
+ Range newVisibleRowRange = getWidget().getEscalator()
+ .getVisibleRowRange();
+ // only process the section that is actually available
+ newVisibleRowRange = availableRowRange
+ .partitionWith(newVisibleRowRange)[1];
+ if (newVisibleRowRange.equals(latestVisibleRowRange)) {
+ // no need to update
+ return;
+ }
+
+ // check whether the visible range has simply got shortened
+ // (e.g. by changing the default row height)
+ boolean subsectionOfOld = latestVisibleRowRange
+ .partitionWith(newVisibleRowRange)[1]
+ .length() == newVisibleRowRange.length();
+
+ // update visible range
+ latestVisibleRowRange = newVisibleRowRange;
+
+ if (subsectionOfOld) {
+ // only detach extra rows
+ detachExcludingRange(latestVisibleRowRange);
+ } else {
+ // there are completely new visible rows, full refresh
+ detachOldAndRefreshCurrentDetails();
+ }
}
@Override
public void dataAdded(int firstRowIndex, int numberOfRows) {
- refreshDetails();
+ refreshDetailsVisibilityWithRange(
+ Range.withLength(firstRowIndex, numberOfRows));
}
}
/**
* Height aware details generator for client-side Grid.
*/
+ @SuppressWarnings("deprecation")
private class CustomDetailsGenerator
implements HeightAwareDetailsGenerator {
@@ -129,8 +273,24 @@ public class DetailsManagerConnector extends AbstractExtensionConnector {
public Widget getDetails(int rowIndex) {
String id = getDetailsComponentConnectorId(rowIndex);
if (id == null) {
+ detachIfNeeded(rowIndex, id);
return null;
}
+ String oldId = indexToDetailConnectorId.get(rowIndex);
+ if (oldId != null && !oldId.equals(id)) {
+ // remove outdated connector
+ ComponentConnector connector = (ComponentConnector) ConnectorMap
+ .get(getConnection()).getConnector(oldId);
+ if (connector != null) {
+ Element element = connector.getWidget().getElement();
+ elementToResizeCommand.remove(element);
+ getLayoutManager().removeElementResizeListener(element,
+ detailsRowResizeListener);
+ }
+ }
+
+ indexToDetailConnectorId.put(rowIndex, id);
+ getWidget().setDetailsVisible(rowIndex, true);
Widget widget = getConnector(id).getWidget();
getLayoutManager().addElementResizeListener(widget.getElement(),
@@ -169,7 +329,10 @@ public class DetailsManagerConnector extends AbstractExtensionConnector {
ComponentConnector componentConnector = getConnector(id);
getLayoutManager().setNeedsMeasureRecursively(componentConnector);
- getLayoutManager().layoutNow();
+ if (!getLayoutManager().isLayoutRunning()
+ && !getConnection().getMessageHandler().isUpdatingState()) {
+ getLayoutManager().layoutNow();
+ }
Element element = componentConnector.getWidget().getElement();
if (spacerCellBorderHeights == null) {
@@ -209,20 +372,144 @@ public class DetailsManagerConnector extends AbstractExtensionConnector {
dataChangeRegistration = getWidget().getDataSource()
.addDataChangeHandler(new DetailsChangeHandler());
- // When details element is shown, remeasure it in the layout phase
- spacerVisibilityChangeRegistration = getParent().getWidget()
- .addSpacerVisibilityChangedHandler(event -> {
- if (event.isSpacerVisible()) {
- String id = indexToDetailConnectorId
- .get(event.getRowIndex());
- ComponentConnector connector = (ComponentConnector) ConnectorMap
- .get(getConnection()).getConnector(id);
- getLayoutManager()
- .setNeedsMeasureRecursively(connector);
+ rowVisibilityChangeHandlerRegistration = getWidget()
+ .addRowVisibilityChangeHandler(event -> {
+ if (getConnection().getMessageHandler().isUpdatingState()) {
+ // don't update in the middle of state changes,
+ // leave to dataAvailable
+ return;
+ }
+ Range newVisibleRowRange = event.getVisibleRowRange();
+ if (newVisibleRowRange.equals(latestVisibleRowRange)) {
+ // no need to update
+ return;
+ }
+ Range availableAndVisible = availableRowRange
+ .partitionWith(newVisibleRowRange)[1];
+ if (availableAndVisible.isEmpty()) {
+ // nothing to update yet, leave to dataAvailable
+ return;
+ }
+
+ if (!availableAndVisible.equals(latestVisibleRowRange)) {
+ // check whether the visible range has simply got
+ // shortened
+ // (e.g. by changing the default row height)
+ boolean subsectionOfOld = latestVisibleRowRange
+ .partitionWith(newVisibleRowRange)[1]
+ .length() == newVisibleRowRange
+ .length();
+
+ // update visible range
+ latestVisibleRowRange = availableAndVisible;
+
+ if (subsectionOfOld) {
+ // only detach extra rows
+ detachExcludingRange(latestVisibleRowRange);
+ } else {
+ // there are completely new visible rows, full
+ // refresh
+ detachOldAndRefreshCurrentDetails();
+ }
+ } else {
+ // refresh only the visible range, nothing to detach
+ refreshDetailsVisibilityWithRange(availableAndVisible);
+
+ // the update may have affected details row contents and
+ // size, recalculation is needed
+ triggerDelayedRepositioning(
+ availableAndVisible.getStart(),
+ availableAndVisible.length());
}
});
}
+ /**
+ * Triggers repositioning of the the contents from the first affected row
+ * downwards if any of the rows fall within the visual range. If any other
+ * delayed repositioning has been triggered within this round trip the
+ * affected range is expanded as needed. The processing is delayed to make
+ * sure all updates have time to get in, otherwise the repositioning will be
+ * calculated separately for each details row addition or removal from the
+ * server side (see
+ * {@link DataCommunicatorClientRpc#updateData(elemental.json.JsonArray)}
+ * implementation within {@link DataCommunicatorConnector}).
+ *
+ * @param firstRowIndex
+ * the index of the first changed row
+ * @param numberOfRows
+ * the number of changed rows
+ */
+ private void triggerDelayedRepositioning(int firstRowIndex,
+ int numberOfRows) {
+ if (delayedRepositioningStart == null
+ || delayedRepositioningStart > firstRowIndex) {
+ delayedRepositioningStart = firstRowIndex;
+ }
+ if (delayedRepositioningEnd == null
+ || delayedRepositioningEnd < firstRowIndex + numberOfRows) {
+ delayedRepositioningEnd = firstRowIndex + numberOfRows;
+ }
+ if (!delayedRepositioningTriggered) {
+ delayedRepositioningTriggered = true;
+
+ Scheduler.get().scheduleFinally(() -> {
+ // refresh the positions of all affected rows and those
+ // below them, unless all affected rows are outside of the
+ // visual range
+ if (getWidget().getEscalator().getVisibleRowRange()
+ .intersects(Range.between(delayedRepositioningStart,
+ delayedRepositioningEnd))) {
+ getWidget().getEscalator().getBody().updateRowPositions(
+ delayedRepositioningStart,
+ getWidget().getEscalator().getBody().getRowCount()
+ - delayedRepositioningStart);
+ }
+ delayedRepositioningTriggered = false;
+ delayedRepositioningStart = null;
+ delayedRepositioningEnd = null;
+ });
+ }
+ }
+
+ /**
+ * Makes sure that after the layout phase has finished Grid will be informed
+ * whether any details rows were added or updated. This delay is needed to
+ * allow the details row(s) to get their final size, and it's possible that
+ * more than one operation that might affect that size or details row
+ * existence will be performed (and consequently this method called) before
+ * the check can actually be made.
+ * <p>
+ * If this method is called with value {@code true} at least once within the
+ * delay phase Grid will be told to run any pending position-sensitive
+ * operations it might have in store.
+ * <p>
+ * If this method is only called with value {@code false} within the delay
+ * period Grid will be told to cancel the pending operations.
+ * <p>
+ * If this method isn't called at all, Grid won't be instructed to either
+ * trigger the pending operations or cancel them and hence they remain in a
+ * pending state.
+ *
+ * @param newOrUpdatedDetails
+ * {@code true} if the calling operation added or updated
+ * details, {@code false} otherwise
+ */
+ private void markDetailsAddedOrUpdatedForDelayedAlertToGrid(
+ boolean newOrUpdatedDetails) {
+ if (newOrUpdatedDetails) {
+ delayedDetailsAddedOrUpdated = true;
+ }
+ if (!delayedDetailsAddedOrUpdatedAlertTriggered) {
+ delayedDetailsAddedOrUpdatedAlertTriggered = true;
+ Scheduler.get().scheduleFinally(() -> {
+ getParent().detailsRefreshed(delayedDetailsAddedOrUpdated);
+ delayedDetailsAddedOrUpdatedAlertTriggered = false;
+ delayedDetailsAddedOrUpdated = false;
+ });
+ }
+ }
+
private void detachIfNeeded(int rowIndex, String id) {
if (indexToDetailConnectorId.containsKey(rowIndex)) {
if (indexToDetailConnectorId.get(rowIndex).equals(id)) {
@@ -256,8 +543,8 @@ public class DetailsManagerConnector extends AbstractExtensionConnector {
dataChangeRegistration.remove();
dataChangeRegistration = null;
- spacerVisibilityChangeRegistration.removeHandler();
spacerIndexChangedHandlerRegistration.removeHandler();
+ rowVisibilityChangeHandlerRegistration.removeHandler();
indexToDetailConnectorId.clear();
}
@@ -284,8 +571,7 @@ public class DetailsManagerConnector extends AbstractExtensionConnector {
* @return connector id; {@code null} if row or id is not found
*/
private String getDetailsComponentConnectorId(int rowIndex) {
- JsonObject row = getParent().getWidget().getDataSource()
- .getRow(rowIndex);
+ JsonObject row = getWidget().getDataSource().getRow(rowIndex);
if (row == null || !row.hasKey(GridState.JSONKEY_DETAILS_VISIBLE)
|| row.getString(GridState.JSONKEY_DETAILS_VISIBLE).isEmpty()) {
@@ -300,20 +586,29 @@ public class DetailsManagerConnector extends AbstractExtensionConnector {
}
/**
- * Schedules a deferred opening for new details components.
+ * Refreshes the existence of details components within the given range, and
+ * gives a delayed notice to Grid if any got added or updated.
*/
- private void refreshDetails() {
- if (refreshing) {
+ private void refreshDetailsVisibilityWithRange(Range rangeToRefresh) {
+ if (!getState().hasDetailsGenerator) {
+ markDetailsAddedOrUpdatedForDelayedAlertToGrid(false);
return;
}
+ boolean newOrUpdatedDetails = false;
- refreshing = true;
- Scheduler.get().scheduleFinally(this::refreshDetailsVisibility);
- }
+ // Don't update the latestVisibleRowRange class variable here, the
+ // calling method should take care of that if relevant.
+ Range currentVisibleRowRange = getWidget().getEscalator()
+ .getVisibleRowRange();
+
+ Range[] partitions = currentVisibleRowRange
+ .partitionWith(rangeToRefresh);
+
+ // only inspect the range where visible and refreshed rows overlap
+ Range intersectingRange = partitions[1];
- private void refreshDetailsVisibility() {
- boolean shownDetails = false;
- for (int i = 0; i < getWidget().getDataSource().size(); ++i) {
+ for (int i = intersectingRange.getStart(); i < intersectingRange
+ .getEnd(); ++i) {
String id = getDetailsComponentConnectorId(i);
detachIfNeeded(i, id);
@@ -324,9 +619,98 @@ public class DetailsManagerConnector extends AbstractExtensionConnector {
indexToDetailConnectorId.put(i, id);
getWidget().setDetailsVisible(i, true);
- shownDetails = true;
+ newOrUpdatedDetails = true;
+ }
+
+ markDetailsAddedOrUpdatedForDelayedAlertToGrid(newOrUpdatedDetails);
+ }
+
+ private void detachOldAndRefreshCurrentDetails() {
+ Range[] partitions = availableRowRange
+ .partitionWith(latestVisibleRowRange);
+ Range availableAndVisible = partitions[1];
+
+ detachExcludingRange(availableAndVisible);
+
+ boolean newOrUpdatedDetails = refreshRange(availableAndVisible);
+
+ markDetailsAddedOrUpdatedForDelayedAlertToGrid(newOrUpdatedDetails);
+ }
+
+ private void detachExcludingRange(Range keep) {
+ // remove all spacers that are no longer in range
+ for (Integer existingIndex : indexToDetailConnectorId.keySet()) {
+ if (!keep.contains(existingIndex)) {
+ detachDetails(existingIndex);
+ }
+ }
+ }
+
+ private boolean refreshRange(Range rangeToRefresh) {
+ // make sure all spacers that are currently in range are up to date
+ boolean newOrUpdatedDetails = false;
+ for (int i = rangeToRefresh.getStart(); i < rangeToRefresh
+ .getEnd(); ++i) {
+ int rowIndex = i;
+ if (refreshDetails(rowIndex)) {
+ newOrUpdatedDetails = true;
+ }
+ }
+ return newOrUpdatedDetails;
+ }
+
+ private void detachDetails(int rowIndex) {
+ String id = indexToDetailConnectorId.remove(rowIndex);
+ if (id != null) {
+ ComponentConnector connector = (ComponentConnector) ConnectorMap
+ .get(getConnection()).getConnector(id);
+ if (connector != null) {
+ Element element = connector.getWidget().getElement();
+ elementToResizeCommand.remove(element);
+ getLayoutManager().removeElementResizeListener(element,
+ detailsRowResizeListener);
+ }
+ }
+ getWidget().setDetailsVisible(rowIndex, false);
+ }
+
+ private boolean refreshDetails(int rowIndex) {
+ String id = getDetailsComponentConnectorId(rowIndex);
+ String oldId = indexToDetailConnectorId.get(rowIndex);
+ if ((oldId == null && id == null)
+ || (oldId != null && oldId.equals(id))) {
+ // nothing to update, move along
+ return false;
+ }
+ boolean newOrUpdatedDetails = false;
+ if (oldId != null) {
+ // Details have been hidden or updated, listeners attached
+ // to the old component need to be removed
+ ComponentConnector connector = (ComponentConnector) ConnectorMap
+ .get(getConnection()).getConnector(oldId);
+ if (connector != null) {
+ Element element = connector.getWidget().getElement();
+ elementToResizeCommand.remove(element);
+ getLayoutManager().removeElementResizeListener(element,
+ detailsRowResizeListener);
+ }
+ if (id == null) {
+ // hidden, clear reference
+ getWidget().setDetailsVisible(rowIndex, false);
+ indexToDetailConnectorId.remove(rowIndex);
+ } else {
+ // updated, replace reference
+ indexToDetailConnectorId.put(rowIndex, id);
+ newOrUpdatedDetails = true;
+ }
+ } else {
+ // new Details content, listeners will get attached to the connector
+ // when Escalator requests for the Details through
+ // CustomDetailsGenerator#getDetails(int)
+ indexToDetailConnectorId.put(rowIndex, id);
+ newOrUpdatedDetails = true;
+ getWidget().setDetailsVisible(rowIndex, true);
}
- refreshing = false;
- getParent().detailsRefreshed(shownDetails);
+ return newOrUpdatedDetails;
}
}
diff --git a/client/src/main/java/com/vaadin/client/connectors/grid/GridConnector.java b/client/src/main/java/com/vaadin/client/connectors/grid/GridConnector.java
index 62e04fdb9c..74a584aa5b 100644
--- a/client/src/main/java/com/vaadin/client/connectors/grid/GridConnector.java
+++ b/client/src/main/java/com/vaadin/client/connectors/grid/GridConnector.java
@@ -87,6 +87,61 @@ public class GridConnector extends AbstractListingConnector
private Set<Runnable> refreshDetailsCallbacks = new HashSet<>();
+ /**
+ * Server-to-client RPC implementation for GridConnector.
+ * <p>
+ * The scrolling methods must trigger the scrolling only after any potential
+ * resizing or other similar action triggered from the server side within
+ * the same round trip has had a chance to happen, so there needs to be a
+ * delay. The delay is done with <code>scheduleFinally</code> rather than
+ * <code>scheduleDeferred</code> because the latter has been known to cause
+ * flickering in Grid.
+ *
+ */
+ private class GridConnectorClientRpc implements GridClientRpc {
+ private final Grid<JsonObject> grid;
+
+ private GridConnectorClientRpc(Grid<JsonObject> grid) {
+ this.grid = grid;
+ }
+
+ @Override
+ public void scrollToRow(int row, ScrollDestination destination) {
+ Scheduler.get().scheduleFinally(() -> {
+ grid.scrollToRow(row, destination);
+ // Add details refresh listener and handle possible detail
+ // for scrolled row.
+ addDetailsRefreshCallback(() -> {
+ if (rowHasDetails(row)) {
+ grid.scrollToRow(row, destination);
+ }
+ });
+ });
+ }
+
+ @Override
+ public void scrollToStart() {
+ Scheduler.get().scheduleFinally(() -> grid.scrollToStart());
+ }
+
+ @Override
+ public void scrollToEnd() {
+ Scheduler.get().scheduleFinally(() -> {
+ grid.scrollToEnd();
+ addDetailsRefreshCallback(() -> {
+ if (rowHasDetails(grid.getDataSource().size() - 1)) {
+ grid.scrollToEnd();
+ }
+ });
+ });
+ }
+
+ @Override
+ public void recalculateColumnWidths() {
+ grid.recalculateColumnWidths();
+ }
+ }
+
private class ItemClickHandler
implements BodyClickHandler, BodyDoubleClickHandler {
@@ -217,41 +272,7 @@ public class GridConnector extends AbstractListingConnector
grid.setHeaderVisible(!grid.isHeaderVisible());
grid.setFooterVisible(!grid.isFooterVisible());
- registerRpc(GridClientRpc.class, new GridClientRpc() {
-
- @Override
- public void scrollToRow(int row, ScrollDestination destination) {
- Scheduler.get().scheduleFinally(
- () -> grid.scrollToRow(row, destination));
- // Add details refresh listener and handle possible detail for
- // scrolled row.
- addDetailsRefreshCallback(() -> {
- if (rowHasDetails(row)) {
- grid.scrollToRow(row, destination);
- }
- });
- }
-
- @Override
- public void scrollToStart() {
- Scheduler.get().scheduleFinally(() -> grid.scrollToStart());
- }
-
- @Override
- public void scrollToEnd() {
- Scheduler.get().scheduleFinally(() -> grid.scrollToEnd());
- addDetailsRefreshCallback(() -> {
- if (rowHasDetails(grid.getDataSource().size() - 1)) {
- grid.scrollToEnd();
- }
- });
- }
-
- @Override
- public void recalculateColumnWidths() {
- grid.recalculateColumnWidths();
- }
- });
+ registerRpc(GridClientRpc.class, new GridConnectorClientRpc(grid));
grid.addSortHandler(this::handleSortEvent);
grid.setRowStyleGenerator(rowRef -> {
diff --git a/client/src/main/java/com/vaadin/client/widget/escalator/RowContainer.java b/client/src/main/java/com/vaadin/client/widget/escalator/RowContainer.java
index 7c51bee86f..e8b0335a28 100644
--- a/client/src/main/java/com/vaadin/client/widget/escalator/RowContainer.java
+++ b/client/src/main/java/com/vaadin/client/widget/escalator/RowContainer.java
@@ -132,6 +132,20 @@ public interface RowContainer {
throws IndexOutOfBoundsException, IllegalArgumentException;
/**
+ * Recalculates and updates the positions of rows and spacers within the
+ * given range and ensures there is no gap below the rows if there are
+ * enough rows to fill the space. Recalculates the scrollbars for
+ * virtual viewport.
+ *
+ * @param index
+ * logical index of the first row to reposition
+ * @param numberOfRows
+ * the number of rows to reposition
+ * @since 8.9
+ */
+ public void updateRowPositions(int index, int numberOfRows);
+
+ /**
* Sets a callback function that is executed when new rows are added to
* the escalator.
*
diff --git a/client/src/main/java/com/vaadin/client/widgets/Escalator.java b/client/src/main/java/com/vaadin/client/widgets/Escalator.java
index d926e0e2be..9bee0bbeec 100644
--- a/client/src/main/java/com/vaadin/client/widgets/Escalator.java
+++ b/client/src/main/java/com/vaadin/client/widgets/Escalator.java
@@ -85,6 +85,7 @@ 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.Direction;
import com.vaadin.client.widget.escalator.ScrollbarBundle.HorizontalScrollbarBundle;
import com.vaadin.client.widget.escalator.ScrollbarBundle.VerticalScrollbarBundle;
import com.vaadin.client.widget.escalator.Spacer;
@@ -160,6 +161,9 @@ import com.vaadin.shared.util.SharedUtil;
BodyRowContainerImpl never displays large data sources
entirely in the DOM, a physical index usually has no
apparent direct relationship with its logical index.
+ This is the sectionRowIndex in TableRowElements.
+ RowIndex in TableRowElements displays the physical index
+ of all row elements, headers and footers included.
VISUAL INDEX is the index relating to the order that you
see a row in, in the browser, as it is rendered. The
@@ -183,6 +187,13 @@ import com.vaadin.shared.util.SharedUtil;
order. See BodyRowContainerImpl.DeferredDomSorter for more
about that.
+ It should be noted that the entire visual range is not
+ necessarily in view at any given time, although it should be
+ optimised to not exceed the maximum amount of rows that can
+ theoretically fit within the viewport when their associated
+ spacers have zero height, except by the two rows that are
+ required for tab navigation to work.
+
*/
/**
@@ -748,13 +759,13 @@ public class Escalator extends Widget
/*-{
var vScroll = esc.@com.vaadin.client.widgets.Escalator::verticalScrollbar;
var vScrollElem = vScroll.@com.vaadin.client.widget.escalator.ScrollbarBundle::getElement()();
-
+
var hScroll = esc.@com.vaadin.client.widgets.Escalator::horizontalScrollbar;
var hScrollElem = hScroll.@com.vaadin.client.widget.escalator.ScrollbarBundle::getElement()();
-
+
return $entry(function(e) {
var target = e.target;
-
+
// in case the scroll event was native (i.e. scrollbars were dragged, or
// the scrollTop/Left was manually modified), the bundles have old cache
// values. We need to make sure that the caches are kept up to date.
@@ -775,29 +786,29 @@ public class Escalator extends Widget
return $entry(function(e) {
var deltaX = e.deltaX ? e.deltaX : -0.5*e.wheelDeltaX;
var deltaY = e.deltaY ? e.deltaY : -0.5*e.wheelDeltaY;
-
+
// Delta mode 0 is in pixels; we don't need to do anything...
-
+
// A delta mode of 1 means we're scrolling by lines instead of pixels
// We need to scale the number of lines by the default line height
if (e.deltaMode === 1) {
var brc = esc.@com.vaadin.client.widgets.Escalator::body;
deltaY *= brc.@com.vaadin.client.widgets.Escalator.AbstractRowContainer::getDefaultRowHeight()();
}
-
+
// Other delta modes aren't supported
if ((e.deltaMode !== undefined) && (e.deltaMode >= 2 || e.deltaMode < 0)) {
var msg = "Unsupported wheel delta mode \"" + e.deltaMode + "\"";
-
+
// Print warning message
esc.@com.vaadin.client.widgets.Escalator::logWarning(*)(msg);
}
-
+
// IE8 has only delta y
if (isNaN(deltaY)) {
deltaY = -0.5*e.wheelDelta;
}
-
+
@com.vaadin.client.widgets.Escalator.JsniUtil::moveScrollFromEvent(*)(esc, deltaX, deltaY, e);
});
}-*/;
@@ -1120,26 +1131,8 @@ public class Escalator extends Widget
public void scrollToRow(final int rowIndex,
final ScrollDestination destination, final double padding) {
-
- final double targetStartPx = (body.getDefaultRowHeight() * rowIndex)
- + body.spacerContainer
- .getSpacerHeightsSumUntilIndex(rowIndex);
- final double targetEndPx = targetStartPx
- + body.getDefaultRowHeight();
-
- final double viewportStartPx = getScrollTop();
- final double viewportEndPx = viewportStartPx
- + body.getHeightOfSection();
-
- final double scrollTop = getScrollPos(destination, targetStartPx,
- targetEndPx, viewportStartPx, viewportEndPx, padding);
-
- /*
- * note that it doesn't matter if the scroll would go beyond the
- * content, since the browser will adjust for that, and everything
- * falls into line accordingly.
- */
- setScrollTop(scrollTop);
+ body.scrollToRowSpacerOrBoth(rowIndex, destination, padding,
+ ScrollType.ROW);
}
}
@@ -1411,6 +1404,8 @@ public class Escalator extends Widget
*
* @param tr
* the row element to remove.
+ * @param logicalRowIndex
+ * logical index of the row that is to be removed
*/
protected void paintRemoveRow(final TableRowElement tr,
final int logicalRowIndex) {
@@ -2117,10 +2112,16 @@ public class Escalator extends Widget
positions.remove(tr);
}
+ /**
+ * Triggers delayed auto-detection of default row height if it hasn't
+ * been set by that point and the Escalator is both attached and
+ * displayed.
+ */
public void autodetectRowHeightLater() {
autodetectingRowHeightLater = true;
Scheduler.get().scheduleFinally(() -> {
- if (defaultRowHeightShouldBeAutodetected && isAttached()) {
+ if (defaultRowHeightShouldBeAutodetected && isAttached()
+ && WidgetUtil.isDisplayed(getElement())) {
autodetectRowHeightNow();
defaultRowHeightShouldBeAutodetected = false;
}
@@ -2143,9 +2144,15 @@ public class Escalator extends Widget
}
}
+ /**
+ * Auto-detect row height immediately, if possible. If Escalator isn't
+ * attached and displayed yet, auto-detecting cannot be performed
+ * correctly. In such cases auto-detecting is left to wait for these
+ * conditions to change, and will be performed when they do.
+ */
public void autodetectRowHeightNow() {
- if (!isAttached()) {
- // Run again when attached
+ if (!isAttached() || !WidgetUtil.isDisplayed(getElement())) {
+ // Run again when attached and displayed
defaultRowHeightShouldBeAutodetected = true;
return;
}
@@ -2183,8 +2190,8 @@ public class Escalator extends Widget
/*
* 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.
+ * (a row or spacer) and ensure the element is inside the dom
+ * hierarchy of the root element. If not, return null.
*/
if (root == element || element.getParentElement() == root
|| !root.isOrHasChild(element)) {
@@ -2317,6 +2324,9 @@ public class Escalator extends Widget
* @return the logical index of the given element
*/
public int getLogicalRowIndex(final TableRowElement tr) {
+ // Note: BodyRowContainerImpl overrides this behaviour, since the
+ // physical index and logical index don't match there. For header
+ // and footer there is a match.
return tr.getSectionRowIndex();
};
@@ -2599,6 +2609,17 @@ public class Escalator extends Widget
*/
private Consumer<List<TableRowElement>> newEscalatorRowCallback;
+ /**
+ * Set the logical index of the first dom row in visual order.
+ * <p>
+ * NOTE: this is not necessarily the first dom row in the dom tree, just
+ * the one positioned to the top with CSS. See maintenance notes at the
+ * top of this class for further information.
+ *
+ * @param topRowLogicalIndex
+ * logical index of the first dom row in visual order, might
+ * not match the dom tree order
+ */
private void setTopRowLogicalIndex(int topRowLogicalIndex) {
if (LogConfiguration.loggingIsEnabled(Level.INFO)) {
Logger.getLogger("Escalator.BodyRowContainer")
@@ -2618,10 +2639,33 @@ public class Escalator extends Widget
this.topRowLogicalIndex = topRowLogicalIndex;
}
+ /**
+ * Returns the logical index of the first dom row in visual order. This
+ * also gives the offset between the logical and visual indexes.
+ * <p>
+ * NOTE: this is not necessarily the first dom row in the dom tree, just
+ * the one positioned to the top with CSS. See maintenance notes at the
+ * top of this class for further information.
+ *
+ * @return logical index of the first dom row in visual order, might not
+ * match the dom tree order
+ */
public int getTopRowLogicalIndex() {
return topRowLogicalIndex;
}
+ /**
+ * Updates the logical index of the first dom row in visual order with
+ * the given difference.
+ * <p>
+ * NOTE: this is not necessarily the first dom row in the dom tree, just
+ * the one positioned to the top with CSS. See maintenance notes at the
+ * top of this class for further information.
+ *
+ * @param diff
+ * the amount to increase or decrease the logical index of
+ * the first dom row in visual order
+ */
private void updateTopRowLogicalIndex(int diff) {
setTopRowLogicalIndex(topRowLogicalIndex + diff);
}
@@ -2690,6 +2734,8 @@ public class Escalator extends Widget
private final SpacerContainer spacerContainer = new SpacerContainer();
+ private boolean insertingOrRemoving = false;
+
public BodyRowContainerImpl(final TableSectionElement bodyElement) {
super(bodyElement);
}
@@ -2724,131 +2770,265 @@ public class Escalator extends Widget
// TODO [[mpixscroll]]
final double scrollTop = tBodyScrollTop;
- final double viewportOffset = topElementPosition - scrollTop;
+ final double sectionHeight = getHeightOfSection();
/*
- * TODO [[optimize]] this if-else can most probably be refactored
- * into a neater block of code
+ * Calculate how the visual range is situated in relation to the
+ * viewport. Negative value means part of visual range is hidden
+ * above or below the viewport, positive value means there is a gap
+ * at the top or the bottom of the viewport, zero means exact match.
+ * If there is a gap, some rows that are out of view may need to be
+ * recycled from the opposite end.
*/
+ final double viewportOffsetTop = topElementPosition - scrollTop;
+ final double viewportOffsetBottom = scrollTop + sectionHeight
+ - getRowTop(
+ getTopRowLogicalIndex() + visualRowOrder.size());
- 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);
+ /*
+ * You can only scroll far enough to leave a gap if visualRowOrder
+ * contains a maximal amount of rows and there is at least one more
+ * outside of the visual range. Consequently there can only be a gap
+ * in one end of the viewport at a time.
+ */
+ if (viewportOffsetTop > 0 || (viewportOffsetTop == 0
+ && getTopRowLogicalIndex() > 0)) {
+ /*
+ * Scrolling up. Either there's empty room on top, or there
+ * should be a buffer row for tab navigation on top, but there
+ * isn't.
+ */
+ recycleRowsUpOnScroll(viewportOffsetTop);
rowsWereMoved = true;
- } else if (viewportOffset + nextRowBottomOffset <= 0) {
+ } else if ((viewportOffsetBottom > 0
+ && (viewportOffsetTop + nextRowBottomOffset <= 0))
+ || (viewportOffsetBottom == 0 && (getTopRowLogicalIndex()
+ + visualRowOrder.size() < getRowCount() - 2))) {
/*
+ * Scrolling down. Either there's empty room at the bottom and
* the viewport has been scrolled more than the topmost visual
- * row.
+ * row, or there should be a buffer row at the bottom to ensure
+ * tab navigation works, but there isn't.
*/
+ recycleRowsDownOnScroll(topElementPosition, scrollTop);
+
+ // Moving rows may have removed more spacers and created another
+ // gap, this time the scroll position needs adjusting. The last
+ // row within visual range should be just below the viewport as
+ // a buffer for helping with tab navigation, unless it's the
+ // last row altogether.
+ int lastRowInVisualRange = getTopRowLogicalIndex()
+ + visualRowOrder.size() - 1;
+ double expectedBottom = getRowTop(lastRowInVisualRange);
+ if (lastRowInVisualRange == getRowCount() - 1) {
+ expectedBottom += getDefaultRowHeight() + spacerContainer
+ .getSpacerHeight(lastRowInVisualRange);
+ }
+ if (expectedBottom < scrollTop + sectionHeight) {
+ double expectedTop = Math.max(0,
+ expectedBottom - sectionHeight);
+ setBodyScrollPosition(tBodyScrollLeft, expectedTop);
+ setScrollTop(expectedTop);
+ }
- double rowPx = getRowHeightsSumBetweenPx(topElementPosition,
- scrollTop);
-
- int originalRowsToMove = (int) (rowPx / getDefaultRowHeight());
- int rowsToMove = Math.min(originalRowsToMove,
- visualRowOrder.size());
+ rowsWereMoved = true;
+ }
- 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);
- }
+ if (rowsWereMoved) {
+ fireRowVisibilityChangeEvent();
- /*
- * 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();
+ // schedule updating of the physical indexes
+ domSorter.reschedule();
+ }
+ }
- // 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;
- }
+ /**
+ * Recycling rows up for {@link #updateEscalatorRowsOnScroll()}.
+ * <p>
+ * NOTE: This method should not be called directly from anywhere else.
+ *
+ * @param viewportOffsetTop
+ */
+ private void recycleRowsUpOnScroll(double viewportOffsetTop) {
+ /*
+ * We can ignore spacers here, because we keep enough rows within
+ * the visual range to fill the viewport completely whether or not
+ * any spacers are shown. There is a small tradeoff of having some
+ * rows rendered even if they are outside of the viewport, but this
+ * simplifies the handling significantly (we can't know what height
+ * any individual spacer has before it has been rendered, which
+ * happens with a delay) and keeps the visual range size stable
+ * while scrolling. Consequently, even if there are spacers within
+ * the current visual range, repositioning this many rows won't
+ * cause us to run out of rows at the bottom.
+ *
+ * The viewportOffsetTop is positive and we round up, and
+ * visualRowOrder can't be empty since we are scrolling, so there is
+ * always going to be at least one row to move. There should also be
+ * one buffer row that actually falls outside of the viewport, in
+ * order to ensure that tabulator navigation works if the rows have
+ * components in them. The buffer row is only needed if filling the
+ * gap doesn't bring us to the top row already.
+ */
+ int rowsToFillTheGap = (int) Math
+ .ceil(viewportOffsetTop / getDefaultRowHeight());
+ // ensure we don't try to move more rows than are available
+ // above
+ rowsToFillTheGap = Math.min(rowsToFillTheGap,
+ getTopRowLogicalIndex());
+ // add the buffer row if there is room for it
+ if (rowsToFillTheGap < getTopRowLogicalIndex()) {
+ ++rowsToFillTheGap;
+ }
+ // we may have scrolled up past all the rows and beyond, can
+ // only recycle as many rows as we have
+ int rowsToRecycle = Math.min(rowsToFillTheGap,
+ visualRowOrder.size());
+
+ // select the rows to recycle from the end of the visual range
+ int end = visualRowOrder.size();
+ int start = end - rowsToRecycle;
- /*
- * 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));
+ /*
+ * Calculate the logical index for insertion point based on how many
+ * rows would be needed to fill the gap. Because we are recycling
+ * rows to the top the insertion index will also be the new top row
+ * logical index.
+ */
+ int newTopRowLogicalIndex = getTopRowLogicalIndex()
+ - rowsToFillTheGap;
- moveAndUpdateEscalatorRows(Range.between(0, rowsToMove),
- targetVisualIndex, logicalRowIndex);
+ // recycle the rows and move them to their new positions
+ moveAndUpdateEscalatorRows(Range.between(start, end), 0,
+ newTopRowLogicalIndex);
- 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);
+ setTopRowLogicalIndex(newTopRowLogicalIndex);
+ }
- /*
- * 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);
- }
+ /**
+ * Recycling rows down for {@link #updateEscalatorRowsOnScroll()}.
+ * <p>
+ * NOTE: This method should not be called directly from anywhere else.
+ *
+ * @param topElementPosition
+ * @param scrollTop
+ */
+ private void recycleRowsDownOnScroll(double topElementPosition,
+ double scrollTop) {
+ /*
+ * It's better to have any extra rows below than above, so move as
+ * many of them as possible regardless of how many are needed to
+ * fill the gap, as long as one buffer row remains at the top. It
+ * should not be possible to scroll down enough to create a gap
+ * without it being possible to recycle rows to fill the gap, so
+ * viewport itself doesn't need adjusting no matter what.
+ */
- final int naiveNewLogicalIndex = getTopRowLogicalIndex()
- + originalRowsToMove;
- final int maxLogicalIndex = getRowCount()
- - visualRowOrder.size();
- setTopRowLogicalIndex(
- Math.min(naiveNewLogicalIndex, maxLogicalIndex));
+ // we already have the rows and spacers here and we don't want
+ // to recycle rows that are going to stay visible, so the
+ // spacers have to be taken into account
+ double extraRowPxAbove = getRowHeightsSumBetweenPxExcludingSpacers(
+ topElementPosition, scrollTop);
+
+ // how many rows fit within that extra space and can be
+ // recycled, rounded towards zero to avoid moving any partially
+ // visible rows
+ int rowsToCoverTheExtra = (int) Math
+ .floor(extraRowPxAbove / getDefaultRowHeight());
+ // leave one to ensure there is a buffer row to help with tab
+ // navigation
+ if (rowsToCoverTheExtra > 0) {
+ --rowsToCoverTheExtra;
+ }
+ /*
+ * Don't move more rows than there are to move, but also don't move
+ * more rows than should exist at the bottom. However, it's not
+ * possible to scroll down beyond available rows, so there is always
+ * at least one row to recycle.
+ */
+ int rowsToRecycle = Math.min(
+ Math.min(rowsToCoverTheExtra, visualRowOrder.size()),
+ getRowCount() - getTopRowLogicalIndex()
+ - visualRowOrder.size());
+
+ // are only some of the rows getting recycled instead of all
+ // of them
+ boolean partialMove = rowsToRecycle < visualRowOrder.size();
+
+ // calculate the logical index where the rows should be moved
+ int logicalTargetIndex;
+ if (partialMove) {
+ /*
+ * We scroll so little that we can just keep adding the rows
+ * immediately below the current escalator.
+ */
+ logicalTargetIndex = getTopRowLogicalIndex()
+ + visualRowOrder.size();
+ } else {
+ /*
+ * Since all escalator rows are getting recycled all spacers are
+ * going to get removed and the calculations have to ignore the
+ * spacers again in order to figure out which rows are to be
+ * displayed. In practice we may end up scrolling further down
+ * than the scroll position indicated initially as the spacers
+ * that get removed give room for more rows than expected.
+ *
+ * We can rely on calculations here because there won't be any
+ * old rows left to end up mismatched with.
+ */
+ logicalTargetIndex = (int) Math
+ .floor(scrollTop / getDefaultRowHeight());
- rowsWereMoved = true;
+ /*
+ * Make sure we don't try to move rows below the actual row
+ * count, even if some of the rows end up hidden at the top as a
+ * result. This won't leave us with any old rows in any case,
+ * because we already checked earlier that there is room to
+ * recycle all the rows. It's only a question of how the new
+ * visual range gets positioned in relation to the viewport.
+ */
+ if (logicalTargetIndex
+ + visualRowOrder.size() > getRowCount()) {
+ logicalTargetIndex = getRowCount() - visualRowOrder.size();
+ }
}
- if (rowsWereMoved) {
- fireRowVisibilityChangeEvent();
- domSorter.reschedule();
+ /*
+ * Recycle the rows and move them to their new positions. Since we
+ * are moving the viewport downwards, the visual target index is
+ * always at the bottom and matches the length of the visual range.
+ * 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".
+ */
+ moveAndUpdateEscalatorRows(Range.between(0, rowsToRecycle),
+ visualRowOrder.size(), logicalTargetIndex);
+
+ // top row logical index needs to be updated differently
+ // depending on which update strategy was used, since the rows
+ // are being moved down
+ if (partialMove) {
+ // move down by the amount of recycled rows
+ updateTopRowLogicalIndex(rowsToRecycle);
+ } else {
+ // the insertion index is the new top row logical index
+ setTopRowLogicalIndex(logicalTargetIndex);
}
}
- private double getRowHeightsSumBetweenPx(double y1, double y2) {
+ /**
+ * Calculates how much of the given range contains only rows with
+ * spacers excluded.
+ *
+ * @param y1
+ * start position
+ * @param y2
+ * end position
+ * @return position difference excluding any space taken up by spacers
+ */
+ private double getRowHeightsSumBetweenPxExcludingSpacers(double y1,
+ double y2) {
assert y1 < y2 : "y1 must be smaller than y2";
double viewportPx = y2 - y1;
@@ -2859,14 +3039,11 @@ public class Escalator extends Widget
return viewportPx - spacerPx;
}
- private int getLogicalRowIndex(final double px) {
- double rowPx = px - spacerContainer.getSpacerHeightsSumUntilPx(px);
- return (int) (rowPx / getDefaultRowHeight());
- }
-
@Override
public void insertRows(int index, int numberOfRows) {
+ insertingOrRemoving = true;
super.insertRows(index, numberOfRows);
+ insertingOrRemoving = false;
if (heightMode == HeightMode.UNDEFINED) {
setHeightByRows(getRowCount());
@@ -2875,7 +3052,9 @@ public class Escalator extends Widget
@Override
public void removeRows(int index, int numberOfRows) {
+ insertingOrRemoving = true;
super.removeRows(index, numberOfRows);
+ insertingOrRemoving = false;
if (heightMode == HeightMode.UNDEFINED) {
setHeightByRows(getRowCount());
@@ -2885,120 +3064,364 @@ public class Escalator extends Widget
@Override
protected void paintInsertRows(final int index,
final int numberOfRows) {
- if (numberOfRows == 0) {
+ assert index >= 0
+ && index < getRowCount() : "Attempting to insert a row "
+ + "outside of the available range.";
+ assert numberOfRows > 0 : "Attempting to insert a non-positive "
+ + "amount of rows, something must be wrong.";
+
+ if (numberOfRows <= 0) {
return;
}
+ /*
+ * NOTE: this method handles and manipulates logical, visual, and
+ * physical indexes a lot. If you don't remember what those mean and
+ * how they relate to each other, see the top of this class for
+ * Maintenance Notes.
+ *
+ * At the beginning of this method the logical index of the data
+ * provider has already been updated to include the new rows, but
+ * visual and physical indexes have not, nor has the spacer indexing
+ * been updated, and the topRowLogicalIndex may be out of date as
+ * well.
+ */
- spacerContainer.shiftSpacersByRows(index, numberOfRows);
+ // top of visible area before any rows are actually added
+ double scrollTop = getScrollTop();
+
+ // logical index of the first row within the visual range before any
+ // rows are actually added
+ int oldTopRowLogicalIndex = getTopRowLogicalIndex();
+
+ // length of the visual range before any rows are actually added
+ int oldVisualRangeLength = visualRowOrder.size();
/*
- * TODO: this method should probably only add physical rows, and not
- * populate them - let everything be populated as appropriate by the
- * logic that follows.
+ * If there is room for more dom rows within the maximum visual
+ * range, add them. Calling this method repositions all the rows and
+ * spacers below the insertion point and updates the spacer indexes
+ * accordingly.
*
- * This also would lead to the fact that paintInsertRows wouldn't
- * need to return anything.
+ * TODO: Details rows should be added and populated here, since they
+ * have variable heights and affect the position calculations.
+ * Currently that's left to be triggered at the end and with a
+ * delay. If any new spacers exist, everything below them is going
+ * to be repositioned again for every spacer addition.
*/
final List<TableRowElement> addedRows = fillAndPopulateEscalatorRowsIfNeeded(
- index, numberOfRows);
+ index - oldTopRowLogicalIndex, index, numberOfRows);
+
+ // is the insertion point for new rows below visual range (viewport
+ // is irrelevant)
+ final boolean newRowsInsertedBelowVisualRange = index >= oldVisualRangeLength
+ + oldTopRowLogicalIndex;
+
+ // is the insertion point for new rows above initial visual range
+ final boolean newRowsInsertedAboveVisualRange = index <= oldTopRowLogicalIndex;
+
+ // is the insertion point for new rows above viewport
+ final boolean newRowsInsertedAboveCurrentViewport = getRowTop(
+ index) < scrollTop;
+
+ if (newRowsInsertedBelowVisualRange) {
+ /*
+ * There is no change to scroll position, and all other changes
+ * to positioning and indexing are out of visual range or
+ * already done (if addedRows is not empty).
+ */
+ } else if (newRowsInsertedAboveVisualRange && addedRows.isEmpty()
+ && newRowsInsertedAboveCurrentViewport) {
+ /*
+ * This section can only be reached if the insertion point is
+ * above the visual range, the visual range already covers a
+ * maximal amount of rows, and we are scrolled down enough that
+ * the top row is either partially or completely hidden. The
+ * last two points happen by default if the first row of the
+ * visual range has any other logical index than zero. Any other
+ * use cases involving the top row within the visual range need
+ * different handling.
+ */
+ paintInsertRowsAboveViewPort(index, numberOfRows,
+ oldTopRowLogicalIndex);
+ } else if (newRowsInsertedAboveCurrentViewport) {
+ /*
+ * Rows were inserted within the visual range but above the
+ * viewport. This includes the use case where the insertion
+ * point is just above the visual range and we are scrolled down
+ * a bit but the visual range doesn't have maximal amount of
+ * rows yet (can only happen with spacers in play), so more rows
+ * were added to the visual range but no rows need to be
+ * recycled.
+ */
+ paintInsertRowsWithinVisualRangeButAboveViewport(index,
+ numberOfRows, oldTopRowLogicalIndex, addedRows.size());
+ } else {
+ /*
+ * Rows added within visual range and either within or below the
+ * viewport. Recycled rows come from the END of the visual
+ * range.
+ */
+ paintInsertRowsWithinVisualRangeAndWithinOrBelowViewport(index,
+ numberOfRows, oldTopRowLogicalIndex, addedRows.size());
+ }
/*
- * insertRows will always change the number of rows - update the
- * scrollbar sizes.
+ * Calling insertRows will always change the number of rows - update
+ * the scrollbar sizes. This calculation isn't affected by actual
+ * dom rows amount or contents except for spacer heights. Spacers
+ * that don't fit the visual range are considered to have no height
+ * and might affect scrollbar calculations aversely, but that can't
+ * be avoided since they have unknown and variable heights.
*/
scroller.recalculateScrollbarsForVirtualViewport();
+ }
+
+ /**
+ * Row insertion handling for {@link #paintInsertRows(int, int)} when
+ * the range will be inserted above the visual range.
+ * <p>
+ * NOTE: This method should not be called directly from anywhere else.
+ *
+ * @param index
+ * @param numberOfRows
+ * @param oldTopRowLogicalIndex
+ */
+ private void paintInsertRowsAboveViewPort(int index, int numberOfRows,
+ int oldTopRowLogicalIndex) {
+ /*
+ * Because there is no need to expand the visual range, no row or
+ * spacer contents get updated. All rows, spacers, and scroll
+ * position simply need to be shifted down accordingly and the
+ * spacer indexes need updating.
+ */
+ spacerContainer.updateSpacerIndexesForRowAndAfter(index,
+ oldTopRowLogicalIndex + visualRowOrder.size(),
+ numberOfRows);
+
+ // height of a single row
+ double defaultRowHeight = getDefaultRowHeight();
+
+ // height of new rows, out of visual range so spacers assumed to
+ // have no height
+ double newRowsHeight = numberOfRows * defaultRowHeight;
+
+ // update the positions
+ moveViewportAndContent(index, newRowsHeight, newRowsHeight,
+ newRowsHeight);
+
+ // top row logical index moves down by the number of new rows
+ updateTopRowLogicalIndex(numberOfRows);
+ }
+
+ /**
+ * Row insertion handling for {@link #paintInsertRows(int, int)} when
+ * the range will be inserted within the visual range above the
+ * viewport.
+ * <p>
+ * NOTE: This method should not be called directly from anywhere else.
+ *
+ * @param index
+ * @param numberOfRows
+ * @param oldTopRowLogicalIndex
+ * @param addedRowCount
+ */
+ private void paintInsertRowsWithinVisualRangeButAboveViewport(int index,
+ int numberOfRows, int oldTopRowLogicalIndex,
+ int addedRowCount) {
+ /*
+ * Unless we are scrolled all the way to the top the visual range is
+ * always out of view because we need a buffer row for tabulator
+ * navigation. Depending on the scroll position and spacers there
+ * might even be several rendered rows above the viewport,
+ * especially when we are scrolled all the way to the bottom.
+ *
+ * Even though the new rows will be initially out of view they still
+ * need to be correctly populated and positioned. Their contents
+ * won't be refreshed if they become visible later on (e.g. when a
+ * spacer gets hidden, which causes more rows to fit within the
+ * viewport) because they are expected to be already up to date.
+ *
+ * Note that it's not possible to insert content so that it's
+ * partially visible at the top. A partially visible row at top will
+ * still be the exact same partially visible row after the
+ * insertion, no matter which side of that row the new content gets
+ * inserted to. This section handles the use case where the new
+ * content is inserted above the partially visible row.
+ *
+ * Because the insertion point is out of view above the viewport,
+ * the only thing that should change for the end user visually is
+ * the scroll handle, which gets a new position and possibly turns a
+ * bit smaller if a lot of rows got inserted.
+ *
+ * From a technical point of view this also means that any rows that
+ * might need to get recycled should be taken from the BEGINNING of
+ * the visual range, above the insertion point. There might still be
+ * some "extra" rows below the viewport as well, but those should be
+ * left alone. They are going to be needed where they are if any
+ * spacers get closed or reduced in size.
+ *
+ * On a practical level we need to tweak the virtual viewport --
+ * scroll handle positions, row and spacer positions, and ensure the
+ * scroll area height is calculated correctly. Viewport should
+ * remain in a fixed position in relation to the existing rows and
+ * display no new rows. If any rows get recycled and have spacers
+ * either before or after the update the height of those spacers
+ * affects the position calculations.
+ *
+ * Insertion point can be anywhere from just before the previous
+ * first row of the visual range to just before the first actually
+ * visible row. The insertion shifts down the content below
+ * insertion point, which excludes any dom rows that remain above
+ * the insertion point after recycling is finished. After the rows
+ * below insertion point have been moved the viewport needs to be
+ * shifted down a similar amount to regain its old relative position
+ * again.
+ *
+ * The visual range only ever contains at most as many rows as would
+ * fit within the viewport without any spacers with one extra row on
+ * both at the top and at the bottom as buffer rows, so the amount
+ * of rows that needs to be checked is always reasonably limited.
+ */
+ // insertion index within the visual range
+ int visualTargetIndex = index - oldTopRowLogicalIndex;
+
+ // how many dom rows before insertion point versus how many new
+ // rows didn't get their own dom rows -- smaller amount
+ // determines how many rows can and need to be recycled
+ int rowsToUpdate = Math.min(visualTargetIndex,
+ numberOfRows - addedRowCount);
+
+ // height of a single row
+ double defaultRowHeight = getDefaultRowHeight();
+
+ boolean rowVisibilityChanged = false;
+ if (rowsToUpdate > 0) {
+ // recycle the rows and update the positions, adjust
+ // logical index for inserted rows that won't fit within
+ // visual range
+ int logicalIndex = index + numberOfRows - rowsToUpdate;
+ if (visualTargetIndex > 0) {
+ // move after any added dom rows
+ moveAndUpdateEscalatorRows(Range.between(0, rowsToUpdate),
+ visualTargetIndex + addedRowCount, logicalIndex);
+ } else {
+ // move before any added dom rows
+ moveAndUpdateEscalatorRows(Range.between(0, rowsToUpdate),
+ visualTargetIndex, logicalIndex);
+ }
+
+ // adjust viewport down to maintain the initial position
+ double newRowsHeight = numberOfRows * defaultRowHeight;
+ double newSpacerHeights = spacerContainer
+ .getSpacerHeightsSumUntilIndex(
+ logicalIndex + rowsToUpdate)
+ - spacerContainer.getSpacerHeightsSumUntilIndex(index);
- double spacerHeightsSumUntilIndex = spacerContainer
- .getSpacerHeightsSumUntilIndex(index);
- final boolean addedRowsAboveCurrentViewport = index
- * getDefaultRowHeight()
- + spacerHeightsSumUntilIndex < getScrollTop();
- final boolean addedRowsBelowCurrentViewport = index
- * getDefaultRowHeight()
- + spacerHeightsSumUntilIndex > getScrollTop()
- + getHeightOfSection();
-
- if (addedRowsAboveCurrentViewport) {
/*
- * We need to tweak the virtual viewport (scroll handle
- * positions, table "scroll position" and row locations), but
- * without re-evaluating any rows.
+ * FIXME: spacers haven't been added yet and they can cause
+ * escalator contents to shift after the fact in a way that
+ * can't be countered for here.
+ *
+ * FIXME: verticalScrollbar internal state causes this update to
+ * fail partially and the next attempt at scrolling causes
+ * things to jump.
+ *
+ * Couldn't find a quick fix to either problem and this use case
+ * is somewhat marginal so left them here for now.
*/
+ moveViewportAndContent(null, 0, 0,
+ newSpacerHeights + newRowsHeight);
- final double yDelta = numberOfRows * getDefaultRowHeight();
- moveViewportAndContent(yDelta);
- updateTopRowLogicalIndex(numberOfRows);
- } else if (addedRowsBelowCurrentViewport) {
- // NOOP, we already recalculated scrollbars.
+ rowVisibilityChanged = true;
} else {
- // some rows were added inside the current viewport
- final int unupdatedLogicalStart = index + addedRows.size();
- final int visualOffset = getLogicalRowIndex(
- visualRowOrder.getFirst());
+ // no rows to recycle but update the spacer indexes
+ spacerContainer.updateSpacerIndexesForRowAndAfter(index,
+ index + numberOfRows - addedRowCount,
+ numberOfRows - addedRowCount);
+
+ double newRowsHeight = numberOfRows * defaultRowHeight;
+ if (addedRowCount > 0) {
+ // update the viewport, rows and spacers were
+ // repositioned already by the method for adding dom
+ // rows
+ moveViewportAndContent(null, 0, 0, newRowsHeight);
+
+ rowVisibilityChanged = true;
+ } else {
+ // all changes are actually above the viewport after
+ // all, update all positions
+ moveViewportAndContent(index, newRowsHeight, newRowsHeight,
+ newRowsHeight);
+ }
+ }
+ if (numberOfRows > addedRowCount) {
/*
- * At this point, we have added new escalator rows, if so
- * needed.
- *
- * If more rows were added than the new escalator rows can
- * account for, we need to start to spin the escalator to update
- * the remaining rows as well.
+ * If there are more new rows than how many new dom rows got
+ * added, the top row logical index necessarily gets shifted
+ * down by that difference because recycling doesn't replace any
+ * logical rows, just shifts them off the visual range, and the
+ * inserted rows that don't fit to the visual range also push
+ * the other rows down. If every new row got new dom rows as
+ * well the top row logical index doesn't change, because the
+ * insertion point was within the visual range.
*/
- final int rowsStillNeeded = numberOfRows - addedRows.size();
-
- if (rowsStillNeeded > 0) {
- final Range unupdatedVisual = convertToVisual(
- Range.withLength(unupdatedLogicalStart,
- rowsStillNeeded));
- final int end = getDomRowCount();
- final int start = end - unupdatedVisual.length();
- final int visualTargetIndex = unupdatedLogicalStart
- - visualOffset;
- moveAndUpdateEscalatorRows(Range.between(start, end),
- visualTargetIndex, unupdatedLogicalStart);
-
- // move the surrounding rows to their correct places.
- double rowTop = (unupdatedLogicalStart + (end - start))
- * getDefaultRowHeight();
-
- // TODO: Get rid of this try/catch block by fixing the
- // underlying issue. The reason for this erroneous behavior
- // might be that Escalator actually works 'by mistake', and
- // the order of operations is, in fact, wrong.
- try {
- final ListIterator<TableRowElement> i = visualRowOrder
- .listIterator(
- visualTargetIndex + (end - start));
-
- int logicalRowIndexCursor = unupdatedLogicalStart;
- while (i.hasNext()) {
- rowTop += spacerContainer
- .getSpacerHeight(logicalRowIndexCursor++);
-
- final TableRowElement tr = i.next();
- setRowPosition(tr, 0, rowTop);
- rowTop += getDefaultRowHeight();
- }
- } catch (Exception e) {
- Logger logger = getLogger();
- logger.warning(
- "Ignored out-of-bounds row element access");
- logger.warning("Escalator state: start=" + start
- + ", end=" + end + ", visualTargetIndex="
- + visualTargetIndex + ", visualRowOrder.size()="
- + visualRowOrder.size());
- logger.warning(e.toString());
- }
- }
+ updateTopRowLogicalIndex(numberOfRows - addedRowCount);
+ }
+ if (rowVisibilityChanged) {
fireRowVisibilityChangeEvent();
+ }
+ if (rowsToUpdate > 0) {
+ // update the physical index
+ sortDomElements();
+ }
+ }
+
+ /**
+ * Row insertion handling for {@link #paintInsertRows(int, int)} when
+ * the range will be inserted within the visual range either within or
+ * below the viewport.
+ * <p>
+ * NOTE: This method should not be called directly from anywhere else.
+ *
+ * @param index
+ * @param numberOfRows
+ * @param oldTopRowLogicalIndex
+ * @param addedRowCount
+ */
+ private void paintInsertRowsWithinVisualRangeAndWithinOrBelowViewport(
+ int index, int numberOfRows, int oldTopRowLogicalIndex,
+ int addedRowCount) {
+ // insertion index within the visual range
+ int visualIndex = index - oldTopRowLogicalIndex;
+
+ // how many dom rows after insertion point versus how many new
+ // rows to add -- smaller amount determines how many rows can or
+ // need to be recycled, excluding the rows that already got new
+ // dom rows
+ int rowsToUpdate = Math.max(
+ Math.min(visualRowOrder.size() - visualIndex, numberOfRows)
+ - addedRowCount,
+ 0);
+
+ if (rowsToUpdate > 0) {
+ moveAndUpdateEscalatorRows(
+ Range.between(visualRowOrder.size() - rowsToUpdate,
+ visualRowOrder.size()),
+ visualIndex + addedRowCount, index + addedRowCount);
+
+ fireRowVisibilityChangeEvent();
+
+ // update the physical index
sortDomElements();
}
}
/**
* Move escalator rows around, and make sure everything gets
- * appropriately repositioned and repainted.
+ * appropriately repositioned and repainted. In the case of insertion or
+ * removal, following spacer indexes get updated as well.
*
* @param visualSourceRange
* the range of rows to move to a new place
@@ -3014,6 +3437,9 @@ public class Escalator extends Widget
if (visualSourceRange.isEmpty()) {
return;
}
+ int sourceRangeLength = visualSourceRange.length();
+ int domRowCount = getDomRowCount();
+ int rowCount = getRowCount();
assert visualSourceRange.getStart() >= 0 : "Visual source start "
+ "must be 0 or greater (was "
@@ -3025,18 +3451,18 @@ public class Escalator extends Widget
assert visualTargetIndex >= 0 : "Visual target must be 0 or greater (was "
+ visualTargetIndex + ")";
- assert visualTargetIndex <= getDomRowCount() : "Visual target "
+ assert visualTargetIndex <= domRowCount : "Visual target "
+ "must not be greater than the number of escalator rows (was "
- + visualTargetIndex + ", escalator rows " + getDomRowCount()
+ + visualTargetIndex + ", escalator rows " + domRowCount
+ ")";
assert logicalTargetIndex
- + visualSourceRange.length() <= getRowCount() : "Logical "
+ + sourceRangeLength <= rowCount : "Logical "
+ "target leads to rows outside of the data range ("
+ Range.withLength(logicalTargetIndex,
- visualSourceRange.length())
- + " goes beyond "
- + Range.withLength(0, getRowCount()) + ")";
+ sourceRangeLength)
+ + " goes beyond " + Range.withLength(0, rowCount)
+ + ")";
/*
* Since we move a range into another range, the indices might move
@@ -3050,13 +3476,31 @@ public class Escalator extends Widget
final int adjustedVisualTargetIndex;
if (visualSourceRange.getStart() < visualTargetIndex) {
adjustedVisualTargetIndex = visualTargetIndex
- - visualSourceRange.length();
+ - sourceRangeLength;
} else {
adjustedVisualTargetIndex = visualTargetIndex;
}
- if (visualSourceRange.getStart() != adjustedVisualTargetIndex) {
+ int oldTopRowLogicalIndex = getTopRowLogicalIndex();
+ // first moved row's logical index before move
+ int oldSourceRangeLogicalStart = oldTopRowLogicalIndex
+ + visualSourceRange.getStart();
+
+ // new top row logical index
+ int newTopRowLogicalIndex = logicalTargetIndex
+ - adjustedVisualTargetIndex;
+
+ // variables for update types that require special handling
+ boolean recycledToTop = logicalTargetIndex < oldTopRowLogicalIndex;
+ boolean recycledFromTop = visualSourceRange.getStart() == 0;
+ boolean scrollingUp = recycledToTop
+ && visualSourceRange.getEnd() == visualRowOrder.size();
+ boolean scrollingDown = recycledFromTop
+ && logicalTargetIndex >= oldTopRowLogicalIndex
+ + visualRowOrder.size();
+
+ if (visualSourceRange.getStart() != adjustedVisualTargetIndex) {
/*
* Reorder the rows to their correct places within
* visualRowOrder (unless rows are moved back to their original
@@ -3071,8 +3515,8 @@ public class Escalator extends Widget
*/
final List<TableRowElement> removedRows = new ArrayList<>(
- visualSourceRange.length());
- for (int i = 0; i < visualSourceRange.length(); i++) {
+ sourceRangeLength);
+ for (int i = 0; i < sourceRangeLength; i++) {
final TableRowElement tr = visualRowOrder
.remove(visualSourceRange.getStart());
removedRows.add(tr);
@@ -3080,33 +3524,369 @@ public class Escalator extends Widget
visualRowOrder.addAll(adjustedVisualTargetIndex, removedRows);
}
- { // Refresh the contents of the affected rows
- final ListIterator<TableRowElement> iter = visualRowOrder
- .listIterator(adjustedVisualTargetIndex);
- for (int logicalIndex = logicalTargetIndex; logicalIndex < logicalTargetIndex
- + visualSourceRange.length(); logicalIndex++) {
- final TableRowElement tr = iter.next();
- refreshRow(tr, logicalIndex);
+ // refresh contents of rows to be recycled, returns the combined
+ // height of the spacers that got removed from visual range
+ double spacerHeightsOfRecycledRowsBefore = refreshRecycledRowContents(
+ logicalTargetIndex, adjustedVisualTargetIndex,
+ sourceRangeLength, oldSourceRangeLogicalStart);
+
+ boolean movedDown = adjustedVisualTargetIndex != visualTargetIndex;
+ boolean recycledToOrFromTop = recycledToTop || recycledFromTop;
+
+ // update spacer indexes unless we are scrolling -- with scrolling
+ // the remaining spacers are where they belong, the recycled ones
+ // were already removed, and new ones will be added with delay
+ if (!(scrollingUp || scrollingDown)) {
+ if (recycledToOrFromTop) {
+ updateSpacerIndexesForMoveWhenRecycledToOrFromTop(
+ oldSourceRangeLogicalStart, sourceRangeLength,
+ oldTopRowLogicalIndex, newTopRowLogicalIndex,
+ recycledFromTop);
+ } else {
+ updateSpacerIndexesForMoveWhenNotRecycledToOrFromTop(
+ logicalTargetIndex, oldSourceRangeLogicalStart,
+ sourceRangeLength, movedDown);
+ }
+ }
+
+ // Would be useful if new spacer heights could be determined
+ // here already but their contents are populated with delay.
+ // If the heights ever become available immediately, the
+ // handling that follows needs to be updated to take the new
+ // spacer heights into account.
+
+ repositionMovedRows(adjustedVisualTargetIndex, sourceRangeLength,
+ newTopRowLogicalIndex);
+
+ // variables for reducing the amount of necessary parameters
+ boolean scrollingDownAndNoSpacersRemoved = scrollingDown
+ && spacerHeightsOfRecycledRowsBefore <= 0d;
+ boolean spacerHeightsChanged = spacerHeightsOfRecycledRowsBefore > 0d;
+
+ repositionRowsShiftedByTheMove(visualSourceRange, visualTargetIndex,
+ adjustedVisualTargetIndex, newTopRowLogicalIndex,
+ scrollingDownAndNoSpacersRemoved, scrollingUp,
+ recycledToTop);
+
+ repositionRowsBelowMovedAndShiftedIfNeeded(visualSourceRange,
+ visualTargetIndex, adjustedVisualTargetIndex,
+ newTopRowLogicalIndex, (scrollingUp || scrollingDown),
+ recycledToOrFromTop, spacerHeightsChanged);
+ }
+
+ /**
+ * Refresh the contents of the affected rows for
+ * {@link #moveAndUpdateEscalatorRows(Range, int, int)}
+ * <p>
+ * NOTE: This method should not be called directly from anywhere else.
+ *
+ * @param logicalTargetIndex
+ * @param adjustedVisualTargetIndex
+ * @param sourceRangeLength
+ * @param spacerHeightsBeforeMoveTotal
+ * @param oldSourceRangeLogicalStart
+ * @return the combined height of any removed spacers
+ */
+ private double refreshRecycledRowContents(int logicalTargetIndex,
+ int adjustedVisualTargetIndex, int sourceRangeLength,
+ int oldSourceRangeLogicalStart) {
+ final ListIterator<TableRowElement> iter = visualRowOrder
+ .listIterator(adjustedVisualTargetIndex);
+ double removedSpacerHeights = 0d;
+ for (int i = 0; i < sourceRangeLength; ++i) {
+ final TableRowElement tr = iter.next();
+ int logicalIndex = logicalTargetIndex + i;
+
+ // clear old spacer
+ SpacerContainer.SpacerImpl spacer = spacerContainer
+ .getSpacer(oldSourceRangeLogicalStart + i);
+ if (spacer != null) {
+ double spacerHeight = spacer.getHeight();
+ removedSpacerHeights += spacerHeight;
+ spacerContainer
+ .removeSpacer(oldSourceRangeLogicalStart + i);
+ }
+
+ refreshRow(tr, logicalIndex);
+ }
+ return removedSpacerHeights;
+ }
+
+ /**
+ * Update the spacer indexes to correspond with logical indexes for
+ * {@link #moveAndUpdateEscalatorRows(Range, int, int)} when the move
+ * recycles rows to or from top
+ * <p>
+ * NOTE: This method should not be called directly from anywhere else.
+ *
+ * @param oldSourceRangeLogicalStart
+ * @param sourceRangeLength
+ * @param oldTopRowLogicalIndex
+ * @param newTopRowLogicalIndex
+ * @param recycledFromTop
+ */
+ private void updateSpacerIndexesForMoveWhenRecycledToOrFromTop(
+ int oldSourceRangeLogicalStart, int sourceRangeLength,
+ int oldTopRowLogicalIndex, int newTopRowLogicalIndex,
+ boolean recycledFromTop) {
+ if (recycledFromTop) {
+ // first rows are getting recycled thanks to insertion or
+ // removal, all the indexes below need to be updated
+ // accordingly
+ int indexesToShift;
+ if (newTopRowLogicalIndex != oldTopRowLogicalIndex) {
+ indexesToShift = newTopRowLogicalIndex
+ - oldTopRowLogicalIndex;
+ } else {
+ indexesToShift = -sourceRangeLength;
+ }
+ spacerContainer.updateSpacerIndexesForRowAndAfter(
+ oldSourceRangeLogicalStart + sourceRangeLength,
+ oldTopRowLogicalIndex + visualRowOrder.size(),
+ indexesToShift);
+ } else {
+ // rows recycled to the top, move the remaining spacer
+ // indexes up
+ spacerContainer.updateSpacerIndexesForRowAndAfter(
+ oldSourceRangeLogicalStart + sourceRangeLength,
+ getRowCount() + sourceRangeLength, -sourceRangeLength);
+ }
+ }
+
+ /**
+ * Update the spacer indexes to correspond with logical indexes for
+ * {@link #moveAndUpdateEscalatorRows(Range, int, int)} when the move
+ * does not recycle rows to or from top
+ * <p>
+ * NOTE: This method should not be called directly from anywhere else.
+ *
+ * @param logicalTargetIndex
+ * @param oldSourceRangeLogicalStart
+ * @param sourceRangeLength
+ * @param movedDown
+ */
+ private void updateSpacerIndexesForMoveWhenNotRecycledToOrFromTop(
+ int logicalTargetIndex, int oldSourceRangeLogicalStart,
+ int sourceRangeLength, boolean movedDown) {
+ if (movedDown) {
+ // move the shifted spacer indexes up to fill the freed
+ // space
+ spacerContainer.updateSpacerIndexesForRowAndAfter(
+ oldSourceRangeLogicalStart + sourceRangeLength,
+ logicalTargetIndex + sourceRangeLength,
+ -sourceRangeLength);
+ } else {
+ // move the shifted spacer indexes down to fill the freed
+ // space
+ spacerContainer.updateSpacerIndexesForRowAndAfter(
+ logicalTargetIndex, oldSourceRangeLogicalStart,
+ sourceRangeLength);
+ }
+ }
+
+ /**
+ * Reposition the rows that were moved for
+ * {@link #moveAndUpdateEscalatorRows(Range, int, int)}
+ * <p>
+ * NOTE: This method should not be called directly from anywhere else.
+ *
+ * @param adjustedVisualTargetIndex
+ * @param sourceRangeLength
+ * @param newTopRowLogicalIndex
+ */
+ private void repositionMovedRows(int adjustedVisualTargetIndex,
+ int sourceRangeLength, int newTopRowLogicalIndex) {
+ int start = adjustedVisualTargetIndex;
+ updateRowPositions(newTopRowLogicalIndex + start, start,
+ sourceRangeLength);
+ }
+
+ /**
+ * Reposition the rows that were shifted by the move for
+ * {@link #moveAndUpdateEscalatorRows(Range, int, int)}
+ * <p>
+ * NOTE: This method should not be called directly from anywhere else.
+ *
+ * @param visualSourceRange
+ * @param visualTargetIndex
+ * @param adjustedVisualTargetIndex
+ * @param newTopRowLogicalIndex
+ * @param scrollingDownAndNoSpacersRemoved
+ * @param scrollingUp
+ * @param recycledToTop
+ */
+ private void repositionRowsShiftedByTheMove(Range visualSourceRange,
+ int visualTargetIndex, int adjustedVisualTargetIndex,
+ int newTopRowLogicalIndex,
+ boolean scrollingDownAndNoSpacersRemoved, boolean scrollingUp,
+ boolean recycledToTop) {
+ if (visualSourceRange.length() == visualRowOrder.size()) {
+ // all rows got updated and were repositioned already
+ return;
+ }
+ if (scrollingDownAndNoSpacersRemoved || scrollingUp) {
+ // scrolling, no spacers got removed from or added above any
+ // remaining rows so everything is where it belongs already
+ // (there is no check for added spacers because adding happens
+ // with delay, whether any spacers are coming or not they don't
+ // exist yet and thus can't be taken into account here)
+ return;
+ }
+
+ if (adjustedVisualTargetIndex != visualTargetIndex) {
+ // rows moved down, shifted rows need to be moved up
+
+ int start = visualSourceRange.getStart();
+ updateRowPositions(newTopRowLogicalIndex + start, start,
+ adjustedVisualTargetIndex - start);
+ } else {
+ // rows moved up, shifted rows need to be repositioned
+ // unless it's just a recycling and no spacer heights
+ // above got updated
+
+ if (recycledToTop) {
+ // rows below the shifted ones need to be moved up (which is
+ // done in the next helper method) but the shifted rows
+ // themselves are already where they belong
+ // (this should only be done if no spacers were added, but
+ // we can't know that yet so we'll have to adjust for them
+ // afterwards if any do appear)
+ return;
+ }
+
+ int start = adjustedVisualTargetIndex
+ + visualSourceRange.length();
+ updateRowPositions(newTopRowLogicalIndex + start, start,
+ visualSourceRange.getEnd() - start);
+ }
+ }
+
+ /**
+ * If necessary, reposition the rows that are below those rows that got
+ * moved or shifted for
+ * {@link #moveAndUpdateEscalatorRows(Range, int, int)}
+ * <p>
+ * NOTE: This method should not be called directly from anywhere else.
+ *
+ * @param visualSourceRange
+ * @param visualTargetIndex
+ * @param adjustedVisualTargetIndex
+ * @param newTopRowLogicalIndex
+ * @param scrolling
+ * @param recycledToOrFromTop
+ * @param spacerHeightsChanged
+ */
+ private void repositionRowsBelowMovedAndShiftedIfNeeded(
+ Range visualSourceRange, int visualTargetIndex,
+ int adjustedVisualTargetIndex, int newTopRowLogicalIndex,
+ boolean scrolling, boolean recycledToOrFromTop,
+ boolean spacerHeightsChanged) {
+ /*
+ * There is no need to check if any rows preceding the source and
+ * target range need their positions adjusted, but rows below both
+ * may very well need it if spacer heights changed or rows got
+ * inserted or removed instead of just moved around.
+ *
+ * When scrolling to either direction all the rows already got
+ * processed by earlier stages, there are no unprocessed rows left
+ * either above or below.
+ */
+ if (!scrolling && (recycledToOrFromTop || spacerHeightsChanged)) {
+
+ int firstBelow;
+ if (adjustedVisualTargetIndex != visualTargetIndex) {
+ // rows moved down
+ firstBelow = adjustedVisualTargetIndex
+ + visualSourceRange.length();
+ } else {
+ // rows moved up
+ firstBelow = visualSourceRange.getEnd();
}
+ updateRowPositions(newTopRowLogicalIndex + firstBelow,
+ firstBelow, visualRowOrder.size() - firstBelow);
+ }
+ }
+
+ @Override
+ public void updateRowPositions(int index, int numberOfRows) {
+ Range visibleRowRange = getVisibleRowRange();
+ Range rangeToUpdate = Range.withLength(index, numberOfRows);
+ Range intersectingRange = visibleRowRange
+ .partitionWith(rangeToUpdate)[1];
+
+ if (intersectingRange.isEmpty()) {
+ // no overlap with the visual range, ignore the positioning
+ return;
}
- { // Reposition the rows that were moved
- double newRowTop = getRowTop(logicalTargetIndex);
+ int adjustedIndex = intersectingRange.getStart();
+ int adjustedVisualIndex = adjustedIndex - getTopRowLogicalIndex();
+
+ updateRowPositions(adjustedIndex, adjustedVisualIndex,
+ intersectingRange.length());
- final ListIterator<TableRowElement> iter = visualRowOrder
- .listIterator(adjustedVisualTargetIndex);
- for (int i = 0; i < visualSourceRange.length(); i++) {
- final TableRowElement tr = iter.next();
- setRowPosition(tr, 0, newRowTop);
+ // make sure there is no unnecessary gap
+ adjustScrollPositionIfNeeded();
- newRowTop += getDefaultRowHeight();
- newRowTop += spacerContainer
- .getSpacerHeight(logicalTargetIndex + i);
+ scroller.recalculateScrollbarsForVirtualViewport();
+ }
+
+ /**
+ * Re-calculates and updates the positions of rows and spacers within
+ * the given range. Doesn't touch the scroll positions.
+ *
+ * @param logicalIndex
+ * logical index of the first row to reposition
+ * @param visualIndex
+ * visual index of the first row to reposition
+ * @param numberOfRows
+ * the number of rows to reposition
+ */
+ private void updateRowPositions(int logicalIndex, int visualIndex,
+ int numberOfRows) {
+ double newRowTop = getRowTop(logicalIndex);
+ for (int i = 0; i < numberOfRows; ++i) {
+ TableRowElement tr = visualRowOrder.get(visualIndex + i);
+ setRowPosition(tr, 0, newRowTop);
+ newRowTop += getDefaultRowHeight();
+
+ SpacerContainer.SpacerImpl spacer = spacerContainer
+ .getSpacer(logicalIndex + i);
+ if (spacer != null) {
+ spacer.setPosition(0, newRowTop);
+ newRowTop += spacer.getHeight();
}
}
}
/**
+ * Checks whether there is an unexpected gap below the visible rows and
+ * adjusts the viewport if necessary.
+ */
+ private void adjustScrollPositionIfNeeded() {
+ double scrollTop = getScrollTop();
+ int firstBelowVisualRange = getTopRowLogicalIndex()
+ + visualRowOrder.size();
+ double gapBelow = scrollTop + getHeightOfSection()
+ - getRowTop(firstBelowVisualRange);
+ boolean bufferRowNeeded = gapBelow == 0
+ && firstBelowVisualRange < getRowCount();
+ if (scrollTop > 0 && (gapBelow > 0 || bufferRowNeeded)) {
+ /*
+ * This situation can be reached e.g. by removing a spacer.
+ * Scroll position must be adjusted accordingly but no more than
+ * there is room to scroll up. If a buffer row is needed make
+ * sure the last row ends up at least slightly below the
+ * viewport.
+ */
+ double adjustedGap = Math.max(gapBelow,
+ bufferRowNeeded ? 1 : 0);
+ double yDeltaScroll = Math.min(adjustedGap, scrollTop);
+ moveViewportAndContent(null, 0, 0, -yDeltaScroll);
+ }
+ }
+
+ /**
* Adjust the scroll position and move the contained rows.
* <p>
* The difference between using this method and simply scrolling is that
@@ -3126,11 +3906,17 @@ public class Escalator extends Widget
* the row at 20px.</dd>
* </dl>
*
+ * @deprecated This method isn't used by Escalator anymore since Vaadin
+ * 8.9 and the general row handling logic has been
+ * rewritten, so attempting to call this method may lead to
+ * unexpected consequences. This method is likely to get
+ * removed soon.
* @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
*/
+ @Deprecated
public void moveViewportAndContent(final double yDelta) {
if (yDelta == 0) {
@@ -3162,48 +3948,155 @@ public class Escalator extends Widget
}
/**
- * Adds new physical escalator rows to the DOM at the given index if
- * there's still a need for more escalator rows.
+ * Move rows, spacers, and/or viewport up or down. For rows and spacers
+ * either everything within visual range is affected (index
+ * {@code null}) or only those from the given row index forward.
+ * <p>
+ * This method does not update spacer indexes.
+ *
+ * @param index
+ * the logical index from which forward the rows and spacers
+ * should be updated, or {@code null} if all of them
+ * @param yDeltaRows
+ * how much rows should be shifted in pixels
+ * @param yDeltaSpacers
+ * how much spacers should be shifted in pixels
+ * @param yDeltaScroll
+ * how much scroll position should be shifted in pixels
+ */
+ private void moveViewportAndContent(Integer index,
+ final double yDeltaRows, final double yDeltaSpacers,
+ final double yDeltaScroll) {
+
+ if (!WidgetUtil.pixelValuesEqual(yDeltaScroll, 0d)) {
+ double newTop = tBodyScrollTop + yDeltaScroll;
+ verticalScrollbar.setScrollPos(newTop);
+ setBodyScrollPosition(tBodyScrollLeft, newTop);
+ }
+
+ if (!WidgetUtil.pixelValuesEqual(yDeltaSpacers, 0d)) {
+ Collection<SpacerContainer.SpacerImpl> spacers;
+ if (index == null) {
+ spacers = spacerContainer.getSpacersAfterPx(tBodyScrollTop,
+ SpacerInclusionStrategy.PARTIAL);
+ } else {
+ spacers = spacerContainer.getSpacersForRowAndAfter(index);
+ }
+ for (SpacerContainer.SpacerImpl spacer : spacers) {
+ spacer.setPositionDiff(0, yDeltaSpacers);
+ }
+ }
+
+ if (!WidgetUtil.pixelValuesEqual(yDeltaRows, 0d)) {
+ if (index == null) {
+ // move all visible rows to the desired direction
+ for (TableRowElement tr : visualRowOrder) {
+ setRowPosition(tr, 0, getRowTop(tr) + yDeltaRows);
+ }
+ } else {
+ // move all visible rows, including the index row, to the
+ // desired direction
+ shiftRowPositions(index - 1, yDeltaRows);
+ }
+ }
+ }
+
+ /**
+ * Adds new physical escalator rows to the DOM at the given visual index
+ * if there's still a need for more escalator rows.
* <p>
* If Escalator already is at (or beyond) max capacity, this method does
* nothing to the DOM.
+ * <p>
+ * Calling this method repositions all the rows and spacers below the
+ * insertion point.
*
- * @param index
- * the index at which to add new escalator rows.
- * <em>Note:</em>It is assumed that the index is both the
- * visual index and the logical index.
+ * @param visualIndex
+ * the index at which to add new escalator rows to DOM
+ * @param logicalIndex
+ * the logical index that corresponds with the first new
+ * escalator row, should usually be the same as visual index
+ * because there is still need for new rows, but this is not
+ * always the case e.g. if row height is changed
* @param numberOfRows
* the number of rows to add at <code>index</code>
* @return a list of the added rows
*/
private List<TableRowElement> fillAndPopulateEscalatorRowsIfNeeded(
- final int index, final int numberOfRows) {
+ final int visualIndex, final int logicalIndex,
+ final int numberOfRows) {
+ /*
+ * We want to maintain enough rows to fill the entire viewport even
+ * if their spacers have no height. If their spacers do have height
+ * some of these rows may end up outside of the viewport, but that's
+ * ok.
+ */
final int escalatorRowsStillFit = getMaxVisibleRowCount()
- getDomRowCount();
final int escalatorRowsNeeded = Math.min(numberOfRows,
escalatorRowsStillFit);
if (escalatorRowsNeeded > 0) {
+ int rowsBeforeAddition = visualRowOrder.size();
+ // this is AbstractRowContainer method and not easily overridden
+ // to consider logical indexes separately from visual indexes,
+ // so as a workaround we create the rows as if those two were
+ // the same and then update the contents if needed
final List<TableRowElement> addedRows = paintInsertStaticRows(
- index, escalatorRowsNeeded);
- visualRowOrder.addAll(index, addedRows);
-
- double y = index * getDefaultRowHeight()
- + spacerContainer.getSpacerHeightsSumUntilIndex(index);
- for (int i = index; i < visualRowOrder.size(); i++) {
-
- final TableRowElement tr;
- if (i - index < addedRows.size()) {
- tr = addedRows.get(i - index);
+ visualIndex, escalatorRowsNeeded);
+ visualRowOrder.addAll(visualIndex, addedRows);
+
+ if (visualIndex != logicalIndex) {
+ // row got populated with wrong contents, need to update
+ int adjustedLogicalIndex = 0;
+ if (visualIndex == 0) {
+ // added to the beginning of visual range, use the
+ // end of insertion range because the beginning might
+ // not fit completely
+ adjustedLogicalIndex = logicalIndex + numberOfRows
+ - addedRows.size();
} else {
- tr = visualRowOrder.get(i);
+ // added anywhere else, use the beginning of
+ // insertion range and the rest of the rows get
+ // recycled below if there is room for them
+ adjustedLogicalIndex = logicalIndex;
}
+ for (int i = 0; i < addedRows.size(); ++i) {
+ TableRowElement tr = addedRows.get(i);
+ refreshRow(tr, adjustedLogicalIndex + i);
+ }
+ }
- setRowPosition(tr, 0, y);
- y += getDefaultRowHeight();
- y += spacerContainer.getSpacerHeight(i);
+ // if something is getting inserted instead of just being
+ // brought to visual range, the rows below the insertion point
+ // need to have their spacer indexes updated accordingly
+ if (logicalIndex >= getTopRowLogicalIndex()
+ && visualIndex < rowsBeforeAddition) {
+ spacerContainer.updateSpacerIndexesForRowAndAfter(
+ logicalIndex, getRowCount(), addedRows.size());
+ }
+
+ // update the positions of the added rows and the rows below
+ // them
+ // TODO: this can lead to moving things around twice in case
+ // some rows didn't get new dom rows (e.g. when expanding a
+ // TreeGrid node with more children than can fit within the max
+ // visual range size), consider moving this update elsewhere
+ double rowTop = getRowTop(logicalIndex);
+ for (int i = visualIndex; i < visualRowOrder.size(); i++) {
+
+ final TableRowElement tr = visualRowOrder.get(i);
+
+ setRowPosition(tr, 0, rowTop);
+ rowTop += getDefaultRowHeight();
+ SpacerContainer.SpacerImpl spacer = spacerContainer
+ .getSpacer(logicalIndex - visualIndex + i);
+ if (spacer != null) {
+ spacer.setPosition(0, rowTop);
+ rowTop += spacer.getHeight();
+ }
}
// Execute the registered callback function for newly created
@@ -3221,11 +4114,12 @@ public class Escalator extends Widget
double heightOfSection = getHeightOfSection();
// By including the possibly shown scrollbar height, we get a
// consistent count and do not add/remove rows whenever a scrollbar
- // is shown
+ // is shown. Make sure that two extra rows are included for
+ // assisting with tab navigation on both sides of the viewport.
heightOfSection += horizontalScrollbarDeco.getOffsetHeight();
double defaultRowHeight = getDefaultRowHeight();
final int maxVisibleRowCount = (int) Math
- .ceil(heightOfSection / defaultRowHeight) + 1;
+ .ceil(heightOfSection / defaultRowHeight) + 2;
/*
* maxVisibleRowCount can become negative if the headers and footers
@@ -3241,399 +4135,261 @@ public class Escalator extends Widget
if (numberOfRows == 0) {
return;
}
-
- final Range viewportRange = getVisibleRowRange();
- final Range removedRowsRange = Range.withLength(index,
- numberOfRows);
-
/*
- * Removing spacers as the very first step will correct the
- * scrollbars and row offsets right away.
+ * NOTE: this method handles and manipulates logical, visual, and
+ * physical indexes a lot. If you don't remember what those mean and
+ * how they relate to each other, see the top of this class for
+ * Maintenance Notes.
*
- * TODO: actually, it kinda sounds like a Grid feature that a spacer
- * would be associated with a particular row. Maybe it would be
- * better to have a spacer separate from rows, and simply collapse
- * them if they happen to end up on top of each other. This would
- * probably make supporting the -1 row pretty easy, too.
+ * At the beginning of this method the logical index of the data
+ * provider has already been updated to include the new rows, but
+ * visual and physical indexes have not, nor has the spacer indexing
+ * been updated, and the topRowLogicalIndex may be out of date as
+ * well.
*/
- spacerContainer.paintRemoveSpacers(removedRowsRange);
- final Range[] partitions = removedRowsRange
- .partitionWith(viewportRange);
- final Range removedAbove = partitions[0];
- final Range removedLogicalInside = partitions[1];
- final Range removedVisualInside = convertToVisual(
- removedLogicalInside);
-
- /*
- * TODO: extract the following if-block to a separate method. I'll
- * leave this be inlined for now, to make linediff-based code
- * reviewing easier. Probably will be moved in the following patch
- * set.
- */
+ // logical index of the first old row, also the difference between
+ // logical index and visual index before any rows have been removed
+ final int oldTopRowLogicalIndex = getTopRowLogicalIndex();
+ // length of the visual range before anything gets removed
+ final int oldVisualRangeLength = visualRowOrder.size();
- /*
- * Adjust scroll position in one of two scenarios:
- *
- * 1) Rows were removed above. Then we just need to adjust the
- * scrollbar by the height of the removed rows.
- *
- * 2) There are no logical rows above, and at least the first (if
- * not more) visual row is removed. Then we need to snap the scroll
- * position to the first visible row (i.e. reset scroll position to
- * absolute 0)
- *
- * The logic is optimized in such a way that the
- * moveViewportAndContent is called only once, to avoid extra
- * reflows, and thus the code might seem a bit obscure.
- */
- final boolean firstVisualRowIsRemoved = !removedVisualInside
- .isEmpty() && removedVisualInside.getStart() == 0;
-
- if (!removedAbove.isEmpty() || firstVisualRowIsRemoved) {
- final double yDelta = removedAbove.length()
- * getDefaultRowHeight();
- final double firstLogicalRowHeight = getDefaultRowHeight();
- final boolean removalScrollsToShowFirstLogicalRow = verticalScrollbar
- .getScrollPos() - yDelta < firstLogicalRowHeight;
-
- if (removedVisualInside.isEmpty()
- && (!removalScrollsToShowFirstLogicalRow
- || !firstVisualRowIsRemoved)) {
- /*
- * rows were removed from above the viewport, so all we need
- * to do is to adjust the scroll position to account for the
- * removed rows
- */
- moveViewportAndContent(-yDelta);
- } else if (removalScrollsToShowFirstLogicalRow) {
- /*
- * It seems like we've removed all rows from above, and also
- * into the current viewport. This means we'll need to even
- * out the scroll position to exactly 0 (i.e. adjust by the
- * current negative scrolltop, presto!), so that it isn't
- * aligned funnily
- */
- moveViewportAndContent(-verticalScrollbar.getScrollPos());
- }
- }
+ // logical range of the removed rows
+ final Range removedRowsLogicalRange = Range.withLength(index,
+ numberOfRows);
- // ranges evaluated, let's do things.
- if (!removedVisualInside.isEmpty()) {
- int escalatorRowCount = body.getDomRowCount();
+ // check which parts of the removed range fall within or beyond the
+ // visual range
+ final Range[] partitions = removedRowsLogicalRange
+ .partitionWith(Range.withLength(oldTopRowLogicalIndex,
+ oldVisualRangeLength));
+ final Range removedLogicalAbove = partitions[0];
+ final Range removedLogicalBelow = partitions[2];
+ final Range removedLogicalWithin = partitions[1];
+ if (removedLogicalBelow.length() == numberOfRows) {
/*
- * remember: the rows have already been subtracted from the row
- * count at this point
+ * Rows were removed entirely from below the visual range. No
+ * rows to recycle or scroll position to adjust, just need to
+ * recalculate scrollbar height. No need to touch the spacer
+ * indexing or the physical index.
*/
- int rowsLeft = getRowCount();
- if (rowsLeft < escalatorRowCount) {
- /*
- * Remove extra DOM rows and refresh contents.
- */
- for (int i = escalatorRowCount - 1; i >= rowsLeft; i--) {
- final TableRowElement tr = visualRowOrder.remove(i);
- paintRemoveRow(tr, i);
- removeRowPosition(tr);
- }
+ scroller.recalculateScrollbarsForVirtualViewport();
- // Move rest of the rows to the Escalator's top
- Range visualRange = Range.withLength(0,
- visualRowOrder.size());
- moveAndUpdateEscalatorRows(visualRange, 0, 0);
-
- sortDomElements();
- setTopRowLogicalIndex(0);
-
- scroller.recalculateScrollbarsForVirtualViewport();
-
- fireRowVisibilityChangeEvent();
- return;
- } else {
- // No escalator rows need to be removed.
-
- /*
- * Two things (or a combination thereof) can happen:
- *
- * 1) We're scrolled to the bottom, the last rows are
- * removed. SOLUTION: moveAndUpdateEscalatorRows the
- * bottommost rows, and place them at the top to be
- * refreshed.
- *
- * 2) We're scrolled somewhere in the middle, arbitrary rows
- * are removed. SOLUTION: moveAndUpdateEscalatorRows the
- * removed rows, and place them at the bottom to be
- * refreshed.
- *
- * Since a combination can also happen, we need to handle
- * this in a smart way, all while avoiding
- * double-refreshing.
- */
-
- final double contentBottom = getRowCount()
- * getDefaultRowHeight();
- final double viewportBottom = tBodyScrollTop
- + getHeightOfSection();
- if (viewportBottom <= contentBottom) {
- /*
- * We're in the middle of the row container, everything
- * is added to the bottom
- */
- paintRemoveRowsAtMiddle(removedLogicalInside,
- removedVisualInside, 0);
- } else if (removedVisualInside.contains(0)
- && numberOfRows >= visualRowOrder.size()) {
- /*
- * We're removing so many rows that the viewport is
- * pushed up more than a screenful. This means we can
- * simply scroll up and everything will work without a
- * sweat.
- */
-
- double left = horizontalScrollbar.getScrollPos();
- double top = contentBottom
- - visualRowOrder.size() * getDefaultRowHeight();
- setBodyScrollPosition(left, top);
-
- Range allEscalatorRows = Range.withLength(0,
- visualRowOrder.size());
- int logicalTargetIndex = getRowCount()
- - allEscalatorRows.length();
- moveAndUpdateEscalatorRows(allEscalatorRows, 0,
- logicalTargetIndex);
-
- /*
- * moveAndUpdateEscalatorRows recalculates the rows, but
- * logical top row index bookkeeping is handled in this
- * method.
- *
- * TODO: Redesign how to keep it easy to track this.
- */
- updateTopRowLogicalIndex(
- -removedLogicalInside.length());
-
- /*
- * Scrolling the body to the correct location will be
- * fixed automatically. Because the amount of rows is
- * decreased, the viewport is pushed up as the scrollbar
- * shrinks. So no need to do anything there.
- *
- * TODO [[optimize]]: This might lead to a double body
- * refresh. Needs investigation.
- */
- } else if (contentBottom
- + (numberOfRows * getDefaultRowHeight())
- - viewportBottom < getDefaultRowHeight()) {
- /*
- * We're at the end of the row container, everything is
- * added to the top.
- */
-
- /*
- * FIXME [[spacer]]: above if-clause is coded to only
- * work with default row heights - will not work with
- * variable row heights
- */
+ // Visual range contents remain the same, no need to fire a
+ // RowVisibilityChangeEvent.
+ } else if (removedLogicalAbove.length() == numberOfRows) {
+ /*
+ * Rows were removed entirely from above the visual range. No
+ * rows to recycle, just need to update the spacer indexing and
+ * the content positions. No need to touch the physical index.
+ */
- paintRemoveRowsAtBottom(removedLogicalInside,
- removedVisualInside);
- updateTopRowLogicalIndex(
- -removedLogicalInside.length());
- } else {
- /*
- * We're in a combination, where we need to both scroll
- * up AND show new rows at the bottom.
- *
- * Example: Scrolled down to show the second to last
- * row. Remove two. Viewport scrolls up, revealing the
- * row above row. The last element collapses up and into
- * view.
- *
- * Reminder: this use case handles only the case when
- * there are enough escalator rows to still render a
- * full view. I.e. all escalator rows will _always_ be
- * populated
- */
- /*-
- * 1 1 |1| <- newly rendered
- * |2| |2| |2|
- * |3| ==> |*| ==> |5| <- newly rendered
- * |4| |*|
- * 5 5
- *
- * 1 1 |1| <- newly rendered
- * |2| |*| |4|
- * |3| ==> |*| ==> |5| <- newly rendered
- * |4| |4|
- * 5 5
- */
+ // update the logical indexes of remaining spacers
+ spacerContainer.updateSpacerIndexesForRowAndAfter(
+ oldTopRowLogicalIndex,
+ oldTopRowLogicalIndex + oldVisualRangeLength,
+ -numberOfRows);
- /*
- * STEP 1:
- *
- * reorganize deprecated escalator rows to bottom, but
- * don't re-render anything yet
- */
- /*-
- * 1 1 1
- * |2| |*| |4|
- * |3| ==> |*| ==> |*|
- * |4| |4| |*|
- * 5 5 5
- */
- double newTop = getRowTop(visualRowOrder
- .get(removedVisualInside.getStart()));
- for (int i = 0; i < removedVisualInside.length(); i++) {
- final TableRowElement tr = visualRowOrder
- .remove(removedVisualInside.getStart());
- visualRowOrder.addLast(tr);
- }
+ // default height of a single row
+ final double defaultRowHeight = getDefaultRowHeight();
- for (int i = removedVisualInside
- .getStart(); i < escalatorRowCount; i++) {
- final TableRowElement tr = visualRowOrder.get(i);
- setRowPosition(tr, 0, (int) newTop);
- newTop += getDefaultRowHeight();
- newTop += spacerContainer.getSpacerHeight(
- i + removedLogicalInside.getStart());
- }
+ // how much viewport, rows, and spacers should be shifted based
+ // on the removed rows, assume there were no spacers to remove
+ final double yDelta = numberOfRows * defaultRowHeight;
- /*
- * STEP 2:
- *
- * manually scroll
- */
- /*-
- * 1 |1| <-- newly rendered (by scrolling)
- * |4| |4|
- * |*| ==> |*|
- * |*|
- * 5 5
- */
- final double newScrollTop = contentBottom
- - getHeightOfSection();
- setScrollTop(newScrollTop);
- /*
- * Manually call the scroll handler, so we get immediate
- * effects in the escalator.
- */
- scroller.onScroll();
+ // shift everything up
+ moveViewportAndContent(null, -yDelta, -yDelta, -yDelta);
- /*
- * Move the bottommost (n+1:th) escalator row to top,
- * because scrolling up doesn't handle that for us
- * automatically
- */
- moveAndUpdateEscalatorRows(
- Range.withOnly(escalatorRowCount - 1), 0,
- getLogicalRowIndex(visualRowOrder.getFirst())
- - 1);
- updateTopRowLogicalIndex(-1);
+ // update the top row logical index according to any removed
+ // rows
+ updateTopRowLogicalIndex(-numberOfRows);
- /*
- * STEP 3:
- *
- * update remaining escalator rows
- */
- /*-
- * |1| |1|
- * |4| ==> |4|
- * |*| |5| <-- newly rendered
- *
- * 5
- */
+ // update scrollbar
+ scroller.recalculateScrollbarsForVirtualViewport();
- final int rowsScrolled = (int) (Math
- .ceil((viewportBottom - contentBottom)
- / getDefaultRowHeight()));
- final int start = escalatorRowCount
- - (removedVisualInside.length() - rowsScrolled);
- final Range visualRefreshRange = Range.between(start,
- escalatorRowCount);
- final int logicalTargetIndex = getLogicalRowIndex(
- visualRowOrder.getFirst()) + start;
- // in-place move simply re-renders the rows.
- moveAndUpdateEscalatorRows(visualRefreshRange, start,
- logicalTargetIndex);
- }
- }
+ // Visual range contents remain the same, no need to fire a
+ // RowVisibilityChangeEvent.
+ } else {
+ /*
+ * Rows are being removed at least partially from within the
+ * visual range. This is where things get tricky. We might have
+ * to scroll up or down or nowhere at all, depending on the
+ * situation.
+ */
- fireRowVisibilityChangeEvent();
- sortDomElements();
+ // Visual range contents changed, RowVisibilityChangeEvent will
+ // be triggered within this method
+ paintRemoveRowsWithinVisualRange(index, numberOfRows,
+ oldTopRowLogicalIndex, oldVisualRangeLength,
+ removedLogicalAbove.length(), removedLogicalWithin);
}
+ }
- updateTopRowLogicalIndex(-removedAbove.length());
-
+ /**
+ * Row removal handling for {@link #paintRemoveRows(int, int)} when the
+ * removed range intersects the visual range at least partially.
+ * <p>
+ * NOTE: This method should not be called directly from anywhere else.
+ *
+ * @param index
+ * @param numberOfRows
+ * @param oldTopRowLogicalIndex
+ * @param oldVisualRangeLength
+ * @param removedAboveLength
+ * @param removedLogicalWithin
+ */
+ private void paintRemoveRowsWithinVisualRange(int index,
+ int numberOfRows, int oldTopRowLogicalIndex,
+ int oldVisualRangeLength, int removedAboveLength,
+ Range removedLogicalWithin) {
/*
- * this needs to be done after the escalator has been shrunk down,
- * or it won't work correctly (due to setScrollTop invocation)
+ * Calculating where the visual range should start after the
+ * removals is not entirely trivial.
+ *
+ * Initially, any rows removed from within the visual range won't
+ * affect the top index, even if they are removed from the
+ * beginning, as the rows are also removed from the logical index.
+ * Likewise we don't need to care about rows removed from below the
+ * visual range. On the other hand, any rows removed from above the
+ * visual range do shift the index down.
+ *
+ * However, in all of these cases, if there aren't enough rows below
+ * the visual range to replace the content removed from within the
+ * visual range, more rows need to be brought in from above the old
+ * visual range in turn. This shifts the index down even further.
*/
- scroller.recalculateScrollbarsForVirtualViewport();
- }
- private void paintRemoveRowsAtMiddle(final Range removedLogicalInside,
- final Range removedVisualInside, final int logicalOffset) {
- /*-
- * : : :
- * |2| |2| |2|
- * |3| ==> |*| ==> |4|
- * |4| |4| |6| <- newly rendered
- * : : :
- */
+ // scroll position before any rows or spacers are removed
+ double scrollTop = getScrollTop();
+
+ Range removedVisualWithin = convertToVisual(removedLogicalWithin);
+ int remainingVisualRangeRowCount = visualRowOrder.size()
+ - removedVisualWithin.length();
+
+ int newTopRowLogicalIndex = oldTopRowLogicalIndex
+ - removedAboveLength;
+ int rowsToIncludeFromBelow = Math.min(
+ getRowCount() - newTopRowLogicalIndex
+ - remainingVisualRangeRowCount,
+ removedLogicalWithin.length());
+ int rowsToIncludeFromAbove = removedLogicalWithin.length()
+ - rowsToIncludeFromBelow;
+ int rowsToRemoveFromDom = 0;
+ if (rowsToIncludeFromAbove > 0) {
+ // don't try to bring in more rows than exist, it's possible
+ // to remove enough rows that visual range won't be full
+ // anymore
+ rowsToRemoveFromDom = Math
+ .max(rowsToIncludeFromAbove - newTopRowLogicalIndex, 0);
+ rowsToIncludeFromAbove -= rowsToRemoveFromDom;
+
+ newTopRowLogicalIndex -= rowsToIncludeFromAbove;
+ }
+
+ int visualIndexToRemove = Math.max(index - oldTopRowLogicalIndex,
+ 0);
+
+ // remove extra dom rows and their spacers if any
+ double removedFromDomSpacerHeights = 0d;
+ if (rowsToRemoveFromDom > 0) {
+ for (int i = 0; i < rowsToRemoveFromDom; ++i) {
+ TableRowElement tr = visualRowOrder
+ .remove(visualIndexToRemove);
+
+ // logical index of this row before anything got removed
+ int logicalRowIndex = oldTopRowLogicalIndex
+ + visualIndexToRemove + i;
+ double spacerHeight = spacerContainer
+ .getSpacerHeight(logicalRowIndex);
+ removedFromDomSpacerHeights += spacerHeight;
+ spacerContainer.removeSpacer(logicalRowIndex);
+
+ paintRemoveRow(tr, removedVisualWithin.getStart());
+ removeRowPosition(tr);
+ }
- final int escalatorRowCount = visualRowOrder.size();
-
- final int logicalTargetIndex = getLogicalRowIndex(
- visualRowOrder.getLast())
- - (removedVisualInside.length() - 1) + logicalOffset;
- moveAndUpdateEscalatorRows(removedVisualInside, escalatorRowCount,
- logicalTargetIndex);
-
- // move the surrounding rows to their correct places.
- final ListIterator<TableRowElement> iterator = visualRowOrder
- .listIterator(removedVisualInside.getStart());
-
- double rowTop = getRowTop(
- removedLogicalInside.getStart() + logicalOffset);
- for (int i = removedVisualInside.getStart(); i < escalatorRowCount
- - removedVisualInside.length(); i++) {
- final TableRowElement tr = iterator.next();
- setRowPosition(tr, 0, rowTop);
- rowTop += getDefaultRowHeight();
- rowTop += spacerContainer
- .getSpacerHeight(i + removedLogicalInside.getStart());
- }
- }
-
- private void paintRemoveRowsAtBottom(final Range removedLogicalInside,
- final Range removedVisualInside) {
- /*-
- * :
- * : : |4| <- newly rendered
- * |5| |5| |5|
- * |6| ==> |*| ==> |7|
- * |7| |7|
- */
+ // update the associated row indexes for remaining spacers,
+ // even for those rows that are going to get recycled
+ spacerContainer.updateSpacerIndexesForRowAndAfter(
+ oldTopRowLogicalIndex + visualIndexToRemove
+ + rowsToRemoveFromDom,
+ oldTopRowLogicalIndex + oldVisualRangeLength,
+ -rowsToRemoveFromDom);
+ }
+
+ // add new content from below visual range, if there is any
+ if (rowsToIncludeFromBelow > 0) {
+ // removed rows are recycled to just below the old visual
+ // range, calculate the logical index of the insertion
+ // point that is just below the existing rows, taking into
+ // account that the indexing has changed with the removal
+ int firstBelow = newTopRowLogicalIndex + rowsToIncludeFromAbove
+ + remainingVisualRangeRowCount;
+
+ moveAndUpdateEscalatorRows(
+ Range.withLength(visualIndexToRemove,
+ rowsToIncludeFromBelow),
+ visualRowOrder.size(), firstBelow);
+ }
+
+ // add new content from above visual range, if there is any
+ // -- this is left last because most of the time it isn't even
+ // needed
+ if (rowsToIncludeFromAbove > 0) {
+ moveAndUpdateEscalatorRows(
+ Range.withLength(visualIndexToRemove,
+ rowsToIncludeFromAbove),
+ 0, newTopRowLogicalIndex);
+ }
+
+ // recycling updates all relevant row and spacer positions but
+ // if we only removed DOM rows and didn't recycle any we still
+ // need to shift up the rows below the removal point
+ if (rowsToIncludeFromAbove <= 0 && rowsToIncludeFromBelow <= 0) {
+ // update the positions for the rows and spacers below the
+ // removed ones, assume there is no need to update scroll
+ // position since the final check adjusts that if needed
+ double yDelta = numberOfRows * getDefaultRowHeight()
+ + removedFromDomSpacerHeights;
+ moveViewportAndContent(
+ newTopRowLogicalIndex + visualIndexToRemove, -yDelta,
+ -yDelta, 0);
+ }
+
+ setTopRowLogicalIndex(newTopRowLogicalIndex);
- final int logicalTargetIndex = getLogicalRowIndex(
- visualRowOrder.getFirst()) - removedVisualInside.length();
- moveAndUpdateEscalatorRows(removedVisualInside, 0,
- logicalTargetIndex);
+ scroller.recalculateScrollbarsForVirtualViewport();
- // move the surrounding rows to their correct places.
- int firstUpdatedIndex = removedVisualInside.getEnd();
- final ListIterator<TableRowElement> iterator = visualRowOrder
- .listIterator(firstUpdatedIndex);
+ // calling this method also triggers adding new spacers to the
+ // recycled rows, if any are needed
+ fireRowVisibilityChangeEvent();
- double rowTop = getRowTop(removedLogicalInside.getStart());
- int i = 0;
- while (iterator.hasNext()) {
- final TableRowElement tr = iterator.next();
- setRowPosition(tr, 0, rowTop);
- rowTop += getDefaultRowHeight();
- rowTop += spacerContainer
- .getSpacerHeight(firstUpdatedIndex + i++);
- }
+ // populating the spacers might take a while, delay calculations
+ // or the viewport might get adjusted too high
+ Scheduler.get().scheduleFinally(() -> {
+ // make sure there isn't a gap at the bottom after removal
+ // and adjust the viewport if there is
+
+ // FIXME: this should be doable with
+ // adjustScrollPositionIfNeeded() but it uses current
+ // scrollTop, which may have ended in wrong position and
+ // results in assuming too big gap and consequently
+ // scrolling up too much
+ double extraSpaceAtBottom = scrollTop + getHeightOfSection()
+ - getRowTop(getTopRowLogicalIndex()
+ + visualRowOrder.size());
+ if (extraSpaceAtBottom > 0 && scrollTop > 0) {
+ // we need to move the viewport up to adjust, while the
+ // rows and spacers can remain where they are
+ double yDeltaScroll = Math.min(extraSpaceAtBottom,
+ scrollTop);
+ moveViewportAndContent(null, 0, 0, -yDeltaScroll);
+ }
+ });
+
+ // update physical index
+ sortDomElements();
}
@Override
@@ -3680,16 +4436,10 @@ public class Escalator extends Widget
return Range.withLength(0, 0);
}
- /*
- * TODO [[spacer]]: these assumptions will be totally broken with
- * spacers.
- */
- final int maxVisibleRowCount = getMaxVisibleRowCount();
- final int currentTopRowIndex = getLogicalRowIndex(
- visualRowOrder.getFirst());
+ final int currentTopRowIndex = getTopRowLogicalIndex();
- final Range[] partitions = logicalRange.partitionWith(
- Range.withLength(currentTopRowIndex, maxVisibleRowCount));
+ final Range[] partitions = logicalRange
+ .partitionWith(getVisibleRowRange());
final Range insideRange = partitions[1];
return insideRange.offsetBy(-currentTopRowIndex);
}
@@ -3781,7 +4531,7 @@ public class Escalator extends Widget
* This method indeed has a smell very similar to paintRemoveRows
* and paintInsertRows.
*
- * Unfortunately, those the code can't trivially be shared, since
+ * Unfortunately, the code of those 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
@@ -3801,120 +4551,117 @@ public class Escalator extends Widget
return;
}
+ int oldTopRowLogicalIndex = getTopRowLogicalIndex();
+ int oldVisualRangeLength = visualRowOrder.size();
+
final int maxVisibleRowCount = getMaxVisibleRowCount();
final int neededEscalatorRows = Math.min(maxVisibleRowCount,
body.getRowCount());
- final int neededEscalatorRowsDiff = neededEscalatorRows
- - visualRowOrder.size();
- if (neededEscalatorRowsDiff > 0) {
- // needs more
+ final int rowDiff = neededEscalatorRows - oldVisualRangeLength;
- /*
- * This is a workaround for the issue where we might be scrolled
- * to the bottom, and the widget expands beyond the content
- * range
- */
+ if (rowDiff > 0) {
+ // more rows are needed
- final int index = visualRowOrder.size();
- final int nextLastLogicalIndex;
+ // calculate the indexes for adding rows below the last row of
+ // the visual range
+ final int visualTargetIndex = oldVisualRangeLength;
+ final int logicalTargetIndex;
if (!visualRowOrder.isEmpty()) {
- nextLastLogicalIndex = getLogicalRowIndex(
- visualRowOrder.getLast()) + 1;
+ logicalTargetIndex = oldTopRowLogicalIndex
+ + visualTargetIndex;
} else {
- nextLastLogicalIndex = 0;
+ logicalTargetIndex = 0;
}
- final boolean contentWillFit = nextLastLogicalIndex < getRowCount()
- - neededEscalatorRowsDiff;
- if (contentWillFit) {
- final List<TableRowElement> addedRows = fillAndPopulateEscalatorRowsIfNeeded(
- index, neededEscalatorRowsDiff);
+ // prioritise adding to the bottom so that there's less chance
+ // for a gap if a details row is later closed (e.g. by user)
+ final int addToBottom = Math.min(rowDiff,
+ getRowCount() - logicalTargetIndex);
+ final int addToTop = rowDiff - addToBottom;
- /*
- * Since fillAndPopulateEscalatorRowsIfNeeded operates on
- * the assumption that index == visual index == logical
- * index, we thank for the added escalator rows, but since
- * they're painted in the wrong CSS position, we need to
- * move them to their actual locations.
- *
- * Note: this is the second (see body.paintInsertRows)
- * occasion where fillAndPopulateEscalatorRowsIfNeeded would
- * behave "more correctly" if it only would add escalator
- * rows to the DOM and appropriate bookkeping, and not
- * actually populate them :/
- */
- moveAndUpdateEscalatorRows(
- Range.withLength(index, addedRows.size()), index,
- nextLastLogicalIndex);
- } else {
- /*
- * TODO [[optimize]]
- *
- * We're scrolled so far down that all rows can't be simply
- * appended at the end, since we might start displaying
- * escalator rows that don't exist. To avoid the mess that
- * is body.paintRemoveRows, this is a dirty hack that dumbs
- * the problem down to a more basic and already-solved
- * problem:
- *
- * 1) scroll all the way up 2) add the missing escalator
- * rows 3) scroll back to the original position.
- *
- * Letting the browser scroll back to our original position
- * will automatically solve any possible overflow problems,
- * since the browser will not allow us to scroll beyond the
- * actual content.
- */
+ if (addToTop > 0) {
+ fillAndPopulateEscalatorRowsIfNeeded(0,
+ oldTopRowLogicalIndex - addToTop, addToTop);
- final double oldScrollTop = getScrollTop();
- setScrollTop(0);
- scroller.onScroll();
- fillAndPopulateEscalatorRowsIfNeeded(index,
- neededEscalatorRowsDiff);
- setScrollTop(oldScrollTop);
- scroller.onScroll();
+ updateTopRowLogicalIndex(-addToTop);
}
- } else if (neededEscalatorRowsDiff < 0) {
- // needs less
-
- final ListIterator<TableRowElement> iter = visualRowOrder
- .listIterator(visualRowOrder.size());
- for (int i = 0; i < -neededEscalatorRowsDiff; i++) {
- final Element last = iter.previous();
- last.removeFromParent();
- iter.remove();
+ if (addToBottom > 0) {
+ fillAndPopulateEscalatorRowsIfNeeded(visualTargetIndex,
+ logicalTargetIndex, addToBottom);
+ }
+ } else if (rowDiff < 0) {
+ // rows need to be removed
+
+ // prioritise removing rows from above the viewport as they are
+ // less likely to be needed in a hurry -- the rows below are
+ // more likely to slide into view when spacer contents are
+ // updated
+
+ // top of visible area before any rows are actually added
+ double scrollTop = getScrollTop();
+
+ // visual index of the first actually visible row, including
+ // spacer
+ int oldFirstVisibleVisualIndex = -1;
+ ListIterator<TableRowElement> iter = visualRowOrder
+ .listIterator(0);
+ for (int i = 0; i < visualRowOrder.size(); ++i) {
+ if (positions.getTop(iter.next()) > scrollTop) {
+ break;
+ }
+ oldFirstVisibleVisualIndex = i;
}
- /*
- * If we were scrolled to the bottom so that we didn't have an
- * extra escalator row at the bottom, we'll probably end up with
- * blank space at the bottom of the escalator, and one extra row
- * above the header.
- *
- * Experimentation idea #1: calculate "scrollbottom" vs content
- * bottom and remove one row from top, rest from bottom. This
- * FAILED, since setHeight has already happened, thus we never
- * will detect ourselves having been scrolled all the way to the
- * bottom.
- */
+ int rowsToRemoveFromAbove = Math.max(0, Math
+ .min(Math.abs(rowDiff), oldFirstVisibleVisualIndex));
- if (!visualRowOrder.isEmpty()) {
- final double firstRowTop = getRowTop(
- visualRowOrder.getFirst());
- final double firstRowMinTop = tBodyScrollTop
- - getDefaultRowHeight();
- if (firstRowTop < firstRowMinTop) {
- final int newLogicalIndex = getLogicalRowIndex(
- visualRowOrder.getLast()) + 1;
- moveAndUpdateEscalatorRows(Range.withOnly(0),
- visualRowOrder.size(), newLogicalIndex);
- updateTopRowLogicalIndex(1);
+ boolean spacersRemovedFromAbove = false;
+ if (rowsToRemoveFromAbove > 0) {
+ double initialSpacerHeightSum = spacerContainer
+ .getSpacerHeightsSum();
+ iter = visualRowOrder.listIterator(0);
+ for (int i = 0; i < rowsToRemoveFromAbove; ++i) {
+ final Element first = iter.next();
+ first.removeFromParent();
+ iter.remove();
+
+ spacerContainer.removeSpacer(oldTopRowLogicalIndex + i);
}
+ spacersRemovedFromAbove = initialSpacerHeightSum != spacerContainer
+ .getSpacerHeightsSum();
+ }
+
+ // if there weren't enough rows above, remove the rest from
+ // below
+ int rowsToRemoveFromBelow = Math.abs(rowDiff)
+ - rowsToRemoveFromAbove;
+ if (rowsToRemoveFromBelow > 0) {
+ iter = visualRowOrder.listIterator(visualRowOrder.size());
+ for (int i = 1; i <= rowsToRemoveFromBelow; ++i) {
+ final Element last = iter.previous();
+ last.removeFromParent();
+ iter.remove();
+
+ spacerContainer.removeSpacer(oldTopRowLogicalIndex
+ + oldVisualRangeLength - i);
+ }
+ }
+
+ updateTopRowLogicalIndex(rowsToRemoveFromAbove);
+
+ if (spacersRemovedFromAbove) {
+ updateRowPositions(oldTopRowLogicalIndex, 0,
+ visualRowOrder.size());
}
+
+ // removing rows might cause a gap at the bottom
+ adjustScrollPositionIfNeeded();
}
- if (neededEscalatorRowsDiff != 0) {
+ if (rowDiff != 0) {
+ scroller.recalculateScrollbarsForVirtualViewport();
+
fireRowVisibilityChangeEvent();
}
@@ -3930,18 +4677,28 @@ public class Escalator extends Widget
Profiler.enter(
"Escalator.BodyRowContainer.reapplyDefaultRowHeights");
- double spacerHeights = 0;
+ double spacerHeightsAboveViewport = spacerContainer
+ .getSpacerHeightsSumUntilPx(
+ verticalScrollbar.getScrollPos());
+ double allSpacerHeights = spacerContainer.getSpacerHeightsSum();
/* step 1: resize and reposition rows */
+
+ // there should be no spacers above the visual range
+ double spacerHeights = 0;
for (int i = 0; i < visualRowOrder.size(); i++) {
TableRowElement tr = visualRowOrder.get(i);
reapplyRowHeight(tr, getDefaultRowHeight());
final int logicalIndex = getTopRowLogicalIndex() + i;
- setRowPosition(tr, 0,
- logicalIndex * getDefaultRowHeight() + spacerHeights);
-
- spacerHeights += spacerContainer.getSpacerHeight(logicalIndex);
+ double y = logicalIndex * getDefaultRowHeight() + spacerHeights;
+ setRowPosition(tr, 0, y);
+ SpacerContainer.SpacerImpl spacer = spacerContainer
+ .getSpacer(logicalIndex);
+ if (spacer != null) {
+ spacer.setPosition(0, y + getDefaultRowHeight());
+ spacerHeights += spacer.getHeight();
+ }
}
/*
@@ -3949,16 +4706,16 @@ public class Escalator extends Widget
* place
*/
- /*
- * This ratio needs to be calculated with the scrollsize (not max
- * scroll position) in order to align the top row with the new
- * scroll position.
- */
- double scrollRatio = verticalScrollbar.getScrollPos()
- / verticalScrollbar.getScrollSize();
+ // scrollRatio has to be calculated without spacers for it to be
+ // comparable between different row heights
+ double scrollRatio = (verticalScrollbar.getScrollPos()
+ - spacerHeightsAboveViewport)
+ / (verticalScrollbar.getScrollSize() - allSpacerHeights);
scroller.recalculateScrollbarsForVirtualViewport();
- verticalScrollbar.setScrollPos((int) (getDefaultRowHeight()
- * getRowCount() * scrollRatio));
+ // spacer heights have to be added back for setting new scrollPos
+ verticalScrollbar.setScrollPos(
+ (int) ((getDefaultRowHeight() * getRowCount() * scrollRatio)
+ + spacerHeightsAboveViewport));
setBodyScrollPosition(horizontalScrollbar.getScrollPos(),
verticalScrollbar.getScrollPos());
scroller.onScroll();
@@ -3968,10 +4725,6 @@ public class Escalator extends Widget
*/
verifyEscalatorCount();
- int logicalLogical = (int) (getRowTop(visualRowOrder.getFirst())
- / getDefaultRowHeight());
- setTopRowLogicalIndex(logicalLogical);
-
Profiler.leave(
"Escalator.BodyRowContainer.reapplyDefaultRowHeights");
}
@@ -4041,7 +4794,9 @@ public class Escalator extends Widget
* position in the DOM will remain undefined.
*/
- // If a spacer was not reordered, it means that it's out of view.
+ // If a spacer was not reordered, it means that it's out of visual
+ // range. This should never happen with default Grid implementations
+ // but it's possible on an extended Escalator.
for (SpacerContainer.SpacerImpl unmovedSpacer : spacers.values()) {
unmovedSpacer.hide();
}
@@ -4105,6 +4860,21 @@ public class Escalator extends Widget
return rowContainingFocus;
}
+ /**
+ * Returns the cell object which contains information about the cell or
+ * spacer the element is in. As an implementation detail each spacer is
+ * a row with one cell, but they are stored in their own container and
+ * share the indexing with the regular rows.
+ *
+ * @param element
+ * The element to get the cell for. If element is not present
+ * in row or spacer container then <code>null</code> is
+ * returned.
+ *
+ * @return the cell reference of the element, or <code>null</code> if
+ * element is not present in the {@link RowContainer} or the
+ * {@link SpacerContainer}.
+ */
@Override
public Cell getCell(Element element) {
Cell cell = super.getCell(element);
@@ -4115,6 +4885,16 @@ public class Escalator extends Widget
// Convert DOM coordinates to logical coordinates for rows
TableRowElement rowElement = (TableRowElement) cell.getElement()
.getParentElement();
+ if (!visualRowOrder.contains(rowElement)) {
+ for (Entry<Integer, SpacerContainer.SpacerImpl> entry : spacerContainer
+ .getSpacers().entrySet()) {
+ if (rowElement.equals(entry.getValue().getRootElement())) {
+ return new Cell(entry.getKey(), cell.getColumn(),
+ cell.getElement());
+ }
+ }
+ return null;
+ }
return new Cell(getLogicalRowIndex(rowElement), cell.getColumn(),
cell.getElement());
}
@@ -4142,17 +4922,20 @@ public class Escalator extends Widget
}
/**
- * <em>Calculates</em> the correct top position of a row at a logical
- * index, regardless if there is one there or not.
+ * <em>Calculates</em> the expected top position of a row at a logical
+ * index, regardless if there is one there currently or not.
* <p>
- * 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.
+ * This method relies on fixed row height (by
+ * {@link #getDefaultRowHeight()}) and can only take into account
+ * spacers that are within visual range. Any scrolling might invalidate
+ * these results, so this method shouldn't be used to estimate scroll
+ * positions.
*
* @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}
+ * @return the position where the row should currently be, were it to
+ * exist
* @see #getRowTop(TableRowElement)
*/
private double getRowTop(int logicalIndex) {
@@ -4204,9 +4987,393 @@ public class Escalator extends Widget
spacerContainer.reapplySpacerWidths();
}
- void scrollToSpacer(int spacerIndex, ScrollDestination destination,
- int padding) {
- spacerContainer.scrollToSpacer(spacerIndex, destination, padding);
+ void scrollToRowSpacerOrBoth(int targetRowIndex,
+ ScrollDestination destination, double padding,
+ ScrollType scrollType) {
+ if (!ensureScrollingAllowed()) {
+ return;
+ }
+ validateScrollDestination(destination, (int) padding);
+ // ignore the special case of -1 index spacer from the row index
+ // validation
+ if (!(targetRowIndex == -1 && !ScrollType.ROW.equals(scrollType))) {
+ // throws an IndexOutOfBoundsException if not valid
+ verifyValidRowIndex(targetRowIndex);
+ }
+ int oldTopRowLogicalIndex = getTopRowLogicalIndex();
+ int visualRangeLength = visualRowOrder.size();
+ int paddingInRows = 0;
+ if (!WidgetUtil.pixelValuesEqual(padding, 0d)) {
+ paddingInRows = (int) Math
+ .ceil(Double.valueOf(padding) / getDefaultRowHeight());
+ }
+
+ // calculate the largest index necessary to include at least
+ // partially below the top of the viewport and the smallest index
+ // necessary to include at least partially above the bottom of the
+ // viewport (target row itself might not be if padding is negative)
+ int firstVisibleIndexIfScrollingUp = targetRowIndex - paddingInRows;
+ int lastVisibleIndexIfScrollingDown = targetRowIndex
+ + paddingInRows;
+
+ int oldFirstBelowIndex = oldTopRowLogicalIndex + visualRangeLength;
+ int newTopRowLogicalIndex;
+ int logicalTargetIndex;
+ switch (destination) {
+ case ANY:
+ // scroll as little as possible, take into account that there
+ // needs to be a buffer row at both ends if there is room for
+ // one
+ boolean newRowsNeededAbove = (firstVisibleIndexIfScrollingUp < oldTopRowLogicalIndex)
+ || (firstVisibleIndexIfScrollingUp == oldTopRowLogicalIndex
+ && targetRowIndex > 0);
+ boolean rowsNeededBelow = (lastVisibleIndexIfScrollingDown >= oldFirstBelowIndex)
+ || ((lastVisibleIndexIfScrollingDown == oldFirstBelowIndex
+ - 1) && (oldFirstBelowIndex < getRowCount()));
+ if (newRowsNeededAbove) {
+ // scroll up, add buffer row if it fits
+ logicalTargetIndex = Math
+ .max(firstVisibleIndexIfScrollingUp - 1, 0);
+ newTopRowLogicalIndex = logicalTargetIndex;
+ } else if (rowsNeededBelow) {
+ // scroll down, add buffer row if it fits
+ newTopRowLogicalIndex = Math.min(
+ lastVisibleIndexIfScrollingDown + 1,
+ getRowCount() - 1) - visualRangeLength + 1;
+ if (newTopRowLogicalIndex
+ - oldTopRowLogicalIndex < visualRangeLength) {
+ // partial recycling, target index at the end of
+ // current range
+ logicalTargetIndex = oldFirstBelowIndex;
+ } else {
+ // full recycling, target index the same as the new
+ // top row index
+ logicalTargetIndex = newTopRowLogicalIndex;
+ }
+ } else {
+ // no need to recycle rows but viewport might need
+ // adjusting regardless
+ logicalTargetIndex = -1;
+ newTopRowLogicalIndex = oldTopRowLogicalIndex;
+ }
+ break;
+ case END:
+ // target row at the bottom of the viewport
+ newTopRowLogicalIndex = Math.min(
+ lastVisibleIndexIfScrollingDown + 1, getRowCount() - 1)
+ - visualRangeLength + 1;
+ if ((newTopRowLogicalIndex > oldTopRowLogicalIndex)
+ && (newTopRowLogicalIndex
+ - oldTopRowLogicalIndex < visualRangeLength)) {
+ // partial recycling, target index at the end of
+ // current range
+ logicalTargetIndex = oldFirstBelowIndex;
+ } else {
+ // full recycling, target index the same as the new
+ // top row index
+ logicalTargetIndex = newTopRowLogicalIndex;
+ }
+ break;
+ case MIDDLE:
+ // target row at the middle of the viewport, padding has to be
+ // zero or we never would have reached this far
+ newTopRowLogicalIndex = targetRowIndex - visualRangeLength / 2;
+ // ensure we don't attempt to go beyond the bottom row
+ if (newTopRowLogicalIndex + visualRangeLength > getRowCount()) {
+ newTopRowLogicalIndex = getRowCount() - visualRangeLength;
+ }
+ if (newTopRowLogicalIndex < oldTopRowLogicalIndex) {
+ logicalTargetIndex = newTopRowLogicalIndex;
+ } else if (newTopRowLogicalIndex > oldTopRowLogicalIndex) {
+ if (newTopRowLogicalIndex
+ - oldTopRowLogicalIndex < visualRangeLength) {
+ // partial recycling, target index at the end of
+ // current range
+ logicalTargetIndex = oldFirstBelowIndex;
+ } else {
+ // full recycling, target index the same as the new
+ // top row index
+ logicalTargetIndex = newTopRowLogicalIndex;
+ }
+ } else {
+ logicalTargetIndex = -1;
+ }
+ break;
+ case START:
+ // target row at the top of the viewport, include buffer
+ // row if there is room for one
+ logicalTargetIndex = Math
+ .max(firstVisibleIndexIfScrollingUp - 1, 0);
+ newTopRowLogicalIndex = logicalTargetIndex;
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Internal: Unsupported ScrollDestination: "
+ + destination.name());
+ }
+
+ // adjust visual range if necessary
+ if (newTopRowLogicalIndex < oldTopRowLogicalIndex) {
+ adjustVisualRangeUpForScrollToRowSpacerOrBoth(
+ oldTopRowLogicalIndex, visualRangeLength,
+ logicalTargetIndex);
+ } else if (newTopRowLogicalIndex > oldTopRowLogicalIndex) {
+ adjustVisualRangeDownForScrollToRowSpacerOrBoth(
+ oldTopRowLogicalIndex, visualRangeLength,
+ newTopRowLogicalIndex, logicalTargetIndex);
+ }
+ boolean rowsWereMoved = newTopRowLogicalIndex != oldTopRowLogicalIndex;
+
+ // update scroll position if necessary
+ double scrollTop = calculateScrollPositionForScrollToRowSpacerOrBoth(
+ targetRowIndex, destination, padding, scrollType);
+ if (scrollTop != getScrollTop()) {
+ setScrollTop(scrollTop);
+ setBodyScrollPosition(tBodyScrollLeft, scrollTop);
+ }
+
+ if (rowsWereMoved) {
+ fireRowVisibilityChangeEvent();
+
+ // schedule updating of the physical indexes
+ domSorter.reschedule();
+ }
+ }
+
+ /**
+ * Checks that scrolling is allowed and resets the scroll position if
+ * it's not.
+ *
+ * @return {@code true} if scrolling is allowed, {@code false} otherwise
+ */
+ private boolean ensureScrollingAllowed() {
+ if (isScrollLocked(Direction.VERTICAL)) {
+ // no scrolling can happen
+ if (getScrollTop() != tBodyScrollTop) {
+ setBodyScrollPosition(tBodyScrollLeft, getScrollTop());
+ }
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Adjusts visual range up for
+ * {@link #scrollToRowSpacerOrBoth(int, ScrollDestination, double, boolean, boolean)},
+ * reuse at your own peril.
+ *
+ * @param oldTopRowLogicalIndex
+ * @param visualRangeLength
+ * @param logicalTargetIndex
+ */
+ private void adjustVisualRangeUpForScrollToRowSpacerOrBoth(
+ int oldTopRowLogicalIndex, int visualRangeLength,
+ int logicalTargetIndex) {
+ // recycle at most the visual range's worth of rows to fill
+ // the gap between the new visualTargetIndex and the existing
+ // rows
+ int rowsToRecycle = Math.min(
+ oldTopRowLogicalIndex - logicalTargetIndex,
+ visualRangeLength);
+ // recycle from the end to the beginning
+ moveAndUpdateEscalatorRows(
+ Range.withLength(visualRangeLength - rowsToRecycle,
+ rowsToRecycle),
+ 0, logicalTargetIndex);
+ // update the index
+ setTopRowLogicalIndex(logicalTargetIndex);
+ }
+
+ /**
+ * Adjusts visual range down for
+ * {@link #scrollToRowSpacerOrBoth(int, ScrollDestination, double, boolean, boolean)},
+ * reuse at your own peril.
+ *
+ * @param oldTopRowLogicalIndex
+ * @param visualRangeLength
+ * @param newTopRowLogicalIndex
+ * @param logicalTargetIndex
+ */
+ private void adjustVisualRangeDownForScrollToRowSpacerOrBoth(
+ int oldTopRowLogicalIndex, int visualRangeLength,
+ int newTopRowLogicalIndex, int logicalTargetIndex) {
+ // recycle at most the visual range's worth of rows to fill
+ // the gap between the new visualTargetIndex and the existing
+ // rows
+ int rowsToRecycle;
+ if (newTopRowLogicalIndex
+ - oldTopRowLogicalIndex >= visualRangeLength) {
+ // full recycling
+ rowsToRecycle = visualRangeLength;
+ } else {
+ // partial recycling
+ rowsToRecycle = newTopRowLogicalIndex - oldTopRowLogicalIndex;
+ }
+ // recycle from the beginning to the end
+ moveAndUpdateEscalatorRows(Range.withLength(0, rowsToRecycle),
+ visualRangeLength, logicalTargetIndex);
+ // update the index
+ setTopRowLogicalIndex(newTopRowLogicalIndex);
+ }
+
+ /**
+ * Calculates scroll position for
+ * {@link #scrollToRowSpacerOrBoth(int, ScrollDestination, double, boolean, boolean)},
+ * reuse at your own peril.
+ *
+ * @param targetRowIndex
+ * @param destination
+ * @param padding
+ * @param scrollType
+ * @return expected scroll position
+ */
+ private double calculateScrollPositionForScrollToRowSpacerOrBoth(
+ int targetRowIndex, ScrollDestination destination,
+ double padding, ScrollType scrollType) {
+ /*
+ * attempting to scroll above first row or below last row would get
+ * automatically corrected later but that causes unnecessary
+ * calculations, so try not to overshoot
+ */
+ double sectionHeight = getHeightOfSection();
+ double rowTop = getRowTop(targetRowIndex);
+ double spacerHeight = spacerContainer
+ .getSpacerHeight(targetRowIndex);
+
+ double scrollTop;
+ switch (destination) {
+ case ANY:
+ if (!ScrollType.SPACER.equals(scrollType)
+ && Math.max(rowTop - padding, 0) < getScrollTop()) {
+ // within visual range but row top above the viewport or not
+ // enough padding, shift a little
+ scrollTop = Math.max(rowTop - padding, 0);
+ } else if (ScrollType.SPACER.equals(scrollType)
+ && Math.max(rowTop + getDefaultRowHeight() - padding,
+ 0) < getScrollTop()) {
+ // within visual range but spacer top above the viewport or
+ // not enough padding, shift a little
+ scrollTop = Math
+ .max(rowTop + getDefaultRowHeight() - padding, 0);
+ } else if (ScrollType.ROW.equals(scrollType)
+ && rowTop + getDefaultRowHeight()
+ + padding > getScrollTop() + sectionHeight) {
+ // within visual range but end of row below the viewport
+ // or not enough padding, shift a little
+ scrollTop = rowTop + getDefaultRowHeight() - sectionHeight
+ + padding;
+ // ensure that we don't overshoot beyond bottom
+ scrollTop = Math.min(scrollTop,
+ getRowTop(getRowCount() - 1) + getDefaultRowHeight()
+ + spacerContainer
+ .getSpacerHeight(getRowCount() - 1)
+ - sectionHeight);
+ // if padding is set we want to overshoot or undershoot,
+ // otherwise make sure the top of the row is in view
+ if (padding == 0) {
+ scrollTop = Math.min(scrollTop, rowTop);
+ }
+ } else if (rowTop + getDefaultRowHeight() + spacerHeight
+ + padding > getScrollTop() + sectionHeight) {
+ // within visual range but end of spacer below the viewport
+ // or not enough padding, shift a little
+ scrollTop = rowTop + getDefaultRowHeight() + spacerHeight
+ - sectionHeight + padding;
+ // ensure that we don't overshoot beyond bottom
+ scrollTop = Math.min(scrollTop,
+ getRowTop(getRowCount() - 1) + getDefaultRowHeight()
+ + spacerContainer
+ .getSpacerHeight(getRowCount() - 1)
+ - sectionHeight);
+ // if padding is set we want to overshoot or undershoot,
+ // otherwise make sure the top of the row or spacer is
+ // in view
+ if (padding == 0) {
+ if (ScrollType.SPACER.equals(scrollType)) {
+ scrollTop = Math.min(scrollTop,
+ rowTop + getDefaultRowHeight());
+ } else {
+ scrollTop = Math.min(scrollTop, rowTop);
+ }
+ }
+ } else {
+ // we are fine where we are
+ scrollTop = getScrollTop();
+ }
+ break;
+ case END:
+ if (ScrollType.ROW.equals(scrollType)
+ && rowTop + getDefaultRowHeight()
+ + padding > getScrollTop() + sectionHeight) {
+ // within visual range but end of row below the viewport
+ // or not enough padding, shift a little
+ scrollTop = rowTop + getDefaultRowHeight() - sectionHeight
+ + padding;
+ // ensure that we don't overshoot beyond bottom
+ scrollTop = Math.min(scrollTop,
+ getRowTop(getRowCount() - 1) + getDefaultRowHeight()
+ + spacerContainer
+ .getSpacerHeight(getRowCount() - 1)
+ - sectionHeight);
+ } else if (rowTop + getDefaultRowHeight() + spacerHeight
+ + padding > getScrollTop() + sectionHeight) {
+ // within visual range but end of spacer below the viewport
+ // or not enough padding, shift a little
+ scrollTop = rowTop + getDefaultRowHeight() + spacerHeight
+ - sectionHeight + padding;
+ // ensure that we don't overshoot beyond bottom
+ scrollTop = Math.min(scrollTop,
+ getRowTop(getRowCount()) - sectionHeight);
+ } else {
+ // we are fine where we are
+ scrollTop = getScrollTop();
+ }
+ break;
+ case MIDDLE:
+ double center;
+ if (ScrollType.ROW.equals(scrollType)) {
+ // center the row itself
+ center = rowTop + (getDefaultRowHeight() / 2.0);
+ } else if (ScrollType.ROW_AND_SPACER.equals(scrollType)) {
+ // center both
+ center = rowTop
+ + ((getDefaultRowHeight() + spacerHeight) / 2.0);
+ } else {
+ // center the spacer
+ center = rowTop + getDefaultRowHeight()
+ + (spacerHeight / 2.0);
+ }
+ scrollTop = center - Math.ceil(sectionHeight / 2.0);
+ // ensure that we don't overshoot beyond bottom
+ scrollTop = Math.min(scrollTop,
+ getRowTop(getRowCount() - 1) + getDefaultRowHeight()
+ + spacerContainer
+ .getSpacerHeight(getRowCount() - 1)
+ - sectionHeight);
+ // ensure that we don't overshoot beyond top
+ scrollTop = Math.max(0, scrollTop);
+ break;
+ case START:
+ if (!ScrollType.SPACER.equals(scrollType)
+ && Math.max(rowTop - padding, 0) < getScrollTop()) {
+ // row top above the viewport or not enough padding, shift a
+ // little
+ scrollTop = Math.max(rowTop - padding, 0);
+ } else if (ScrollType.SPACER.equals(scrollType)
+ && Math.max(rowTop + getDefaultRowHeight() - padding,
+ 0) < getScrollTop()) {
+ // spacer top above the viewport or not enough padding,
+ // shift a little
+ scrollTop = Math
+ .max(rowTop + getDefaultRowHeight() - padding, 0);
+ } else {
+ scrollTop = getScrollTop();
+ }
+ break;
+ default:
+ scrollTop = getScrollTop();
+ }
+ return scrollTop;
}
@Override
@@ -4845,6 +6012,17 @@ public class Escalator extends Widget
UIObject.setStylePrimaryName(deco, style + "-spacer-deco");
}
+ /**
+ * Clear spacer height without moving other contents.
+ *
+ * @see #setHeight(double)
+ */
+ private void clearHeight() {
+ height = 0;
+ root.getStyle().setHeight(0, Unit.PX);
+ updateDecoratorGeometry(0);
+ }
+
public void setHeight(double height) {
assert height >= 0 : "Height must be more >= 0 (was " + height
@@ -4996,7 +6174,13 @@ public class Escalator extends Widget
/**
* Updates the spacer's visibility parameters, based on whether it
* is being currently visible or not.
+ *
+ * @deprecated Escalator no longer uses this logic at initialisation
+ * as there can only be a limited number of spacers and
+ * hidden spacers within visual range interfere with
+ * position calculations.
*/
+ @Deprecated
public void updateVisibility() {
if (isInViewport()) {
show();
@@ -5015,15 +6199,13 @@ public class Escalator extends Widget
public void show() {
getRootElement().getStyle().clearDisplay();
getDecoElement().getStyle().clearDisplay();
- Escalator.this.fireEvent(
- new SpacerVisibilityChangedEvent(getRow(), true));
+ fireEvent(new SpacerVisibilityChangedEvent(getRow(), true));
}
public void hide() {
getRootElement().getStyle().setDisplay(Display.NONE);
getDecoElement().getStyle().setDisplay(Display.NONE);
- Escalator.this.fireEvent(
- new SpacerVisibilityChangedEvent(getRow(), false));
+ fireEvent(new SpacerVisibilityChangedEvent(getRow(), false));
}
/**
@@ -5136,23 +6318,8 @@ public class Escalator extends Widget
assert !destination.equals(ScrollDestination.MIDDLE)
|| padding != 0 : "destination/padding check should be done before this method";
- if (!rowIndexToSpacer.containsKey(spacerIndex)) {
- throw new IllegalArgumentException(
- "No spacer open at index " + spacerIndex);
- }
-
- SpacerImpl spacer = rowIndexToSpacer.get(spacerIndex);
- double targetStartPx = spacer.getTop();
- double targetEndPx = targetStartPx + spacer.getHeight();
-
- Range viewportPixels = getViewportPixels();
- double viewportStartPx = viewportPixels.getStart();
- double viewportEndPx = viewportPixels.getEnd();
-
- double scrollTop = getScrollPos(destination, targetStartPx,
- targetEndPx, viewportStartPx, viewportEndPx, padding);
-
- setScrollTop(scrollTop);
+ body.scrollToRowSpacerOrBoth(spacerIndex, destination, padding,
+ ScrollType.SPACER);
}
public void reapplySpacerWidths() {
@@ -5163,12 +6330,29 @@ public class Escalator extends Widget
}
}
+ /**
+ * @deprecated This method is no longer used by Escalator and is likely
+ * to be removed soon.
+ *
+ * @param removedRowsRange
+ */
+ @Deprecated
public void paintRemoveSpacers(Range removedRowsRange) {
removeSpacers(removedRowsRange);
shiftSpacersByRows(removedRowsRange.getStart(),
-removedRowsRange.length());
}
+ /**
+ * Removes spacers of the given range without moving other contents.
+ * <p>
+ * NOTE: Changed functionality since 8.9. Previous incarnation of this
+ * method updated the positions of all the contents below the first
+ * removed spacer.
+ *
+ * @param removedRange
+ * logical range of spacers to remove
+ */
@SuppressWarnings("boxing")
public void removeSpacers(Range removedRange) {
@@ -5180,15 +6364,16 @@ public class Escalator extends Widget
return;
}
- for (SpacerImpl spacer : removedSpacers.values()) {
- /*
- * [[optimization]] TODO: Each invocation of the setHeight
- * method has a cascading effect in the DOM. if this proves to
- * be slow, the DOM offset could be updated as a batch.
- */
+ double specialSpacerHeight = removedRange.contains(-1)
+ ? getSpacerHeight(-1)
+ : 0;
+
+ for (Entry<Integer, SpacerImpl> entry : removedSpacers.entrySet()) {
+ SpacerImpl spacer = entry.getValue();
+ rowIndexToSpacer.remove(entry.getKey());
destroySpacerContent(spacer);
- spacer.setHeight(0); // resets row offsets
+ spacer.clearHeight();
spacer.getRootElement().removeFromParent();
spacer.getDecoElement().removeFromParent();
}
@@ -5200,6 +6385,15 @@ public class Escalator extends Widget
spacerScrollerRegistration.removeHandler();
spacerScrollerRegistration = null;
}
+
+ // if a rowless spacer at the top got removed, all rows and spacers
+ // need to be moved up accordingly
+ if (!WidgetUtil.pixelValuesEqual(specialSpacerHeight, 0)) {
+ double scrollDiff = Math.min(specialSpacerHeight,
+ getScrollTop());
+ body.moveViewportAndContent(null, -specialSpacerHeight,
+ -specialSpacerHeight, -scrollDiff);
+ }
}
public Map<Integer, SpacerImpl> getSpacers() {
@@ -5427,7 +6621,8 @@ public class Escalator extends Widget
/**
* Gets the amount of pixels occupied by spacers until a logical row
- * index.
+ * index. The spacer of the row corresponding with the given index isn't
+ * included.
*
* @param logicalIndex
* a logical row index
@@ -5500,7 +6695,8 @@ public class Escalator extends Widget
initSpacerContent(spacer);
- body.sortDomElements();
+ // schedule updating of the physical indexes
+ body.domSorter.reschedule();
}
private void updateExistingSpacer(int rowIndex, double newHeight) {
@@ -5572,7 +6768,7 @@ public class Escalator extends Widget
assert getElement().isOrHasChild(spacer
.getElement()) : "Spacer element somehow got detached from Escalator during attaching";
- spacer.updateVisibility();
+ spacer.show();
}
public String getSubPartName(Element subElement) {
@@ -5607,10 +6803,11 @@ public class Escalator extends Widget
}
/**
- * Shifts spacers at and after a specific row by an amount of rows.
+ * Shifts spacers at and after a specific row by an amount of rows that
+ * don't contain spacers of their own.
* <p>
- * This moves both their associated row index and also their visual
- * placement.
+ * This moves both their associated logical row index and also their
+ * visual placement.
* <p>
* <em>Note:</em> This method does not check for the validity of any
* arguments.
@@ -5639,6 +6836,40 @@ public class Escalator extends Widget
}
}
+ /**
+ * Update the associated logical row indexes for spacers without moving
+ * their actual positions.
+ * <p>
+ * <em>Note:</em> This method does not check for the validity of any
+ * arguments.
+ *
+ * @param startIndex
+ * the previous logical index of first row to update
+ * @param endIndex
+ * the previous logical index of first row that doesn't need
+ * updating anymore
+ * @param numberOfRows
+ * the number of rows to shift the associated logical index
+ * with. A positive value is downwards, a negative value is
+ * upwards.
+ */
+ private void updateSpacerIndexesForRowAndAfter(int startIndex,
+ int endIndex, int numberOfRows) {
+ List<SpacerContainer.SpacerImpl> spacers = new ArrayList<>(
+ getSpacersForRowAndAfter(startIndex));
+ spacers.removeAll(getSpacersForRowAndAfter(endIndex));
+ if (numberOfRows < 0) {
+ for (SpacerContainer.SpacerImpl spacer : spacers) {
+ spacer.setRowIndex(spacer.getRow() + numberOfRows);
+ }
+ } else {
+ for (int i = spacers.size() - 1; i >= 0; --i) {
+ SpacerContainer.SpacerImpl spacer = spacers.get(i);
+ spacer.setRowIndex(spacer.getRow() + numberOfRows);
+ }
+ }
+ }
+
private void updateSpacerDecosVisibility() {
final Range visibleRowRange = getVisibleRowRange();
Collection<SpacerImpl> visibleSpacers = rowIndexToSpacer
@@ -5745,6 +6976,10 @@ public class Escalator extends Widget
}
}
+ enum ScrollType {
+ ROW, SPACER, ROW_AND_SPACER
+ }
+
// abs(atan(y/x))*(180/PI) = n deg, x = 1, solve y
/**
* The solution to
@@ -5847,6 +7082,13 @@ public class Escalator extends Widget
private boolean layoutIsScheduled = false;
private ScheduledCommand layoutCommand = () -> {
+ // ensure that row heights have been set or auto-detected if
+ // auto-detection is already possible, because visibility changes might
+ // not trigger the default check that happens in onLoad()
+ header.autodetectRowHeightLater();
+ body.autodetectRowHeightLater();
+ footer.autodetectRowHeightLater();
+
recalculateElementSizes();
layoutIsScheduled = false;
};
@@ -6031,6 +7273,9 @@ public class Escalator extends Widget
protected void onLoad() {
super.onLoad();
+ // ensure that row heights have been set or auto-detected if
+ // auto-detection is already possible, if not the check will be
+ // performed again in layoutCommand
header.autodetectRowHeightLater();
body.autodetectRowHeightLater();
footer.autodetectRowHeightLater();
@@ -6400,11 +7645,9 @@ public class Escalator extends Widget
public void scrollToRow(final int rowIndex,
final ScrollDestination destination, final int padding)
throws IndexOutOfBoundsException, IllegalArgumentException {
- Scheduler.get().scheduleFinally(() -> {
- validateScrollDestination(destination, padding);
- verifyValidRowIndex(rowIndex);
- scroller.scrollToRow(rowIndex, destination, padding);
- });
+ verifyValidRowIndex(rowIndex);
+ body.scrollToRowSpacerOrBoth(rowIndex, destination, padding,
+ ScrollType.ROW);
}
private void verifyValidRowIndex(final int rowIndex) {
@@ -6416,7 +7659,7 @@ public class Escalator extends Widget
/**
* 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
+ * visible and there is at least {@literal padding} pixels to the given
* scroll destination.
*
* @since 7.5.0
@@ -6437,8 +7680,8 @@ public class Escalator extends Widget
public void scrollToSpacer(final int spacerIndex,
ScrollDestination destination, final int padding)
throws IllegalArgumentException {
- validateScrollDestination(destination, padding);
- body.scrollToSpacer(spacerIndex, destination, padding);
+ body.scrollToRowSpacerOrBoth(spacerIndex, destination, padding,
+ ScrollType.SPACER);
}
/**
@@ -6468,53 +7711,13 @@ public class Escalator extends Widget
public void scrollToRowAndSpacer(final int rowIndex,
final ScrollDestination destination, final int padding)
throws IllegalArgumentException {
+ // wait for the layout phase to finish
Scheduler.get().scheduleFinally(() -> {
- 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);
+ body.scrollToRowSpacerOrBoth(rowIndex, destination, padding,
+ ScrollType.ROW_AND_SPACER);
});
}
@@ -6608,12 +7811,8 @@ public class Escalator extends Widget
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;
+ int visibleRangeStart = body.getTopRowLogicalIndex();
+ int visibleRowCount = body.visualRowOrder.size();
fireEvent(new RowVisibilityChangeEvent(visibleRangeStart,
visibleRowCount));
} else {
@@ -6695,6 +7894,11 @@ public class Escalator extends Widget
* @see #setHeightMode(HeightMode)
*/
public void setHeightByRows(double rows) throws IllegalArgumentException {
+ if (heightMode == HeightMode.UNDEFINED && body.insertingOrRemoving) {
+ // this will be called again once the operation is finished, ignore
+ // for now
+ return;
+ }
if (rows < 0) {
throw new IllegalArgumentException(
"The number of rows must be a positive number.");
@@ -7037,21 +8241,10 @@ public class Escalator extends Widget
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
+ // If wanted row is not visible, we need to scroll there.
+ // 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")) {
@@ -7119,6 +8312,10 @@ public class Escalator extends Widget
private Element getSubPartElementSpacer(SubPartArguments args) {
if ("spacer".equals(args.getType()) && args.getIndicesLength() == 1) {
+ // If spacer's row is not visible, we need to scroll there.
+ // Scrolling might be a no-op if row is already in the viewport.
+ scrollToSpacer(args.getIndex(0), ScrollDestination.ANY, 0);
+
return body.spacerContainer.getSubPartElement(args.getIndex(0));
} else {
return null;
diff --git a/client/src/main/java/com/vaadin/client/widgets/Grid.java b/client/src/main/java/com/vaadin/client/widgets/Grid.java
index 2ca2ffa961..3a67dcfd3b 100755
--- a/client/src/main/java/com/vaadin/client/widgets/Grid.java
+++ b/client/src/main/java/com/vaadin/client/widgets/Grid.java
@@ -3829,7 +3829,7 @@ public class Grid<T> extends ResizeComposite implements HasSelectionHandlers<T>,
* height, but the spacer cell (td) has the borders, which
* should go on top of the previous row and next row.
*/
- double contentHeight;
+ final double contentHeight;
if (detailsGenerator instanceof HeightAwareDetailsGenerator) {
HeightAwareDetailsGenerator sadg = (HeightAwareDetailsGenerator) detailsGenerator;
contentHeight = sadg.getDetailsHeight(rowIndex);
@@ -3839,8 +3839,32 @@ public class Grid<T> extends ResizeComposite implements HasSelectionHandlers<T>,
}
double borderTopAndBottomHeight = WidgetUtil
.getBorderTopAndBottomThickness(spacerElement);
- double measuredHeight = contentHeight
- + borderTopAndBottomHeight;
+ double measuredHeight = 0d;
+ if (contentHeight > 0) {
+ measuredHeight = contentHeight + borderTopAndBottomHeight;
+ } else {
+ Scheduler.get().scheduleFinally(() -> {
+ // make sure the spacer hasn't got removed
+ if (spacer.getElement().getParentElement() != null) {
+ // re-check the height
+ double confirmedContentHeight = WidgetUtil
+ .getRequiredHeightBoundingClientRectDouble(
+ element);
+ if (confirmedContentHeight > 0) {
+ double confirmedMeasuredHeight = confirmedContentHeight
+ + WidgetUtil
+ .getBorderTopAndBottomThickness(
+ spacer.getElement());
+ escalator.getBody().setSpacer(spacer.getRow(),
+ confirmedMeasuredHeight);
+ if (getHeightMode() == HeightMode.UNDEFINED) {
+ setHeightByRows(getEscalator().getBody()
+ .getRowCount());
+ }
+ }
+ }
+ });
+ }
assert getElement().isOrHasChild(
spacerElement) : "The spacer element wasn't in the DOM during measurement, but was assumed to be.";
spacerHeight = measuredHeight;
@@ -7208,6 +7232,9 @@ public class Grid<T> extends ResizeComposite implements HasSelectionHandlers<T>,
@Override
public void dataRemoved(int firstIndex, int numberOfItems) {
+ for (int i = 0; i < numberOfItems; ++i) {
+ visibleDetails.remove(firstIndex + i);
+ }
escalator.getBody().removeRows(firstIndex,
numberOfItems);
Range removed = Range.withLength(firstIndex,
@@ -9213,17 +9240,6 @@ public class Grid<T> extends ResizeComposite implements HasSelectionHandlers<T>,
recalculateColumnWidths();
}
- if (getEscalatorInnerHeight() != autoColumnWidthsRecalculator.lastCalculatedInnerHeight) {
- Scheduler.get().scheduleFinally(() -> {
- // Trigger re-calculation of all row positions.
- RowContainer.BodyRowContainer body = getEscalator()
- .getBody();
- if (!body.isAutodetectingRowHeightLater()) {
- body.setDefaultRowHeight(body.getDefaultRowHeight());
- }
- });
- }
-
// Vertical resizing could make editor positioning invalid so it
// needs to be recalculated on resize
if (isEditorActive()) {
diff --git a/server/src/main/java/com/vaadin/ui/Grid.java b/server/src/main/java/com/vaadin/ui/Grid.java
index fa6237a83a..104e90c3b9 100644
--- a/server/src/main/java/com/vaadin/ui/Grid.java
+++ b/server/src/main/java/com/vaadin/ui/Grid.java
@@ -737,6 +737,7 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents,
if (this.generator != generator) {
removeAllComponents();
}
+ getState().hasDetailsGenerator = generator != null;
this.generator = generator;
visibleDetails.forEach(this::refresh);
}
diff --git a/shared/src/main/java/com/vaadin/shared/ui/grid/DetailsManagerState.java b/shared/src/main/java/com/vaadin/shared/ui/grid/DetailsManagerState.java
index 1ac8987f01..cb202f96bb 100644
--- a/shared/src/main/java/com/vaadin/shared/ui/grid/DetailsManagerState.java
+++ b/shared/src/main/java/com/vaadin/shared/ui/grid/DetailsManagerState.java
@@ -22,4 +22,12 @@ package com.vaadin.shared.ui.grid;
*/
public class DetailsManagerState extends AbstractGridExtensionState {
+ /**
+ * For informing the connector when details handling can be skipped
+ * altogether as it's not possible to have any details rows without a
+ * generator.
+ *
+ * @since 8.9
+ */
+ public boolean hasDetailsGenerator = false;
}
diff --git a/uitest/src/main/java/com/vaadin/tests/components/treegrid/TreeGridBigDetailsManager.java b/uitest/src/main/java/com/vaadin/tests/components/treegrid/TreeGridBigDetailsManager.java
index 5699ee4372..8fb9a358e4 100644
--- a/uitest/src/main/java/com/vaadin/tests/components/treegrid/TreeGridBigDetailsManager.java
+++ b/uitest/src/main/java/com/vaadin/tests/components/treegrid/TreeGridBigDetailsManager.java
@@ -48,7 +48,7 @@ public class TreeGridBigDetailsManager extends AbstractTestUI {
treeGrid.setSizeFull();
treeGrid.addColumn(String::toString).setCaption("String")
.setId("string");
- treeGrid.addColumn((i) -> "--").setCaption("Nothing");
+ treeGrid.addColumn((i) -> items.indexOf(i)).setCaption("Index");
treeGrid.setHierarchyColumn("string");
treeGrid.setDetailsGenerator(
row -> new Label("details for " + row.toString()));
@@ -77,21 +77,54 @@ public class TreeGridBigDetailsManager extends AbstractTestUI {
treeGrid.collapse(items);
});
collapseAll.setId("collapseAll");
+ @SuppressWarnings("deprecation")
Button scrollTo55 = new Button("Scroll to 55",
event -> treeGrid.scrollTo(55));
scrollTo55.setId("scrollTo55");
scrollTo55.setVisible(false);
+ Button scrollTo3055 = new Button("Scroll to 3055",
+ event -> treeGrid.scrollTo(3055));
+ scrollTo3055.setId("scrollTo3055");
+ scrollTo3055.setVisible(false);
+ Button scrollToEnd = new Button("Scroll to end",
+ event -> treeGrid.scrollToEnd());
+ scrollToEnd.setId("scrollToEnd");
+ scrollToEnd.setVisible(false);
+ Button scrollToStart = new Button("Scroll to start",
+ event -> treeGrid.scrollToStart());
+ scrollToStart.setId("scrollToStart");
+ scrollToStart.setVisible(false);
+
+ Button toggle15 = new Button("Toggle 15",
+ event -> treeGrid.setDetailsVisible(items.get(15),
+ !treeGrid.isDetailsVisible(items.get(15))));
+ toggle15.setId("toggle15");
+ toggle15.setVisible(false);
+
+ Button toggle3000 = new Button("Toggle 3000",
+ event -> treeGrid.setDetailsVisible(items.get(3000),
+ !treeGrid.isDetailsVisible(items.get(3000))));
+ toggle3000.setId("toggle3000");
+ toggle3000.setVisible(false);
+
Button addGrid = new Button("Add grid", event -> {
addComponent(treeGrid);
getLayout().setExpandRatio(treeGrid, 2);
scrollTo55.setVisible(true);
+ scrollTo3055.setVisible(true);
+ scrollToEnd.setVisible(true);
+ scrollToStart.setVisible(true);
+ toggle15.setVisible(true);
+ toggle3000.setVisible(true);
});
addGrid.setId("addGrid");
addComponents(
new HorizontalLayout(showDetails, hideDetails, expandAll,
collapseAll),
- new HorizontalLayout(addGrid, scrollTo55));
+ new HorizontalLayout(scrollTo55, scrollTo3055, scrollToEnd,
+ scrollToStart),
+ new HorizontalLayout(addGrid, toggle15, toggle3000));
getLayout().getParent().setHeight("100%");
getLayout().setHeight("100%");
diff --git a/uitest/src/main/java/com/vaadin/tests/widgetset/client/grid/EscalatorBasicClientFeaturesWidget.java b/uitest/src/main/java/com/vaadin/tests/widgetset/client/grid/EscalatorBasicClientFeaturesWidget.java
index 2d7dd5cc37..941f1ce928 100644
--- a/uitest/src/main/java/com/vaadin/tests/widgetset/client/grid/EscalatorBasicClientFeaturesWidget.java
+++ b/uitest/src/main/java/com/vaadin/tests/widgetset/client/grid/EscalatorBasicClientFeaturesWidget.java
@@ -1,9 +1,12 @@
package com.vaadin.tests.widgetset.client.grid;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import com.google.gwt.core.client.Duration;
+import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.TableCellElement;
import com.google.gwt.user.client.DOM;
@@ -16,6 +19,8 @@ import com.vaadin.client.widget.escalator.RowContainer;
import com.vaadin.client.widget.escalator.RowContainer.BodyRowContainer;
import com.vaadin.client.widget.escalator.Spacer;
import com.vaadin.client.widget.escalator.SpacerUpdater;
+import com.vaadin.client.widget.escalator.events.SpacerIndexChangedEvent;
+import com.vaadin.client.widget.escalator.events.SpacerIndexChangedHandler;
import com.vaadin.client.widgets.Escalator;
import com.vaadin.shared.ui.grid.ScrollDestination;
import com.vaadin.tests.widgetset.client.v7.grid.PureGWTTestApplication;
@@ -156,6 +161,7 @@ public class EscalatorBasicClientFeaturesWidget
private int rowCounter = 0;
private final List<Integer> columns = new ArrayList<>();
private final List<Integer> rows = new ArrayList<>();
+ private final Map<Integer, Integer> spacers = new HashMap<>();
@SuppressWarnings("boxing")
public void insertRows(final int offset, final int amount) {
@@ -247,6 +253,11 @@ public class EscalatorBasicClientFeaturesWidget
cell.setColSpan(2);
}
}
+ if (spacers.containsKey(cell.getRow()) && !escalator
+ .getBody().spacerExists(cell.getRow())) {
+ escalator.getBody().setSpacer(cell.getRow(),
+ spacers.get(cell.getRow()));
+ }
}
@Override
@@ -262,7 +273,12 @@ public class EscalatorBasicClientFeaturesWidget
public void removeRows(final int offset, final int amount) {
for (int i = 0; i < amount; i++) {
rows.remove(offset);
+ if (spacers.containsKey(offset + i)) {
+ spacers.remove(offset + i);
+ }
}
+ // the following spacers get their indexes updated through
+ // SpacerIndexChangedHandler
}
public void removeColumns(final int offset, final int amount) {
@@ -313,6 +329,21 @@ public class EscalatorBasicClientFeaturesWidget
createFrozenMenu();
createColspanMenu();
createSpacerMenu();
+
+ escalator.addHandler(new SpacerIndexChangedHandler() {
+ @Override
+ public void onSpacerIndexChanged(SpacerIndexChangedEvent event) {
+ // remove spacer from old index and move to new index
+ Integer height = data.spacers.remove(event.getOldIndex());
+ if (height != null) {
+ data.spacers.put(event.getNewIndex(), height);
+ } else {
+ // no height, make sure the new index doesn't
+ // point to anything else either
+ data.spacers.remove(event.getNewIndex());
+ }
+ }
+ }, SpacerIndexChangedEvent.TYPE);
}
private void createFrozenMenu() {
@@ -558,11 +589,24 @@ public class EscalatorBasicClientFeaturesWidget
@Override
public void init(Spacer spacer) {
spacer.getElement().appendChild(DOM.createInputText());
+ updateRowPositions(spacer);
}
@Override
public void destroy(Spacer spacer) {
spacer.getElement().removeAllChildren();
+ updateRowPositions(spacer);
+ }
+
+ private void updateRowPositions(Spacer spacer) {
+ if (spacer.getRow() < escalator.getBody()
+ .getRowCount()) {
+ Scheduler.get().scheduleFinally(() -> {
+ escalator.getBody().updateRowPositions(
+ spacer.getRow(),
+ escalator.getBody().getRowCount());
+ });
+ }
}
}), menupath);
@@ -575,12 +619,18 @@ public class EscalatorBasicClientFeaturesWidget
private void createSpacersMenuForRow(final int rowIndex,
String[] menupath) {
menupath = new String[] { menupath[0], menupath[1], "Row " + rowIndex };
- addMenuCommand("Set 100px",
- () -> escalator.getBody().setSpacer(rowIndex, 100), menupath);
- addMenuCommand("Set 50px",
- () -> escalator.getBody().setSpacer(rowIndex, 50), menupath);
- addMenuCommand("Remove",
- () -> escalator.getBody().setSpacer(rowIndex, -1), menupath);
+ addMenuCommand("Set 100px", () -> {
+ escalator.getBody().setSpacer(rowIndex, 100);
+ data.spacers.put(rowIndex, 100);
+ }, menupath);
+ addMenuCommand("Set 50px", () -> {
+ escalator.getBody().setSpacer(rowIndex, 50);
+ data.spacers.put(rowIndex, 50);
+ }, menupath);
+ addMenuCommand("Remove", () -> {
+ escalator.getBody().setSpacer(rowIndex, -1);
+ data.spacers.remove(rowIndex);
+ }, menupath);
addMenuCommand("Scroll here (ANY, 0)", () -> escalator
.scrollToSpacer(rowIndex, ScrollDestination.ANY, 0), menupath);
addMenuCommand("Scroll here row+spacer below (ANY, 0)", () -> escalator
@@ -596,6 +646,9 @@ public class EscalatorBasicClientFeaturesWidget
} else {
container.insertRows(offset, number);
}
+ if (container.getRowCount() > offset + number) {
+ container.refreshRows(offset + number, container.getRowCount());
+ }
}
private void removeRows(final RowContainer container, int offset,
@@ -606,6 +659,9 @@ public class EscalatorBasicClientFeaturesWidget
} else {
container.removeRows(offset, number);
}
+ if (container.getRowCount() > offset) {
+ container.refreshRows(offset, container.getRowCount());
+ }
}
private void insertColumns(final int offset, final int number) {
diff --git a/uitest/src/main/java/com/vaadin/tests/widgetset/client/grid/EscalatorProxy.java b/uitest/src/main/java/com/vaadin/tests/widgetset/client/grid/EscalatorProxy.java
index fe6f7ab94b..5d5148109b 100644
--- a/uitest/src/main/java/com/vaadin/tests/widgetset/client/grid/EscalatorProxy.java
+++ b/uitest/src/main/java/com/vaadin/tests/widgetset/client/grid/EscalatorProxy.java
@@ -125,6 +125,11 @@ public class EscalatorProxy extends Escalator {
throw new UnsupportedOperationException(
"setNewRowCallback is not supported");
}
+
+ @Override
+ public void updateRowPositions(int index, int numberOfRows) {
+ rowContainer.updateRowPositions(index, numberOfRows);
+ }
}
private class RowContainerProxy implements RowContainer {
diff --git a/uitest/src/test/java/com/vaadin/tests/components/grid/GridComponentsTest.java b/uitest/src/test/java/com/vaadin/tests/components/grid/GridComponentsTest.java
index 3ff3d61809..d0122cd9df 100644
--- a/uitest/src/test/java/com/vaadin/tests/components/grid/GridComponentsTest.java
+++ b/uitest/src/test/java/com/vaadin/tests/components/grid/GridComponentsTest.java
@@ -1,5 +1,9 @@
package com.vaadin.tests.components.grid;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
import java.util.Locale;
import java.util.stream.IntStream;
import java.util.stream.Stream;
@@ -21,10 +25,6 @@ import com.vaadin.testbench.elements.TextFieldElement;
import com.vaadin.testbench.parallel.BrowserUtil;
import com.vaadin.tests.tb3.MultiBrowserTest;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
public class GridComponentsTest extends MultiBrowserTest {
@Test
@@ -219,19 +219,14 @@ public class GridComponentsTest extends MultiBrowserTest {
getScrollLeft(grid));
// Navigate back to fully visible TextField
- new Actions(getDriver()).sendKeys(Keys.chord(Keys.SHIFT, Keys.TAB))
- .perform();
+ pressKeyWithModifier(Keys.SHIFT, Keys.TAB);
assertEquals(
"Grid should not scroll when focusing the text field again. ",
scrollMax, getScrollLeft(grid));
// Navigate to out of viewport TextField in Header
- new Actions(getDriver()).sendKeys(Keys.chord(Keys.SHIFT, Keys.TAB))
- .perform();
- // After Chrome 75, sendkeys issues
- if (BrowserUtil.isChrome(getDesiredCapabilities())) {
- grid.getHeaderCell(1, 0).findElement(By.id("headerField")).click();
- }
+ pressKeyWithModifier(Keys.SHIFT, Keys.TAB);
+
assertEquals("Focus should be in TextField in Header", "headerField",
getFocusedElement().getAttribute("id"));
assertEquals("Grid should've scrolled back to start.", 0,
@@ -248,10 +243,30 @@ public class GridComponentsTest extends MultiBrowserTest {
// Navigate to currently out of viewport TextField on Row 8
new Actions(getDriver()).sendKeys(Keys.TAB, Keys.TAB).perform();
- assertTrue("Grid should be scrolled to show row 7",
+ assertTrue("Grid should be scrolled to show row 8",
Integer.parseInt(grid.getVerticalScroller()
.getAttribute("scrollTop")) > scrollTopRow7);
+ // Focus button in first visible row of Grid
+ grid.getCell(2, 2).findElement(By.id("row_2")).click();
+ int scrollTopRow2 = Integer
+ .parseInt(grid.getVerticalScroller().getAttribute("scrollTop"));
+
+ // Navigate to currently out of viewport Button on Row 1
+ pressKeyWithModifier(Keys.SHIFT, Keys.TAB);
+ pressKeyWithModifier(Keys.SHIFT, Keys.TAB);
+ int scrollTopRow1 = Integer
+ .parseInt(grid.getVerticalScroller().getAttribute("scrollTop"));
+ assertTrue("Grid should be scrolled to show row 1",
+ scrollTopRow1 < scrollTopRow2);
+
+ // Continue further to the very first row
+ pressKeyWithModifier(Keys.SHIFT, Keys.TAB);
+ pressKeyWithModifier(Keys.SHIFT, Keys.TAB);
+ assertTrue("Grid should be scrolled to show row 0",
+ Integer.parseInt(grid.getVerticalScroller()
+ .getAttribute("scrollTop")) < scrollTopRow1);
+
// Focus button in last row of Grid
grid.getCell(999, 2).findElement(By.id("row_999")).click();
// Navigate to out of viewport TextField in Footer
@@ -288,4 +303,12 @@ public class GridComponentsTest extends MultiBrowserTest {
assertFalse("Row " + i + " should not have a button",
row.getCell(2).isElementPresent(ButtonElement.class));
}
+
+ // Workaround for Chrome 75, sendKeys(Keys.chord(Keys.SHIFT, Keys.TAB))
+ // doesn't work anymore
+ private void pressKeyWithModifier(Keys keyModifier, Keys key) {
+ new Actions(getDriver()).keyDown(keyModifier).perform();
+ new Actions(getDriver()).sendKeys(key).perform();
+ new Actions(getDriver()).keyUp(keyModifier).perform();
+ }
}
diff --git a/uitest/src/test/java/com/vaadin/tests/components/grid/GridScrolledToBottomTest.java b/uitest/src/test/java/com/vaadin/tests/components/grid/GridScrolledToBottomTest.java
index e4fd2fc2dd..a37e266d30 100644
--- a/uitest/src/test/java/com/vaadin/tests/components/grid/GridScrolledToBottomTest.java
+++ b/uitest/src/test/java/com/vaadin/tests/components/grid/GridScrolledToBottomTest.java
@@ -70,28 +70,27 @@ public class GridScrolledToBottomTest extends MultiBrowserTest {
Actions actions = new Actions(driver);
actions.clickAndHold(splitter).moveByOffset(0, -rowHeight / 2).release()
.perform();
- // the last row is now only half visible, and in DOM tree it's actually
- // the first row now but positioned to the bottom
+ // the last row is now only half visible
// can't query grid.getRow(99) now or it moves the row position,
// have to use element query instead
List<WebElement> rows = grid.findElement(By.className("v-grid-body"))
.findElements(By.className("v-grid-row"));
- WebElement firstRow = rows.get(0);
WebElement lastRow = rows.get(rows.size() - 1);
+ WebElement secondToLastRow = rows.get(rows.size() - 2);
// ensure the scrolling didn't jump extra
assertEquals("Person 99",
- firstRow.findElement(By.className("v-grid-cell")).getText());
- assertEquals("Person 98",
lastRow.findElement(By.className("v-grid-cell")).getText());
+ assertEquals("Person 98", secondToLastRow
+ .findElement(By.className("v-grid-cell")).getText());
// re-calculate current end position
gridBottomY = grid.getLocation().getY() + grid.getSize().getHeight();
// ensure the correct final row really is only half visible at the
// bottom
- assertThat(gridBottomY, greaterThan(firstRow.getLocation().getY()));
- assertThat(firstRow.getLocation().getY() + rowHeight,
+ assertThat(gridBottomY, greaterThan(lastRow.getLocation().getY()));
+ assertThat(lastRow.getLocation().getY() + rowHeight,
greaterThan(gridBottomY));
}
}
diff --git a/uitest/src/test/java/com/vaadin/tests/components/grid/basicfeatures/GridColumnResizeModeTest.java b/uitest/src/test/java/com/vaadin/tests/components/grid/basicfeatures/GridColumnResizeModeTest.java
index df51706ddb..812052a13c 100644
--- a/uitest/src/test/java/com/vaadin/tests/components/grid/basicfeatures/GridColumnResizeModeTest.java
+++ b/uitest/src/test/java/com/vaadin/tests/components/grid/basicfeatures/GridColumnResizeModeTest.java
@@ -103,12 +103,12 @@ public class GridColumnResizeModeTest extends GridBasicsTest {
// ANIMATED resize mode
drag(handle, 100);
- assertTrue(
+ assertTrue("Expected width: " + cell.getSize().getWidth(),
getLogRow(0).contains("Column resized: caption=Column 1, width="
+ cell.getSize().getWidth()));
drag(handle, -100);
- assertTrue(
+ assertTrue("Expected width: " + cell.getSize().getWidth(),
getLogRow(0).contains("Column resized: caption=Column 1, width="
+ cell.getSize().getWidth()));
@@ -117,12 +117,12 @@ public class GridColumnResizeModeTest extends GridBasicsTest {
sleep(250);
drag(handle, 100);
- assertTrue(
+ assertTrue("Expected width: " + cell.getSize().getWidth(),
getLogRow(0).contains("Column resized: caption=Column 1, width="
+ cell.getSize().getWidth()));
drag(handle, -100);
- assertTrue(
+ assertTrue("Expected width: " + cell.getSize().getWidth(),
getLogRow(0).contains("Column resized: caption=Column 1, width="
+ cell.getSize().getWidth()));
}
diff --git a/uitest/src/test/java/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorBasicsTest.java b/uitest/src/test/java/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorBasicsTest.java
index 3e8e7d65a3..87fbb358b9 100644
--- a/uitest/src/test/java/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorBasicsTest.java
+++ b/uitest/src/test/java/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorBasicsTest.java
@@ -3,12 +3,16 @@ package com.vaadin.tests.components.grid.basicfeatures.escalator;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
import java.io.IOException;
import org.junit.Before;
import org.junit.Test;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+import com.vaadin.testbench.TestBenchElement;
import com.vaadin.testbench.elements.NotificationElement;
import com.vaadin.tests.components.grid.basicfeatures.EscalatorBasicClientFeaturesTest;
@@ -49,13 +53,29 @@ public class EscalatorBasicsTest extends EscalatorBasicClientFeaturesTest {
scrollHorizontallyTo(50);
selectMenuPath(GENERAL, DETACH_ESCALATOR);
+ waitForElementNotPresent(By.className("v-escalator"));
selectMenuPath(GENERAL, ATTACH_ESCALATOR);
+ waitForElementPresent(By.className("v-escalator"));
assertEquals("Vertical scroll position", 50, getScrollTop());
assertEquals("Horizontal scroll position", 50, getScrollLeft());
+ TestBenchElement bodyCell = getBodyCell(2, 0);
+ WebElement viewport = findElement(
+ By.className("v-escalator-tablewrapper"));
+ WebElement header = findElement(By.className("v-escalator-header"));
+ // ensure this is the first (partially) visible cell
+ assertTrue(
+ viewport.getLocation().getX() > bodyCell.getLocation().getX());
+ assertTrue(viewport.getLocation().getX() < bodyCell.getLocation().getX()
+ + bodyCell.getSize().getWidth());
+ assertTrue(header.getLocation().getY()
+ + header.getSize().getHeight() > bodyCell.getLocation().getY());
+ assertTrue(header.getLocation().getY()
+ + header.getSize().getHeight() < bodyCell.getLocation().getY()
+ + bodyCell.getSize().getHeight());
assertEquals("First cell of first visible row", "Row 2: 0,2",
- getBodyCell(0, 0).getText());
+ bodyCell.getText());
}
private void assertEscalatorIsRemovedCorrectly() {
diff --git a/uitest/src/test/java/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java b/uitest/src/test/java/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java
index 7924b67503..45e08f9683 100644
--- a/uitest/src/test/java/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java
+++ b/uitest/src/test/java/com/vaadin/tests/components/grid/basicfeatures/escalator/EscalatorSpacerTest.java
@@ -275,12 +275,17 @@ public class EscalatorSpacerTest extends EscalatorBasicClientFeaturesTest {
selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX);
/*
- * we check for row -3 instead of -1, because escalator has two rows
+ * we check for row -2 instead of -1, because escalator has one row
* buffered underneath the footer
*/
selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, SCROLL_TO, ROW_75);
Thread.sleep(500);
- assertEquals("Row 75: 0,75", getBodyCell(-3, 0).getText());
+ TestBenchElement cell75 = getBodyCell(-2, 0);
+ assertEquals("Row 75: 0,75", cell75.getText());
+ // confirm the scroll position
+ WebElement footer = findElement(By.className("v-escalator-footer"));
+ assertEquals(footer.getLocation().y,
+ cell75.getLocation().y + cell75.getSize().height);
selectMenuPath(COLUMNS_AND_ROWS, BODY_ROWS, SCROLL_TO, ROW_25);
Thread.sleep(500);
@@ -406,17 +411,52 @@ public class EscalatorSpacerTest extends EscalatorBasicClientFeaturesTest {
}
@Test
- public void spacersAreInCorrectDomPositionAfterScroll() {
+ public void spacersAreInCorrectDomPositionAfterScroll()
+ throws InterruptedException {
selectMenuPath(FEATURES, SPACERS, ROW_1, SET_100PX);
- scrollVerticallyTo(32); // roughly one row's worth
+ scrollVerticallyTo(40); // roughly two rows' worth
+ // both rows should still be within DOM after this little scrolling, so
+ // the spacer should be the third element within the body (index: 2)
WebElement tbody = getEscalator().findElement(By.tagName("tbody"));
- WebElement spacer = getChild(tbody, 1);
+ WebElement spacer = getChild(tbody, 2);
String cssClass = spacer.getAttribute("class");
assertTrue(
- "element index 1 was not a spacer (class=\"" + cssClass + "\")",
+ "element index 2 was not a spacer (class=\"" + cssClass + "\")",
cssClass.contains("-spacer"));
+
+ // Scroll to last DOM row (Row 20). The exact position varies a bit
+ // depending on the browser.
+ int scrollTo = 172;
+ while (scrollTo < 176) {
+ scrollVerticallyTo(scrollTo);
+ Thread.sleep(500);
+
+ // if spacer is still the third (index: 2) body element, i.e. not
+ // enough scrolling to re-purpose any rows, scroll a bit further
+ spacer = getChild(tbody, 2);
+ cssClass = spacer.getAttribute("class");
+ if (cssClass.contains("-spacer")) {
+ ++scrollTo;
+ } else {
+ break;
+ }
+ }
+ if (getChild(tbody, 20).getText().startsWith("Row 22:")) {
+ // Some browsers scroll too much, spacer should be out of visual
+ // range
+ assertNull("Element found where there should be none",
+ getChild(tbody, 21));
+ } else {
+ // second row should still be within DOM but the first row out of
+ // it, so the spacer should be the second element within the body
+ // (index: 1)
+ spacer = getChild(tbody, 1);
+ cssClass = spacer.getAttribute("class");
+ assertTrue("element index 1 was not a spacer (class=\"" + cssClass
+ + "\")", cssClass.contains("-spacer"));
+ }
}
@Test
diff --git a/uitest/src/test/java/com/vaadin/tests/components/treegrid/TreeGridBigDetailsManagerTest.java b/uitest/src/test/java/com/vaadin/tests/components/treegrid/TreeGridBigDetailsManagerTest.java
index 76c700ec8f..b145dc99bc 100644
--- a/uitest/src/test/java/com/vaadin/tests/components/treegrid/TreeGridBigDetailsManagerTest.java
+++ b/uitest/src/test/java/com/vaadin/tests/components/treegrid/TreeGridBigDetailsManagerTest.java
@@ -1,12 +1,16 @@
package com.vaadin.tests.components.treegrid;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+import static org.hamcrest.Matchers.not;
import static org.hamcrest.number.IsCloseTo.closeTo;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import java.util.List;
+import org.junit.Before;
import org.junit.Test;
import org.openqa.selenium.StaleElementReferenceException;
import org.openqa.selenium.WebDriver;
@@ -15,6 +19,7 @@ import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.support.ui.ExpectedConditions;
import com.vaadin.testbench.By;
+import com.vaadin.testbench.TestBenchElement;
import com.vaadin.testbench.elements.ButtonElement;
import com.vaadin.testbench.elements.TreeGridElement;
import com.vaadin.tests.tb3.MultiBrowserTest;
@@ -33,13 +38,18 @@ public class TreeGridBigDetailsManagerTest extends MultiBrowserTest {
private static final String HIDE_DETAILS = "hideDetails";
private static final String ADD_GRID = "addGrid";
private static final String SCROLL_TO_55 = "scrollTo55";
+ private static final String SCROLL_TO_3055 = "scrollTo3055";
+ private static final String SCROLL_TO_END = "scrollToEnd";
+ private static final String SCROLL_TO_START = "scrollToStart";
+ private static final String TOGGLE_15 = "toggle15";
+ private static final String TOGGLE_3000 = "toggle3000";
private TreeGridElement treeGrid;
private int expectedSpacerHeight = 0;
private int expectedRowHeight = 0;
private ExpectedCondition<Boolean> expectedConditionDetails(final int root,
- final int branch, final int leaf) {
+ final Integer branch, final Integer leaf) {
return new ExpectedCondition<Boolean>() {
@Override
public Boolean apply(WebDriver arg0) {
@@ -49,9 +59,14 @@ public class TreeGridBigDetailsManagerTest extends MultiBrowserTest {
@Override
public String toString() {
// waiting for...
+ if (leaf != null) {
+ return String.format(
+ "Leaf %s/%s/%s details row contents to be found",
+ root, branch, leaf);
+ }
return String.format(
- "Leaf %s/%s/%s details row contents to be found", root,
- branch, leaf);
+ "Branch %s/%s details row contents to be found", root,
+ branch);
}
};
}
@@ -87,6 +102,11 @@ public class TreeGridBigDetailsManagerTest extends MultiBrowserTest {
return null;
}
+ private WebElement getRow(int index) {
+ return treeGrid.getBody().findElements(By.className("v-treegrid-row"))
+ .get(index);
+ }
+
private void ensureExpectedSpacerHeightSet() {
if (expectedSpacerHeight == 0) {
expectedSpacerHeight = treeGrid
@@ -94,9 +114,6 @@ public class TreeGridBigDetailsManagerTest extends MultiBrowserTest {
.getHeight();
assertThat((double) expectedSpacerHeight, closeTo(27d, 2d));
}
- if (expectedRowHeight == 0) {
- expectedRowHeight = treeGrid.getRow(0).getSize().getHeight();
- }
}
private void assertSpacerCount(int expectedSpacerCount) {
@@ -129,10 +146,6 @@ public class TreeGridBigDetailsManagerTest extends MultiBrowserTest {
previousSpacer = spacer;
continue;
}
- if (spacer.getLocation().y == 0) {
- // FIXME: find out why there are cases like this out of order
- continue;
- }
// -1 should be enough, but increased tolerance to -3 for FireFox
// and IE11 since a few pixels' discrepancy isn't relevant for this
// fix
@@ -148,16 +161,24 @@ public class TreeGridBigDetailsManagerTest extends MultiBrowserTest {
treeGrid.findElements(By.className(CLASSNAME_ERROR)).size());
}
- @Test
- public void expandAllOpenAllInitialDetails_toggleOneTwice_hideAll() {
- openTestURL();
- $(ButtonElement.class).id(EXPAND_ALL).click();
- $(ButtonElement.class).id(SHOW_DETAILS).click();
+ private void addGrid() {
$(ButtonElement.class).id(ADD_GRID).click();
-
waitForElementPresent(By.className(CLASSNAME_TREEGRID));
treeGrid = $(TreeGridElement.class).first();
+ expectedRowHeight = treeGrid.getRow(0).getSize().getHeight();
+ }
+
+ @Before
+ public void before() {
+ openTestURL();
+ }
+
+ @Test
+ public void expandAllOpenAllInitialDetails_toggleOneTwice_hideAll() {
+ $(ButtonElement.class).id(EXPAND_ALL).click();
+ $(ButtonElement.class).id(SHOW_DETAILS).click();
+ addGrid();
waitUntil(expectedConditionDetails(0, 0, 0));
ensureExpectedSpacerHeightSet();
@@ -205,14 +226,9 @@ public class TreeGridBigDetailsManagerTest extends MultiBrowserTest {
@Test
public void expandAllOpenAllInitialDetails_toggleAll() {
- openTestURL();
$(ButtonElement.class).id(EXPAND_ALL).click();
$(ButtonElement.class).id(SHOW_DETAILS).click();
- $(ButtonElement.class).id(ADD_GRID).click();
-
- waitForElementPresent(By.className(CLASSNAME_TREEGRID));
-
- treeGrid = $(TreeGridElement.class).first();
+ addGrid();
waitUntil(expectedConditionDetails(0, 0, 0));
ensureExpectedSpacerHeightSet();
@@ -231,9 +247,9 @@ public class TreeGridBigDetailsManagerTest extends MultiBrowserTest {
assertSpacerPositions();
// FIXME: TreeGrid fails to update cache correctly when you expand all
- // and after a long, long wait you end up with 3321 open details rows
- // and row 63/8/0 in view instead of 95 and 0/0/0 as expected.
- // WaitUntil timeouts by then.
+ // and triggers client-side exceptions for rows that fall outside of the
+ // cache because they try to extend the cache with a range that isn't
+ // connected to the cached range
if (true) {// remove this block after fixed
return;
}
@@ -241,7 +257,7 @@ public class TreeGridBigDetailsManagerTest extends MultiBrowserTest {
$(ButtonElement.class).id(EXPAND_ALL).click();
// State should have returned to what it was before collapsing.
- waitUntil(expectedConditionDetails(0, 0, 0));
+ waitUntil(expectedConditionDetails(0, 0, 0), 15);
assertSpacerCount(spacerCount);
assertSpacerHeights();
assertSpacerPositions();
@@ -251,13 +267,8 @@ public class TreeGridBigDetailsManagerTest extends MultiBrowserTest {
@Test
public void expandAllOpenNoInitialDetails_showSeveral_toggleOneByOne() {
- openTestURL();
$(ButtonElement.class).id(EXPAND_ALL).click();
- $(ButtonElement.class).id(ADD_GRID).click();
-
- waitForElementPresent(By.className(CLASSNAME_TREEGRID));
-
- treeGrid = $(TreeGridElement.class).first();
+ addGrid();
// open details for several rows, leave one out from the hierarchy that
// is to be collapsed
@@ -310,17 +321,29 @@ public class TreeGridBigDetailsManagerTest extends MultiBrowserTest {
}
@Test
+ public void expandAllOpenAllInitialDetails_hideOne() {
+ $(ButtonElement.class).id(EXPAND_ALL).click();
+ $(ButtonElement.class).id(SHOW_DETAILS).click();
+ addGrid();
+
+ // check the position of a row
+ int oldY = treeGrid.getCell(2, 0).getLocation().getY();
+
+ // hide the spacer from previous row
+ treeGrid.getCell(1, 0).click();
+
+ // ensure the investigated row moved
+ assertNotEquals(oldY, treeGrid.getCell(2, 0).getLocation().getY());
+ }
+
+ @Test
public void expandAllOpenAllInitialDetailsScrolled_toggleOne_hideAll() {
- openTestURL();
$(ButtonElement.class).id(EXPAND_ALL).click();
$(ButtonElement.class).id(SHOW_DETAILS).click();
- $(ButtonElement.class).id(ADD_GRID).click();
+ addGrid();
- waitForElementPresent(By.className(CLASSNAME_TREEGRID));
$(ButtonElement.class).id(SCROLL_TO_55).click();
- treeGrid = $(TreeGridElement.class).first();
-
waitUntil(expectedConditionDetails(1, 2, 0));
ensureExpectedSpacerHeightSet();
int spacerCount = treeGrid.findElements(By.className(CLASSNAME_SPACER))
@@ -333,8 +356,7 @@ public class TreeGridBigDetailsManagerTest extends MultiBrowserTest {
waitUntil(ExpectedConditions.not(expectedConditionDetails(1, 2, 0)));
assertSpacerHeights();
assertSpacerPositions();
- // FIXME: gives 128, not 90 as expected
- // assertSpacerCount(spacerCount);
+ assertSpacerCount(spacerCount);
treeGrid.expandWithClick(50);
@@ -342,8 +364,7 @@ public class TreeGridBigDetailsManagerTest extends MultiBrowserTest {
waitUntil(expectedConditionDetails(1, 2, 0));
assertSpacerHeights();
assertSpacerPositions();
- // FIXME: gives 131, not 90 as expected
- // assertSpacerCount(spacerCount);
+ assertSpacerCount(spacerCount);
// test that repeating the toggle still doesn't change anything
@@ -352,16 +373,14 @@ public class TreeGridBigDetailsManagerTest extends MultiBrowserTest {
waitUntil(ExpectedConditions.not(expectedConditionDetails(1, 2, 0)));
assertSpacerHeights();
assertSpacerPositions();
- // FIXME: gives 128, not 90 as expected
- // assertSpacerCount(spacerCount);
+ assertSpacerCount(spacerCount);
treeGrid.expandWithClick(50);
waitUntil(expectedConditionDetails(1, 2, 0));
assertSpacerHeights();
assertSpacerPositions();
- // FIXME: gives 131, not 90 as expected
- // assertSpacerCount(spacerCount);
+ assertSpacerCount(spacerCount);
// test that hiding all still won't break things
@@ -373,17 +392,13 @@ public class TreeGridBigDetailsManagerTest extends MultiBrowserTest {
@Test
public void expandAllOpenAllInitialDetailsScrolled_toggleAll() {
- openTestURL();
$(ButtonElement.class).id(EXPAND_ALL).click();
$(ButtonElement.class).id(SHOW_DETAILS).click();
- $(ButtonElement.class).id(ADD_GRID).click();
+ addGrid();
- waitForElementPresent(By.className(CLASSNAME_TREEGRID));
$(ButtonElement.class).id(SCROLL_TO_55).click();
- treeGrid = $(TreeGridElement.class).first();
-
- waitUntil(expectedConditionDetails(1, 1, 0));
+ waitUntil(expectedConditionDetails(1, 3, 0));
ensureExpectedSpacerHeightSet();
int spacerCount = treeGrid.findElements(By.className(CLASSNAME_SPACER))
@@ -400,7 +415,8 @@ public class TreeGridBigDetailsManagerTest extends MultiBrowserTest {
assertSpacerHeights();
assertSpacerPositions();
- // FIXME: collapsing too many rows after scrolling still causes a chaos
+ // FIXME: collapsing and expanding too many rows after scrolling still
+ // fails to reset to the same state
if (true) { // remove this block after fixed
return;
}
@@ -408,7 +424,7 @@ public class TreeGridBigDetailsManagerTest extends MultiBrowserTest {
$(ButtonElement.class).id(EXPAND_ALL).click();
// State should have returned to what it was before collapsing.
- waitUntil(expectedConditionDetails(1, 1, 0));
+ waitUntil(expectedConditionDetails(1, 3, 0));
assertSpacerCount(spacerCount);
assertSpacerHeights();
assertSpacerPositions();
@@ -418,27 +434,24 @@ public class TreeGridBigDetailsManagerTest extends MultiBrowserTest {
@Test
public void expandAllOpenNoInitialDetailsScrolled_showSeveral_toggleOneByOne() {
- openTestURL();
$(ButtonElement.class).id(EXPAND_ALL).click();
- $(ButtonElement.class).id(ADD_GRID).click();
+ addGrid();
- waitForElementPresent(By.className(CLASSNAME_TREEGRID));
$(ButtonElement.class).id(SCROLL_TO_55).click();
- treeGrid = $(TreeGridElement.class).first();
assertSpacerCount(0);
// open details for several rows, leave one out from the hierarchy that
// is to be collapsed
- treeGrid.getCell(50, 0).click();
- treeGrid.getCell(51, 0).click();
- treeGrid.getCell(52, 0).click();
- // no click for cell (53, 0)
- treeGrid.getCell(54, 0).click();
- treeGrid.getCell(55, 0).click();
- treeGrid.getCell(56, 0).click();
- treeGrid.getCell(57, 0).click();
- treeGrid.getCell(58, 0).click();
+ treeGrid.getCell(50, 0).click(); // Branch 1/2
+ treeGrid.getCell(51, 0).click(); // Leaf 1/2/0
+ treeGrid.getCell(52, 0).click(); // Leaf 1/2/1
+ // no click for cell (53, 0) // Leaf 1/2/2
+ treeGrid.getCell(54, 0).click(); // Branch 1/3
+ treeGrid.getCell(55, 0).click(); // Leaf 1/3/0
+ treeGrid.getCell(56, 0).click(); // Leaf 1/3/1
+ treeGrid.getCell(57, 0).click(); // Leaf 1/3/2
+ treeGrid.getCell(58, 0).click(); // Branch 1/4
int spacerCount = 8;
waitUntil(expectedConditionDetails(1, 2, 0));
@@ -507,4 +520,351 @@ public class TreeGridBigDetailsManagerTest extends MultiBrowserTest {
assertNoErrors();
}
+ @Test
+ public void expandAllOpenAllInitialDetailsScrolled_hideOne() {
+ $(ButtonElement.class).id(EXPAND_ALL).click();
+ $(ButtonElement.class).id(SHOW_DETAILS).click();
+ addGrid();
+
+ $(ButtonElement.class).id(SCROLL_TO_55).click();
+
+ // check the position of a row
+ int oldY = treeGrid.getCell(52, 0).getLocation().getY();
+
+ // hide the spacer from previous row
+ treeGrid.getCell(51, 0).click();
+
+ // ensure the investigated row moved
+ assertNotEquals(oldY, treeGrid.getCell(52, 0).getLocation().getY());
+ }
+
+ @Test
+ public void expandAllOpenAllInitialDetailsScrolledFar_toggleOne_hideAll() {
+ $(ButtonElement.class).id(EXPAND_ALL).click();
+ $(ButtonElement.class).id(SHOW_DETAILS).click();
+ addGrid();
+
+ $(ButtonElement.class).id(SCROLL_TO_3055).click();
+
+ waitUntil(expectedConditionDetails(74, 4, 0));
+ ensureExpectedSpacerHeightSet();
+ int spacerCount = treeGrid.findElements(By.className(CLASSNAME_SPACER))
+ .size();
+ assertSpacerPositions();
+
+ treeGrid.collapseWithClick(3051);
+
+ // collapsing one shouldn't affect spacer count, just update the cache
+ waitUntil(ExpectedConditions.not(expectedConditionDetails(1, 2, 0)));
+ assertSpacerHeights();
+ assertSpacerPositions();
+ assertSpacerCount(spacerCount);
+
+ treeGrid.expandWithClick(3051);
+
+ // expanding back shouldn't affect spacer count, just update the cache
+ waitUntil(expectedConditionDetails(74, 4, 0));
+ assertSpacerHeights();
+ assertSpacerPositions();
+ assertSpacerCount(spacerCount);
+
+ // test that repeating the toggle still doesn't change anything
+
+ treeGrid.collapseWithClick(3051);
+
+ waitUntil(ExpectedConditions.not(expectedConditionDetails(74, 4, 0)));
+ assertSpacerHeights();
+ assertSpacerPositions();
+ assertSpacerCount(spacerCount);
+
+ treeGrid.expandWithClick(3051);
+
+ waitUntil(expectedConditionDetails(74, 4, 0));
+ assertSpacerHeights();
+ assertSpacerPositions();
+ assertSpacerCount(spacerCount);
+
+ // test that hiding all still won't break things
+
+ $(ButtonElement.class).id(HIDE_DETAILS).click();
+ waitForElementNotPresent(By.className(CLASSNAME_SPACER));
+
+ assertNoErrors();
+ }
+
+ @Test
+ public void expandAllOpenAllInitialDetailsScrolledFar_toggleAll() {
+ $(ButtonElement.class).id(EXPAND_ALL).click();
+ $(ButtonElement.class).id(SHOW_DETAILS).click();
+ addGrid();
+
+ $(ButtonElement.class).id(SCROLL_TO_3055).click();
+
+ waitUntil(expectedConditionDetails(74, 4, 0));
+ ensureExpectedSpacerHeightSet();
+
+ int spacerCount = treeGrid.findElements(By.className(CLASSNAME_SPACER))
+ .size();
+ assertSpacerPositions();
+
+ $(ButtonElement.class).id(COLLAPSE_ALL).click();
+
+ waitForElementNotPresent(By.className(CLASSNAME_LEAF));
+
+ // There should still be a full cache's worth of details rows open,
+ // just not the same rows than before collapsing all.
+ assertSpacerCount(spacerCount);
+ assertSpacerHeights();
+ assertSpacerPositions();
+
+ // FIXME: collapsing and expanding too many rows after scrolling still
+ // fails to reset to the same state
+ if (true) { // remove this block after fixed
+ return;
+ }
+
+ $(ButtonElement.class).id(EXPAND_ALL).click();
+
+ // State should have returned to what it was before collapsing.
+ waitUntil(expectedConditionDetails(74, 4, 0));
+ assertSpacerCount(spacerCount);
+ assertSpacerHeights();
+ assertSpacerPositions();
+
+ assertNoErrors();
+ }
+
+ @Test
+ public void expandAllOpenNoInitialDetailsScrolledFar_showSeveral_toggleOneByOne() {
+ $(ButtonElement.class).id(EXPAND_ALL).click();
+ addGrid();
+
+ $(ButtonElement.class).id(SCROLL_TO_3055).click();
+
+ assertSpacerCount(0);
+
+ // open details for several rows, leave one out from the hierarchy that
+ // is to be collapsed
+ treeGrid.getCell(3051, 0).click(); // Branch 74/4
+ treeGrid.getCell(3052, 0).click(); // Leaf 74/4/0
+ treeGrid.getCell(3053, 0).click(); // Leaf 74/4/1
+ // no click for cell (3054, 0) // Leaf 74/4/2
+ treeGrid.getCell(3055, 0).click(); // Branch 74/5
+ treeGrid.getCell(3056, 0).click(); // Leaf 74/5/0
+ treeGrid.getCell(3057, 0).click(); // Leaf 74/5/1
+ treeGrid.getCell(3058, 0).click(); // Leaf 74/5/2
+ treeGrid.getCell(3059, 0).click(); // Branch 74/6
+ int spacerCount = 8;
+
+ waitUntil(expectedConditionDetails(74, 4, 0));
+ assertSpacerCount(spacerCount);
+ ensureExpectedSpacerHeightSet();
+ assertSpacerPositions();
+
+ // toggle the branch with partially open details rows
+ treeGrid.collapseWithClick(3051);
+
+ waitUntil(ExpectedConditions.not(expectedConditionDetails(74, 4, 0)));
+ assertSpacerCount(spacerCount - 2);
+ assertSpacerHeights();
+ assertSpacerPositions();
+
+ treeGrid.expandWithClick(3051);
+
+ waitUntil(expectedConditionDetails(74, 4, 0));
+ assertSpacerCount(spacerCount);
+ assertSpacerHeights();
+ assertSpacerPositions();
+
+ // toggle the branch with fully open details rows
+ treeGrid.collapseWithClick(3055);
+
+ waitUntil(ExpectedConditions.not(expectedConditionDetails(74, 5, 0)));
+ assertSpacerCount(spacerCount - 3);
+ assertSpacerHeights();
+ assertSpacerPositions();
+
+ treeGrid.expandWithClick(3055);
+
+ waitUntil(expectedConditionDetails(74, 5, 0));
+ assertSpacerCount(spacerCount);
+ assertSpacerHeights();
+ assertSpacerPositions();
+
+ // repeat both toggles to ensure still no errors
+ treeGrid.collapseWithClick(3051);
+
+ waitUntil(ExpectedConditions.not(expectedConditionDetails(74, 4, 0)));
+ assertSpacerCount(spacerCount - 2);
+ assertSpacerHeights();
+ assertSpacerPositions();
+
+ treeGrid.expandWithClick(3051);
+
+ waitUntil(expectedConditionDetails(74, 4, 0));
+ assertSpacerCount(spacerCount);
+ assertSpacerHeights();
+ assertSpacerPositions();
+ treeGrid.collapseWithClick(3055);
+
+ waitUntil(ExpectedConditions.not(expectedConditionDetails(74, 5, 0)));
+ assertSpacerCount(spacerCount - 3);
+ assertSpacerHeights();
+ assertSpacerPositions();
+
+ treeGrid.expandWithClick(3055);
+
+ waitUntil(expectedConditionDetails(74, 5, 0));
+ assertSpacerCount(spacerCount);
+ assertSpacerHeights();
+ assertSpacerPositions();
+
+ assertNoErrors();
+ }
+
+ @Test
+ public void expandAllOpenAllInitialDetailsScrolledFar_hideOne() {
+ $(ButtonElement.class).id(EXPAND_ALL).click();
+ $(ButtonElement.class).id(SHOW_DETAILS).click();
+ addGrid();
+
+ $(ButtonElement.class).id(SCROLL_TO_3055).click();
+
+ // check the position of a row
+ int oldY = treeGrid.getCell(3052, 0).getLocation().getY();
+
+ // hide the spacer from previous row
+ treeGrid.getCell(3051, 0).click();
+
+ // ensure the investigated row moved
+ assertNotEquals(oldY, treeGrid.getCell(52, 0).getLocation().getY());
+ }
+
+ @Test
+ public void expandAllOpenAllInitialDetails_checkScrollPositions() {
+ $(ButtonElement.class).id(EXPAND_ALL).click();
+ $(ButtonElement.class).id(SHOW_DETAILS).click();
+ addGrid();
+
+ TestBenchElement tableWrapper = treeGrid.getTableWrapper();
+
+ $(ButtonElement.class).id(SCROLL_TO_55).click();
+ waitUntil(expectedConditionDetails(1, 3, 0));
+
+ WebElement detailsRow = getSpacer(1, 3, 0);
+ assertNotNull("Spacer for row 55 not found", detailsRow);
+
+ int wrapperY = tableWrapper.getLocation().getY();
+ int wrapperHeight = tableWrapper.getSize().getHeight();
+
+ int detailsY = detailsRow.getLocation().getY();
+ int detailsHeight = detailsRow.getSize().getHeight();
+
+ assertThat("Scroll to 55 didn't scroll as expected",
+ (double) detailsY + detailsHeight,
+ closeTo(wrapperY + wrapperHeight, 1d));
+
+ $(ButtonElement.class).id(SCROLL_TO_3055).click();
+ waitUntil(expectedConditionDetails(74, 5, null));
+
+ detailsRow = getSpacer(74, 5, null);
+ assertNotNull("Spacer for row 3055 not found", detailsRow);
+
+ detailsY = detailsRow.getLocation().getY();
+ detailsHeight = detailsRow.getSize().getHeight();
+
+ assertThat("Scroll to 3055 didn't scroll as expected",
+ (double) detailsY + detailsHeight,
+ closeTo(wrapperY + wrapperHeight, 1d));
+
+ $(ButtonElement.class).id(SCROLL_TO_END).click();
+ waitUntil(expectedConditionDetails(99, 9, 2));
+
+ detailsRow = getSpacer(99, 9, 2);
+ assertNotNull("Spacer for last row not found", detailsRow);
+
+ detailsY = detailsRow.getLocation().getY();
+ detailsHeight = detailsRow.getSize().getHeight();
+
+ // the layout jumps sometimes, check again
+ wrapperY = tableWrapper.getLocation().getY();
+ wrapperHeight = tableWrapper.getSize().getHeight();
+
+ assertThat("Scroll to end didn't scroll as expected",
+ (double) detailsY + detailsHeight,
+ closeTo(wrapperY + wrapperHeight, 1d));
+
+ $(ButtonElement.class).id(SCROLL_TO_START).click();
+ waitUntil(expectedConditionDetails(0, 0, 0));
+
+ WebElement firstRow = getRow(0);
+ TestBenchElement header = treeGrid.getHeader();
+
+ assertThat("Scroll to start didn't scroll as expected",
+ (double) firstRow.getLocation().getY(),
+ closeTo(wrapperY + header.getSize().getHeight(), 1d));
+ }
+
+ @Test
+ public void expandAllOpenNoInitialDetails_testToggleScrolling() {
+ $(ButtonElement.class).id(EXPAND_ALL).click();
+ addGrid();
+
+ TestBenchElement tableWrapper = treeGrid.getTableWrapper();
+ int wrapperY = tableWrapper.getLocation().getY();
+
+ WebElement firstRow = getRow(0);
+ int firstRowY = firstRow.getLocation().getY();
+
+ TestBenchElement header = treeGrid.getHeader();
+ int headerHeight = header.getSize().getHeight();
+
+ assertThat("Unexpected initial scroll position", (double) firstRowY,
+ closeTo(wrapperY + headerHeight, 1d));
+
+ $(ButtonElement.class).id(TOGGLE_15).click();
+
+ firstRowY = firstRow.getLocation().getY();
+
+ assertThat(
+ "Toggling row 15's details open should have caused scrolling",
+ (double) firstRowY, not(closeTo(wrapperY + headerHeight, 1d)));
+
+ $(ButtonElement.class).id(SCROLL_TO_START).click();
+
+ firstRowY = firstRow.getLocation().getY();
+
+ assertThat("Scrolling to start failed", (double) firstRowY,
+ closeTo(wrapperY + headerHeight, 1d));
+
+ $(ButtonElement.class).id(TOGGLE_15).click();
+
+ firstRowY = firstRow.getLocation().getY();
+
+ assertThat(
+ "Toggling row 15's details closed should not have caused scrolling",
+ (double) firstRowY, closeTo(wrapperY + headerHeight, 1d));
+
+ $(ButtonElement.class).id(TOGGLE_3000).click();
+
+ firstRowY = firstRow.getLocation().getY();
+
+ assertThat(
+ "Toggling row 3000's details open should not have caused scrolling",
+ (double) firstRowY, closeTo(wrapperY + headerHeight, 1d));
+
+ $(ButtonElement.class).id(SCROLL_TO_55).click();
+
+ WebElement row = getRow(0);
+ assertNotEquals("First row should be out of visual range", firstRowY,
+ row);
+
+ $(ButtonElement.class).id(TOGGLE_15).click();
+
+ assertEquals(
+ "Toggling row 15's details open should not have caused scrolling "
+ + "when row 15 is outside of visual range",
+ row, getRow(0));
+ }
+
}
diff --git a/uitest/src/test/java/com/vaadin/tests/components/treegrid/TreeGridDetailsManagerTest.java b/uitest/src/test/java/com/vaadin/tests/components/treegrid/TreeGridDetailsManagerTest.java
index e5301a4731..9ca8b2a974 100644
--- a/uitest/src/test/java/com/vaadin/tests/components/treegrid/TreeGridDetailsManagerTest.java
+++ b/uitest/src/test/java/com/vaadin/tests/components/treegrid/TreeGridDetailsManagerTest.java
@@ -3,6 +3,7 @@ package com.vaadin.tests.components.treegrid;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.number.IsCloseTo.closeTo;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
@@ -292,4 +293,25 @@ public class TreeGridDetailsManagerTest extends MultiBrowserTest {
assertNoErrors();
}
+ @Test
+ public void expandAllOpenAllInitialDetails_hideOne() {
+ openTestURL();
+ $(ButtonElement.class).id(EXPAND_ALL).click();
+ $(ButtonElement.class).id(SHOW_DETAILS).click();
+ $(ButtonElement.class).id(ADD_GRID).click();
+
+ waitForElementPresent(By.className(CLASSNAME_TREEGRID));
+
+ treeGrid = $(TreeGridElement.class).first();
+
+ // check the position of a row
+ int oldY = treeGrid.getCell(2, 0).getLocation().getY();
+
+ // hide the spacer from previous row
+ treeGrid.getCell(1, 0).click();
+
+ // ensure the investigated row moved
+ assertNotEquals(oldY, treeGrid.getCell(2, 0).getLocation().getY());
+ }
+
}