If first attempt at scrolling doesn't succeed it's unlikely that continuing to wait is going to make any difference. Cache should be populated before triggering any actions that depend on the row being visible, otherwise it should be enough to trust that scrollToRow actually scrolls to row and once scrolling is done the row is as much in view as it's going to get. This way we don't get into a situation where Editor never opens because it's still waiting for that one last pixel that can't be achieved thanks to browser zoom causing rounding errors. Continues on #11672tags/8.10.0.alpha1
* The scrolling methods must trigger the scrolling only after any potential | * The scrolling methods must trigger the scrolling only after any potential | ||||
* resizing or other similar action triggered from the server side within | * 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 | * 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. | |||||
* delay. The delay is done with <code>scheduleDeferred</code> rather than | |||||
* <code>scheduleFinally</code> because otherwise the order of the | |||||
* operations isn't guaranteed. | |||||
* | * | ||||
*/ | */ | ||||
private class GridConnectorClientRpc implements GridClientRpc { | private class GridConnectorClientRpc implements GridClientRpc { | ||||
@Override | @Override | ||||
public void scrollToRow(int row, ScrollDestination destination) { | public void scrollToRow(int row, ScrollDestination destination) { | ||||
Scheduler.get().scheduleFinally(() -> { | |||||
Scheduler.get().scheduleDeferred(() -> { | |||||
grid.scrollToRow(row, destination); | grid.scrollToRow(row, destination); | ||||
// Add details refresh listener and handle possible detail | // Add details refresh listener and handle possible detail | ||||
// for scrolled row. | // for scrolled row. | ||||
@Override | @Override | ||||
public void scrollToStart() { | public void scrollToStart() { | ||||
Scheduler.get().scheduleFinally(() -> grid.scrollToStart()); | |||||
Scheduler.get().scheduleDeferred(() -> grid.scrollToStart()); | |||||
} | } | ||||
@Override | @Override | ||||
public void scrollToEnd() { | public void scrollToEnd() { | ||||
Scheduler.get().scheduleFinally(() -> { | |||||
Scheduler.get().scheduleDeferred(() -> { | |||||
grid.scrollToEnd(); | grid.scrollToEnd(); | ||||
addDetailsRefreshCallback(() -> { | addDetailsRefreshCallback(() -> { | ||||
if (rowHasDetails(grid.getDataSource().size() - 1)) { | if (rowHasDetails(grid.getDataSource().size() - 1)) { |
public void scrollToRowAndSpacer(final int rowIndex, | public void scrollToRowAndSpacer(final int rowIndex, | ||||
final ScrollDestination destination, final int padding) | final ScrollDestination destination, final int padding) | ||||
throws IllegalArgumentException { | throws IllegalArgumentException { | ||||
// wait for the layout phase to finish | |||||
Scheduler.get().scheduleFinally(() -> { | |||||
if (rowIndex != -1) { | |||||
verifyValidRowIndex(rowIndex); | |||||
} | |||||
body.scrollToRowSpacerOrBoth(rowIndex, destination, padding, | |||||
ScrollType.ROW_AND_SPACER); | |||||
}); | |||||
if (rowIndex != -1) { | |||||
verifyValidRowIndex(rowIndex); | |||||
} | |||||
body.scrollToRowSpacerOrBoth(rowIndex, destination, padding, | |||||
ScrollType.ROW_AND_SPACER); | |||||
} | } | ||||
private static void validateScrollDestination( | private static void validateScrollDestination( |
} | } | ||||
state = State.ACTIVATING; | state = State.ACTIVATING; | ||||
grid.scrollToRow(rowIndex, | |||||
isBuffered() ? ScrollDestination.MIDDLE | |||||
: ScrollDestination.ANY, | |||||
grid.scrollToRow(rowIndex, ScrollDestination.ANY, | |||||
() -> show(rowIndex, columnIndexDOM)); | () -> show(rowIndex, columnIndexDOM)); | ||||
} | } | ||||
*/ | */ | ||||
public void scrollToRow(int rowIndex, ScrollDestination destination, | public void scrollToRow(int rowIndex, ScrollDestination destination, | ||||
Runnable callback) { | Runnable callback) { | ||||
waitUntilVisible(rowIndex, destination, () -> { | |||||
Reference<HandlerRegistration> registration = new Reference<>(); | |||||
registration.set(addDataAvailableHandler(event -> { | |||||
if (event.getAvailableRows().contains(rowIndex)) { | |||||
registration.get().removeHandler(); | |||||
callback.run(); | |||||
} | |||||
})); | |||||
}); | |||||
waitUntilVisible(rowIndex, destination, callback); | |||||
} | } | ||||
/** | /** | ||||
*/ | */ | ||||
private void waitUntilVisible(int rowIndex, ScrollDestination destination, | private void waitUntilVisible(int rowIndex, ScrollDestination destination, | ||||
Runnable whenVisible) { | Runnable whenVisible) { | ||||
final Escalator escalator = getEscalator(); | |||||
if (escalator.getVisibleRowRange().contains(rowIndex)) { | |||||
TableRowElement rowElement = escalator.getBody() | |||||
.getRowElement(rowIndex); | |||||
long bottomBorder = Math.round(WidgetUtil.getBorderBottomThickness( | |||||
rowElement.getFirstChildElement()) + 0.5d); | |||||
if (rowElement.getAbsoluteTop() + bottomBorder >= escalator | |||||
.getHeader().getElement().getAbsoluteBottom() | |||||
&& rowElement.getAbsoluteBottom() <= escalator.getFooter() | |||||
.getElement().getAbsoluteTop() + bottomBorder) { | |||||
whenVisible.run(); | |||||
return; | |||||
} | |||||
boolean waitForCache = false; | |||||
if (getDataSource().getRow(rowIndex) == null) { | |||||
// not yet in cache, wait for this to change | |||||
waitForCache = true; | |||||
Reference<Registration> registration = new Reference<>(); | |||||
registration.set(getDataSource() | |||||
.addDataChangeHandler(new DataChangeHandler() { | |||||
@Override | |||||
public void resetDataAndSize(int estimatedNewDataSize) { | |||||
// data set changed, cancel the operation | |||||
registration.get().remove(); | |||||
} | |||||
@Override | |||||
public void dataUpdated(int firstRowIndex, | |||||
int numberOfRows) { | |||||
// NOP | |||||
} | |||||
@Override | |||||
public void dataRemoved(int firstRowIndex, | |||||
int numberOfRows) { | |||||
// data set changed, cancel the operation | |||||
registration.get().remove(); | |||||
} | |||||
@Override | |||||
public void dataAvailable(int firstRowIndex, | |||||
int numberOfRows) { | |||||
// if new available range contains the row, | |||||
// try again | |||||
if (Range.withLength(firstRowIndex, numberOfRows) | |||||
.contains(rowIndex)) { | |||||
registration.get().remove(); | |||||
waitUntilVisible(rowIndex, destination, | |||||
whenVisible); | |||||
} | |||||
} | |||||
@Override | |||||
public void dataAdded(int firstRowIndex, | |||||
int numberOfRows) { | |||||
// data set changed, cancel the operation | |||||
registration.get().remove(); | |||||
} | |||||
})); | |||||
} | } | ||||
Reference<HandlerRegistration> registration = new Reference<>(); | |||||
registration.set(addScrollHandler(event -> { | |||||
if (escalator.getVisibleRowRange().contains(rowIndex)) { | |||||
registration.get().removeHandler(); | |||||
whenVisible.run(); | |||||
} | |||||
})); | |||||
scrollToRow(rowIndex, destination); | scrollToRow(rowIndex, destination); | ||||
if (!waitForCache) { | |||||
// all necessary adjustments done, time to perform | |||||
whenVisible.run(); | |||||
} | |||||
} | } | ||||
/** | /** |