package org.fxmisc.flowless; import java.util.Optional; import java.util.function.Function; import javafx.collections.ObservableList; import javafx.scene.Node; import javafx.scene.input.ScrollEvent; import org.reactfx.EventStreams; import org.reactfx.Subscription; import org.reactfx.collection.LiveList; import org.reactfx.collection.MemoizationList; import org.reactfx.collection.QuasiListModification; /** * Tracks all of the cells that the viewport can display ({@link #cells}) and which cells the viewport is currently * displaying ({@link #presentCells}). */ final class CellListManager<T, C extends Cell<T, ? extends Node>> { private final Node owner; private final CellPool<T, C> cellPool; private final MemoizationList<C> cells; private final LiveList<C> presentCells; private final LiveList<Node> cellNodes; private final Subscription presentCellsSubscription; public CellListManager( Node owner, ObservableList<T> items, Function<? super T, ? extends C> cellFactory) { this.owner = owner; this.cellPool = new CellPool<>(cellFactory); this.cells = LiveList.map(items, this::cellForItem).memoize(); this.presentCells = cells.memoizedItems(); this.cellNodes = presentCells.map(Cell::getNode); this.presentCellsSubscription = presentCells.observeQuasiModifications(this::presentCellsChanged); } public void dispose() { // return present cells to pool *before* unsubscribing, // because stopping to observe memoized items may clear memoized items presentCells.forEach(cellPool::acceptCell); presentCellsSubscription.unsubscribe(); cellPool.dispose(); } /** Gets the list of nodes that the viewport is displaying */ public ObservableList<Node> getNodes() { return cellNodes; } public MemoizationList<C> getLazyCellList() { return cells; } public boolean isCellPresent(int itemIndex) { return cells.isMemoized(itemIndex); } public C getPresentCell(int itemIndex) { // both getIfMemoized() and get() may throw return cells.getIfMemoized(itemIndex).get(); } public Optional<C> getCellIfPresent(int itemIndex) { return cells.getIfMemoized(itemIndex); // getIfMemoized() may throw } public C getCell(int itemIndex) { return cells.get(itemIndex); } /** * Updates the list of cells to display * * @param fromItem the index of the first item to display * @param toItem the index of the last item to display */ public void cropTo(int fromItem, int toItem) { fromItem = Math.max(fromItem, 0); toItem = Math.min(toItem, cells.size()); cells.forget(0, fromItem); cells.forget(toItem, cells.size()); } private C cellForItem(T item) { C cell = cellPool.getCell(item); // apply CSS when the cell is first added to the scene Node node = cell.getNode(); EventStreams.nonNullValuesOf(node.sceneProperty()) .subscribeForOne(scene -> { node.applyCss(); }); // Make cell initially invisible. // It will be made visible when it is positioned. node.setVisible(false); if (cell.isReusable()) { // if cell is reused i think adding event handler // would cause resource leakage. node.setOnScroll(this::pushScrollEvent); node.setOnScrollStarted(this::pushScrollEvent); node.setOnScrollFinished(this::pushScrollEvent); } else { node.addEventHandler(ScrollEvent.ANY, this::pushScrollEvent); } return cell; } /** * Push scroll events received by cell nodes directly to * the 'owner' Node. (Generally likely to be a VirtualFlow * but not required.) * * Normal bubbling of scroll events gets interrupted during * a scroll gesture when the Cell's Node receiving the event * has moved out of the viewport and is thus removed from * the Navigator's children list. This breaks expected trackpad * scrolling behaviour, at least on macOS. * * So here we take over event-bubbling duties for ScrollEvent * and push them ourselves directly to the given owner. */ private void pushScrollEvent(ScrollEvent se) { owner.fireEvent(se); se.consume(); } private void presentCellsChanged(QuasiListModification<? extends C> mod) { // add removed cells back to the pool for(C cell: mod.getRemoved()) { cellPool.acceptCell(cell); } // update indices of added cells and cells after the added cells for(int i = mod.getFrom(); i < presentCells.size(); ++i) { presentCells.get(i).updateIndex(cells.indexOfMemoizedItem(i)); } } }