javafx.scene.control.skin.VirtualFlow Maven / Gradle / Ivy
/*
* Copyright (c) 2010, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package javafx.scene.control.skin;
import com.sun.javafx.scene.ParentHelper;
import com.sun.javafx.scene.control.Logging;
import com.sun.javafx.scene.control.Properties;
import com.sun.javafx.scene.control.VirtualScrollBar;
import com.sun.javafx.scene.control.skin.Utils;
import com.sun.javafx.scene.traversal.Algorithm;
import com.sun.javafx.scene.traversal.Direction;
import com.sun.javafx.scene.traversal.ParentTraversalEngine;
import com.sun.javafx.scene.traversal.TraversalContext;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.BooleanPropertyBase;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.collections.ObservableList;
import javafx.event.EventDispatcher;
import javafx.event.EventHandler;
import javafx.geometry.Orientation;
import javafx.scene.AccessibleRole;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Cell;
import javafx.scene.control.IndexedCell;
import javafx.scene.control.ScrollBar;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Rectangle;
import javafx.util.Callback;
import javafx.util.Duration;
import com.sun.javafx.logging.PlatformLogger;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.List;
/**
* Implementation of a virtualized container using a cell based mechanism. This
* is used by the skin implementations for UI controls such as
* {@link javafx.scene.control.ListView}, {@link javafx.scene.control.TreeView},
* {@link javafx.scene.control.TableView}, and {@link javafx.scene.control.TreeTableView}.
*
* @param The type of the item contained within the virtual flow cells
* @since 9
*/
public class VirtualFlow extends Region {
/* *************************************************************************
* *
* Static fields *
* *
**************************************************************************/
/**
* Scroll events may request to scroll about a number of "lines". We first
* decide how big one "line" is - for fixed cell size it's clear,
* for variable cell size we settle on a single number so that the scrolling
* speed is consistent. Now if the line is so big that
* MIN_SCROLLING_LINES_PER_PAGE of them don't fit into one page, we make
* them smaller to prevent the scrolling step to be too big (perhaps
* even more than one page).
*/
private static final int MIN_SCROLLING_LINES_PER_PAGE = 8;
/**
* Indicates that this is a newly created cell and we need call processCSS for it.
*
* See RT-23616 for more details.
*/
private static final String NEW_CELL = "newcell";
private static final double GOLDEN_RATIO_MULTIPLIER = 0.618033987;
/**
* The default improvement for the estimation of the total size. A value
* of x means that every time we need to estimate the size, we will add
* x new cells that are not yet available into the calculations
*/
private static final int DEFAULT_IMPROVEMENT = 2;
/* *************************************************************************
* *
* Private fields *
* *
**************************************************************************/
private boolean touchDetected = false;
private boolean mouseDown = false;
/**
* The width of the VirtualFlow the last time it was laid out. We
* use this information for several fast paths during the layout pass.
*/
double lastWidth = -1;
/**
* The height of the VirtualFlow the last time it was laid out. We
* use this information for several fast paths during the layout pass.
*/
double lastHeight = -1;
/**
* The number of "virtual" cells in the flow the last time it was laid out.
* For example, there may have been 1000 virtual cells, but only 20 actual
* cells created and in use. In that case, lastCellCount would be 1000.
*/
int lastCellCount = 0;
/**
* We remember the last value for vertical the last time we laid out the
* flow. If vertical has changed, we will want to change the max & value
* for the different scroll bars. Since we do all the scroll bar update
* work in the layoutChildren function, we need to know what the old value for
* vertical was.
*/
boolean lastVertical;
/**
* The position last time we laid out. If none of the lastXXX vars have
* changed respective to their values in layoutChildren, then we can just punt
* out of the method (I hope...)
*/
double lastPosition;
/**
* The breadth of the first visible cell last time we laid out.
*/
double lastCellBreadth = -1;
/**
* The length of the first visible cell last time we laid out.
*/
double lastCellLength = -1;
/**
* The list of cells representing those cells which actually make up the
* current view. The cells are ordered such that the first cell in this
* list is the first in the view, and the last cell is the last in the
* view. When pixel scrolling, the list is simply shifted and items drop
* off the beginning or the end, depending on the order of scrolling.
*
* This is package private ONLY FOR TESTING
*/
final ArrayLinkedList cells = new ArrayLinkedList<>();
/**
* A structure containing cells that can be reused later. These are cells
* that at one time were needed to populate the view, but now are no longer
* needed. We keep them here until they are needed again.
*
* This is package private ONLY FOR TESTING
*/
final ArrayLinkedList pile = new ArrayLinkedList<>();
/**
* A special cell used to accumulate bounds, such that we reduce object
* churn. This cell must be recreated whenever the cell factory function
* changes. This has package access ONLY for testing.
*/
T accumCell;
/**
* This group is used for holding the 'accumCell'. 'accumCell' must
* be added to the skin for it to be styled. Otherwise, it doesn't
* report the correct width/height leading to issues when scrolling
* the flow
*/
Group accumCellParent;
/**
* The group which holds the cells.
*/
final Group sheet;
final ObservableList sheetChildren;
/**
* The scroll bar used for scrolling horizontally. This has package access
* ONLY for testing.
*/
private VirtualScrollBar hbar = new VirtualScrollBar(this);
/**
* The scroll bar used to scrolling vertically. This has package access
* ONLY for testing.
*/
private VirtualScrollBar vbar = new VirtualScrollBar(this);
/**
* Control in which the cell's sheet is placed and forms the viewport. The
* viewportBreadth and viewportLength are simply the dimensions of the
* clipView. This has package access ONLY for testing.
*/
ClippedContainer clipView;
/**
* When both the horizontal and vertical scroll bars are visible,
* we have to 'fill in' the bottom right corner where the two scroll bars
* meet. This is handled by this corner region. This has package access
* ONLY for testing.
*/
StackPane corner;
/**
* The offset in pixels between the top of the virtualFlow and the content it
* shows. When manipulating the position of the content (e.g. by scrolling),
* the absoluteOffset must be changed so that it always returns the number of
* pixels that, when applied to a translateY (for vertical) or translateX
* (for horizontal) operation on each cell, the first cell aligns with the
* node.
* The following relation should always be true:
* 0 <= absoluteOffset <= (estimatedSize - viewportLength)
* Based on this relation, the position p is defined as
* 0 <= absoluteOffset/(estimatedSize - viewportLength) <= 1
* As a consequence, whenever p, estimatedSize, or viewportLength
* changes, the absoluteOffset needs to change as well.
* The method adjustAbsoluteOffset()
can be used to calculate the
* value of absoluteOffset
based on the value of the other 3
* variables.
* Vice versa, if we change the absoluteOffset
, we need to make
* sure that the position
is changed in a consistent way. This
* can be done by calling adjustPosition()
*/
double absoluteOffset = 0d;
/**
* An estimation of the total size (height for vertical, width for horizontal).
* A value of -1 means that this value is unusable and should not be trusted.
* This might happen before any calculations take place, or when a method
* invocation is guaranteed to invalidate the current estimation.
*/
double estimatedSize = -1d;
/**
* A list containing the cached version of the calculated size (height for
* vertical, width for horizontal) for a (fictive or real) cell for
* each element of the backing data.
* This list is used to calculate the estimatedSize.
* The list is not expected to be complete, but it is always up to date.
* When the size of the items in the backing list changes, this list is
* cleared.
*/
private ArrayList itemSizeCache = new ArrayList<>();
// used for panning the virtual flow
private double lastX;
private double lastY;
private boolean isPanning = false;
private boolean fixedCellSizeEnabled = false;
private boolean needsReconfigureCells = false; // when cell contents are the same
private boolean needsRecreateCells = false; // when cell factory changed
private boolean needsRebuildCells = false; // when cell contents have changed
private boolean needsCellsLayout = false;
private boolean sizeChanged = false;
private final BitSet dirtyCells = new BitSet();
Timeline sbTouchTimeline;
KeyFrame sbTouchKF1;
KeyFrame sbTouchKF2;
private boolean needBreadthBar;
private boolean needLengthBar;
private boolean tempVisibility = false;
private boolean suppressBreadthBar;
/* *************************************************************************
* *
* Constructors *
* *
**************************************************************************/
/**
* Creates a new VirtualFlow instance.
*/
public VirtualFlow() {
getStyleClass().add("virtual-flow");
setId("virtual-flow");
// initContent
// --- sheet
sheet = new Group();
sheet.getStyleClass().add("sheet");
sheet.setAutoSizeChildren(false);
sheetChildren = sheet.getChildren();
// --- clipView
clipView = new ClippedContainer(this);
clipView.setNode(sheet);
getChildren().add(clipView);
// --- accumCellParent
accumCellParent = new Group();
accumCellParent.setVisible(false);
getChildren().add(accumCellParent);
/*
** don't allow the ScrollBar to handle the ScrollEvent,
** In a VirtualFlow a vertical scroll should scroll on the vertical only,
** whereas in a horizontal ScrollBar it can scroll horizontally.
*/
// block the event from being passed down to children
final EventDispatcher blockEventDispatcher = (event, tail) -> event;
// block ScrollEvent from being passed down to scrollbar's skin
final EventDispatcher oldHsbEventDispatcher = hbar.getEventDispatcher();
hbar.setEventDispatcher((event, tail) -> {
if (event.getEventType() == ScrollEvent.SCROLL &&
!((ScrollEvent)event).isDirect()) {
tail = tail.prepend(blockEventDispatcher);
tail = tail.prepend(oldHsbEventDispatcher);
return tail.dispatchEvent(event);
}
return oldHsbEventDispatcher.dispatchEvent(event, tail);
});
// block ScrollEvent from being passed down to scrollbar's skin
final EventDispatcher oldVsbEventDispatcher = vbar.getEventDispatcher();
vbar.setEventDispatcher((event, tail) -> {
if (event.getEventType() == ScrollEvent.SCROLL &&
!((ScrollEvent)event).isDirect()) {
tail = tail.prepend(blockEventDispatcher);
tail = tail.prepend(oldVsbEventDispatcher);
return tail.dispatchEvent(event);
}
return oldVsbEventDispatcher.dispatchEvent(event, tail);
});
/*
** listen for ScrollEvents over the whole of the VirtualFlow
** area, the above dispatcher having removed the ScrollBars
** scroll event handling.
*/
setOnScroll(new EventHandler() {
@Override public void handle(ScrollEvent event) {
if (Properties.IS_TOUCH_SUPPORTED) {
if (touchDetected == false && mouseDown == false ) {
startSBReleasedAnimation();
}
}
/*
** calculate the delta in the direction of the flow.
*/
double virtualDelta = 0.0;
if (isVertical()) {
switch(event.getTextDeltaYUnits()) {
case PAGES:
virtualDelta = event.getTextDeltaY() * lastHeight;
break;
case LINES:
double lineSize;
if (fixedCellSizeEnabled) {
lineSize = getFixedCellSize();
} else {
// For the scrolling to be reasonably consistent
// we set the lineSize to the average size
// of all currently loaded lines.
T lastCell = cells.getLast();
lineSize =
(getCellPosition(lastCell)
+ getCellLength(lastCell)
- getCellPosition(cells.getFirst()))
/ cells.size();
}
if (lastHeight / lineSize < MIN_SCROLLING_LINES_PER_PAGE) {
lineSize = lastHeight / MIN_SCROLLING_LINES_PER_PAGE;
}
virtualDelta = event.getTextDeltaY() * lineSize;
break;
case NONE:
virtualDelta = event.getDeltaY();
}
} else { // horizontal
switch(event.getTextDeltaXUnits()) {
case CHARACTERS:
// can we get character size here?
// for now, fall through to pixel values
case NONE:
double dx = event.getDeltaX();
double dy = event.getDeltaY();
virtualDelta = (Math.abs(dx) > Math.abs(dy) ? dx : dy);
}
}
if (virtualDelta != 0.0) {
/*
** only consume it if we use it
*/
double result = scrollPixels(-virtualDelta);
if (result != 0.0) {
event.consume();
}
}
ScrollBar nonVirtualBar = isVertical() ? hbar : vbar;
if (needBreadthBar) {
double nonVirtualDelta = isVertical() ? event.getDeltaX() : event.getDeltaY();
if (nonVirtualDelta != 0.0) {
double newValue = nonVirtualBar.getValue() - nonVirtualDelta;
if (newValue < nonVirtualBar.getMin()) {
nonVirtualBar.setValue(nonVirtualBar.getMin());
} else if (newValue > nonVirtualBar.getMax()) {
nonVirtualBar.setValue(nonVirtualBar.getMax());
} else {
nonVirtualBar.setValue(newValue);
}
event.consume();
}
}
}
});
addEventFilter(MouseEvent.MOUSE_PRESSED, new EventHandler() {
@Override
public void handle(MouseEvent e) {
mouseDown = true;
if (Properties.IS_TOUCH_SUPPORTED) {
scrollBarOn();
}
if (isFocusTraversable()) {
// We check here to see if the current focus owner is within
// this VirtualFlow, and if so we back-off from requesting
// focus back to the VirtualFlow itself. This is particularly
// relevant given the bug identified in RT-32869. In this
// particular case TextInputControl was clearing selection
// when the focus on the TextField changed, meaning that the
// right-click context menu was not showing the correct
// options as there was no selection in the TextField.
boolean doFocusRequest = true;
Node focusOwner = getScene().getFocusOwner();
if (focusOwner != null) {
Parent parent = focusOwner.getParent();
while (parent != null) {
if (parent.equals(VirtualFlow.this)) {
doFocusRequest = false;
break;
}
parent = parent.getParent();
}
}
if (doFocusRequest) {
requestFocus();
}
}
lastX = e.getX();
lastY = e.getY();
// determine whether the user has push down on the virtual flow,
// or whether it is the scrollbar. This is done to prevent
// mouse events being 'doubled up' when dragging the scrollbar
// thumb - it has the side-effect of also starting the panning
// code, leading to flicker
isPanning = ! (vbar.getBoundsInParent().contains(e.getX(), e.getY())
|| hbar.getBoundsInParent().contains(e.getX(), e.getY()));
}
});
addEventFilter(MouseEvent.MOUSE_RELEASED, e -> {
mouseDown = false;
if (Properties.IS_TOUCH_SUPPORTED) {
startSBReleasedAnimation();
}
});
addEventFilter(MouseEvent.MOUSE_DRAGGED, e -> {
if (Properties.IS_TOUCH_SUPPORTED) {
scrollBarOn();
}
if (! isPanning || ! isPannable()) return;
// With panning enabled, we support panning in both vertical
// and horizontal directions, regardless of the fact that
// VirtualFlow is virtual in only one direction.
double xDelta = lastX - e.getX();
double yDelta = lastY - e.getY();
// figure out the distance that the mouse moved in the virtual
// direction, and then perform the movement along that axis
// virtualDelta will contain the amount we actually did move
double virtualDelta = isVertical() ? yDelta : xDelta;
double actual = scrollPixels(virtualDelta);
if (actual != 0) {
// update last* here, as we know we've just adjusted the
// scrollbar. This means we don't get the situation where a
// user presses-and-drags a long way past the min or max
// values, only to change directions and see the scrollbar
// start moving immediately.
if (isVertical()) lastY = e.getY();
else lastX = e.getX();
}
// similarly, we do the same in the non-virtual direction
double nonVirtualDelta = isVertical() ? xDelta : yDelta;
ScrollBar nonVirtualBar = isVertical() ? hbar : vbar;
if (nonVirtualBar.isVisible()) {
double newValue = nonVirtualBar.getValue() + nonVirtualDelta;
if (newValue < nonVirtualBar.getMin()) {
nonVirtualBar.setValue(nonVirtualBar.getMin());
} else if (newValue > nonVirtualBar.getMax()) {
nonVirtualBar.setValue(nonVirtualBar.getMax());
} else {
nonVirtualBar.setValue(newValue);
// same as the last* comment above
if (isVertical()) lastX = e.getX();
else lastY = e.getY();
}
}
});
/*
* We place the scrollbars _above_ the rectangle, such that the drag
* operations often used in conjunction with scrollbars aren't
* misinterpreted as drag operations on the rectangle as well (which
* would be the case if the scrollbars were underneath it as the
* rectangle itself doesn't block the mouse.
*/
// --- vbar
vbar.setOrientation(Orientation.VERTICAL);
vbar.addEventHandler(MouseEvent.ANY, event -> {
event.consume();
});
getChildren().add(vbar);
// --- hbar
hbar.setOrientation(Orientation.HORIZONTAL);
hbar.addEventHandler(MouseEvent.ANY, event -> {
event.consume();
});
getChildren().add(hbar);
// --- corner
corner = new StackPane();
corner.getStyleClass().setAll("corner");
getChildren().add(corner);
// initBinds
// clipView binds
InvalidationListener listenerX = valueModel -> {
updateHbar();
};
verticalProperty().addListener(listenerX);
hbar.valueProperty().addListener(listenerX);
hbar.visibleProperty().addListener(listenerX);
visibleProperty().addListener(listenerX);
sceneProperty().addListener(listenerX);
// ChangeListener listenerY = new ChangeListener() {
// @Override public void handle(Bean bean, PropertyReference property) {
// clipView.setClipY(isVertical() ? 0 : vbar.getValue());
// }
// };
// addChangedListener(VERTICAL, listenerY);
// vbar.addChangedListener(ScrollBar.VALUE, listenerY);
ChangeListener listenerY = (ov, t, t1) -> {
clipView.setClipY(isVertical() ? 0 : vbar.getValue());
};
vbar.valueProperty().addListener(listenerY);
super.heightProperty().addListener((observable, oldHeight, newHeight) -> {
// Fix for RT-8480, where the VirtualFlow does not show its content
// after changing size to 0 and back.
if (oldHeight.doubleValue() == 0 && newHeight.doubleValue() > 0) {
recreateCells();
}
});
/*
** there are certain animations that need to know if the touch is
** happening.....
*/
setOnTouchPressed(e -> {
touchDetected = true;
scrollBarOn();
});
setOnTouchReleased(e -> {
touchDetected = false;
startSBReleasedAnimation();
});
ParentHelper.setTraversalEngine(this, new ParentTraversalEngine(this, new Algorithm() {
Node selectNextAfterIndex(int index, TraversalContext context) {
T nextCell;
while ((nextCell = getVisibleCell(++index)) != null) {
if (nextCell.isFocusTraversable()) {
return nextCell;
}
Node n = context.selectFirstInParent(nextCell);
if (n != null) {
return n;
}
}
return null;
}
Node selectPreviousBeforeIndex(int index, TraversalContext context) {
T prevCell;
while ((prevCell = getVisibleCell(--index)) != null) {
Node prev = context.selectLastInParent(prevCell);
if (prev != null) {
return prev;
}
if (prevCell.isFocusTraversable()) {
return prevCell;
}
}
return null;
}
@Override
public Node select(Node owner, Direction dir, TraversalContext context) {
T cell;
if (cells.isEmpty()) return null;
if (cells.contains(owner)) {
cell = (T) owner;
} else {
cell = findOwnerCell(owner);
Node next = context.selectInSubtree(cell, owner, dir);
if (next != null) {
return next;
}
if (dir == Direction.NEXT) dir = Direction.NEXT_IN_LINE;
}
int cellIndex = cell.getIndex();
switch(dir) {
case PREVIOUS:
return selectPreviousBeforeIndex(cellIndex, context);
case NEXT:
Node n = context.selectFirstInParent(cell);
if (n != null) {
return n;
}
// Intentional fall-through
case NEXT_IN_LINE:
return selectNextAfterIndex(cellIndex, context);
}
return null;
}
private T findOwnerCell(Node owner) {
Parent p = owner.getParent();
while (!cells.contains(p)) {
p = p.getParent();
}
return (T)p;
}
@Override
public Node selectFirst(TraversalContext context) {
T firstCell = cells.getFirst();
if (firstCell == null) return null;
if (firstCell.isFocusTraversable()) return firstCell;
Node n = context.selectFirstInParent(firstCell);
if (n != null) {
return n;
}
return selectNextAfterIndex(firstCell.getIndex(), context);
}
@Override
public Node selectLast(TraversalContext context) {
T lastCell = cells.getLast();
if (lastCell == null) return null;
Node p = context.selectLastInParent(lastCell);
if (p != null) {
return p;
}
if (lastCell.isFocusTraversable()) return lastCell;
return selectPreviousBeforeIndex(lastCell.getIndex(), context);
}
}));
}
/* *************************************************************************
* *
* Properties *
* *
**************************************************************************/
/**
* There are two main complicating factors in the implementation of the
* VirtualFlow, which are made even more complicated due to the performance
* sensitive nature of this code. The first factor is the actual
* virtualization mechanism, wired together with the PositionMapper.
* The second complicating factor is the desire to do minimal layout
* and minimal updates to CSS.
*
* Since the layout mechanism runs at most once per pulse, we want to hook
* into this mechanism for minimal recomputation. Whenever a layout pass
* is run we record the width/height that the virtual flow was last laid
* out to. In subsequent passes, if the width/height has not changed then
* we know we only have to rebuild the cells. If the width or height has
* changed, then we can make appropriate decisions based on whether the
* width / height has been reduced or expanded.
*
* In various places, if requestLayout is called it is generally just
* used to indicate that some form of layout needs to happen (either the
* entire thing has to be reconstructed, or just the cells need to be
* reconstructed, generally).
*
* The accumCell is a special cell which is used in some computations
* when an actual cell for that item isn't currently available. However,
* the accumCell must be cleared whenever the cellFactory function is
* changed because we need to use the cells that come from the new factory.
*
* In addition to storing the lastWidth and lastHeight, we also store the
* number of cells that existed last time we performed a layout. In this
* way if the number of cells change, we can request a layout and when it
* occurs we can tell that the number of cells has changed and react
* accordingly.
*
* Because the VirtualFlow can be laid out horizontally or vertically a
* naming problem is present when trying to conceptualize and implement
* the flow. In particular, the words "width" and "height" are not
* precise when describing the unit of measure along the "virtualized"
* axis and the "orthogonal" axis. For example, the height of a cell when
* the flow is vertical is the magnitude along the "virtualized axis",
* and the width is along the axis orthogonal to it.
*
* Since "height" and "width" are not reliable terms, we use the words
* "length" and "breadth" to describe the magnitude of a cell along
* the virtualized axis and orthogonal axis. For example, in a vertical
* flow, the height=length and the width=breadth. In a horizontal axis,
* the height=breadth and the width=length.
*
* These terms are somewhat arbitrary, but chosen so that when reading
* most of the below code you can think in just one dimension, with
* helper functions converting width/height in to length/breadth, while
* also being different from width/height so as not to get confused with
* the actual width/height of a cell.
*/
// --- vertical
/**
* Indicates the primary direction of virtualization. If true, then the
* primary direction of virtualization is vertical, meaning that cells will
* stack vertically on top of each other. If false, then they will stack
* horizontally next to each other.
*/
private BooleanProperty vertical;
public final void setVertical(boolean value) {
verticalProperty().set(value);
}
public final boolean isVertical() {
return vertical == null ? true : vertical.get();
}
public final BooleanProperty verticalProperty() {
if (vertical == null) {
vertical = new BooleanPropertyBase(true) {
@Override protected void invalidated() {
resetIndex(cells);
resetIndex(pile);
pile.clear();
sheetChildren.clear();
cells.clear();
lastWidth = lastHeight = -1;
setMaxPrefBreadth(-1);
setViewportBreadth(0);
setViewportLength(0);
lastPosition = 0;
hbar.setValue(0);
vbar.setValue(0);
setPosition(0.0f);
setNeedsLayout(true);
requestLayout();
}
@Override
public Object getBean() {
return VirtualFlow.this;
}
@Override
public String getName() {
return "vertical";
}
};
}
return vertical;
}
// --- pannable
/**
* Indicates whether the VirtualFlow viewport is capable of being panned
* by the user (either via the mouse or touch events).
*/
private BooleanProperty pannable = new SimpleBooleanProperty(this, "pannable", true);
public final boolean isPannable() { return pannable.get(); }
public final void setPannable(boolean value) { pannable.set(value); }
public final BooleanProperty pannableProperty() { return pannable; }
// --- cell count
/**
* Indicates the number of cells that should be in the flow. The user of
* the VirtualFlow must set this appropriately. When the cell count changes
* the VirtualFlow responds by updating the visuals. If the items backing
* the cells change, but the count has not changed, you must call the
* reconfigureCells() function to update the visuals.
*/
private IntegerProperty cellCount = new SimpleIntegerProperty(this, "cellCount", 0) {
private int oldCount = 0;
@Override protected void invalidated() {
int oldIndex = computeCurrentIndex(oldCount);
double oldOffset = computeViewportOffset(getPosition(), oldCount);
int cellCount = get();
if (oldIndex > cellCount) {
oldIndex = cellCount;
}
resetSizeEstimates();
getOrCreateCellSize(oldIndex);
recalculateAndImproveEstimatedSize(DEFAULT_IMPROVEMENT, oldIndex, oldOffset);
boolean countChanged = oldCount != cellCount;
double boff = computeBaseOffset(oldIndex);
absoluteOffset = boff + oldOffset;
oldCount = cellCount;
// ensure that the virtual scrollbar adjusts in size based on the current
// cell count.
if (countChanged) {
VirtualScrollBar lengthBar = isVertical() ? vbar : hbar;
lengthBar.setMax(cellCount);
}
// I decided *not* to reset maxPrefBreadth here for the following
// situation. Suppose I have 30 cells and then I add 10 more. Just
// because I added 10 more doesn't mean the max pref should be
// reset. Suppose the first 3 cells were extra long, and I was
// scrolled down such that they weren't visible. If I were to reset
// maxPrefBreadth when subsequent cells were added or removed, then the
// scroll bars would erroneously reset as well. So I do not reset
// the maxPrefBreadth here.
// Fix for RT-12512, RT-14301 and RT-14864.
// Without this, the VirtualFlow length-wise scrollbar would not change
// as expected. This would leave items unable to be shown, as they
// would exist outside of the visible area, even when the scrollbar
// was at its maximum position.
// FIXME this should be only executed on the pulse, so this will likely
// lead to performance degradation until it is handled properly.
if (countChanged) {
layoutChildren();
Parent parent = getParent();
if (parent != null) parent.requestLayout();
adjustAbsoluteOffset();
}
// TODO suppose I had 100 cells and I added 100 more. Further
// suppose I was scrolled to the bottom when that happened. I
// actually want to update the position of the mapper such that
// the view remains "stable".
}
};
public final int getCellCount() { return cellCount.get(); }
public final void setCellCount(int value) {
cellCount.set(value);
}
public final IntegerProperty cellCountProperty() { return cellCount; }
// --- position
/**
* The position of the VirtualFlow within its list of cells. This is a value
* between 0 and 1.
*/
private DoubleProperty position = new SimpleDoubleProperty(this, "position") {
@Override public void setValue(Number v) {
super.setValue(com.sun.javafx.util.Utils.clamp(0, get(), 1));
}
@Override protected void invalidated() {
super.invalidated();
adjustAbsoluteOffset();
requestLayout();
}
};
public final double getPosition() { return position.get(); }
public final void setPosition(double value) {
position.set(value);
}
public final DoubleProperty positionProperty() { return position; }
// --- fixed cell size
/**
* For optimisation purposes, some use cases can trade dynamic cell length
* for speed - if fixedCellSize is greater than zero we'll use that rather
* than determine it by querying the cell itself.
*/
private DoubleProperty fixedCellSize = new SimpleDoubleProperty(this, "fixedCellSize") {
@Override protected void invalidated() {
fixedCellSizeEnabled = get() > 0;
needsCellsLayout = true;
layoutChildren();
}
};
public final void setFixedCellSize(final double value) { fixedCellSize.set(value); }
public final double getFixedCellSize() { return fixedCellSize.get(); }
public final DoubleProperty fixedCellSizeProperty() { return fixedCellSize; }
// --- Cell Factory
private ObjectProperty, T>> cellFactory;
/**
* Sets a new cell factory to use in the VirtualFlow. This forces all old
* cells to be thrown away, and new cells to be created with
* the new cell factory.
* @param value the new cell factory
*/
public final void setCellFactory(Callback, T> value) {
cellFactoryProperty().set(value);
}
/**
* Returns the current cell factory.
* @return the current cell factory
*/
public final Callback, T> getCellFactory() {
return cellFactory == null ? null : cellFactory.get();
}
/**
* Setting a custom cell factory has the effect of deferring all cell
* creation, allowing for total customization of the cell. Internally, the
* VirtualFlow is responsible for reusing cells - all that is necessary
* is for the custom cell factory to return from this function a cell
* which might be usable for representing any item in the VirtualFlow.
*
*
Refer to the {@link Cell} class documentation for more detail.
* @return the cell factory property
*/
public final ObjectProperty, T>> cellFactoryProperty() {
if (cellFactory == null) {
cellFactory = new SimpleObjectProperty<>(this, "cellFactory") {
@Override protected void invalidated() {
if (get() != null) {
setNeedsLayout(true);
recreateCells();
if (getParent() != null) getParent().requestLayout();
}
if (accumCellParent != null) {
accumCellParent.getChildren().clear();
}
accumCell = null;
}
};
}
return cellFactory;
}
/* *************************************************************************
* *
* Public API *
* *
**************************************************************************/
/**
* Overridden to implement somewhat more efficient support for layout. The
* VirtualFlow can generally be considered as being unmanaged, in that
* whenever the position changes, or other such things change, we need
* to perform a layout but there is no reason to notify the parent. However
* when things change which may impact the preferred size (such as
* vertical, createCell, and configCell) then we need to notify the
* parent.
*/
@Override public void requestLayout() {
// Note: This block is commented as it was relaying on a bad assumption on how
// layout request was handled in parent class that is now fixed.
//
// // isNeedsLayout() is commented out due to RT-21417. This does not
// // appear to impact performance (indeed, it may help), and resolves the
// // issue identified in RT-21417.
// setNeedsLayout(true);
// The fix is to prograte this layout request to its parent class.
// A better fix will be required if performance is negatively affected
// by this fix.
super.requestLayout();
}
/**
* Keep the position constant and adjust the absoluteOffset to
* match the (new) position.
*/
void adjustAbsoluteOffset() {
absoluteOffset = (estimatedSize - viewportLength) * getPosition();
}
/**
* Keep the absoluteOffset constant and adjust the position to match
* the (new) absoluteOffset.
*/
void adjustPosition() {
if (viewportLength >= estimatedSize) {
setPosition(0.);
} else {
setPosition(absoluteOffset / (estimatedSize - viewportLength));
}
}
/** {@inheritDoc} */
@Override protected void layoutChildren() {
// when we enter this method, the absoluteOffset and position are
// already determined. In case this method invokes other methods
// that may change either absoluteOffset or position, it is the
// responsability of those methods to make sure both parameters are
// changed in a consistent way.
// For example, the recalculateEstimatedSize method also recalculates
// the absoluteOffset and position.
if (needsRecreateCells) {
lastWidth = -1;
lastHeight = -1;
releaseCell(accumCell);
sheet.getChildren().clear();
resetIndex(cells);
resetIndex(pile);
cells.clear();
pile.clear();
releaseAllPrivateCells();
} else if (needsRebuildCells) {
lastWidth = -1;
lastHeight = -1;
releaseCell(accumCell);
resetIndex(cells);
addAllToPile();
releaseAllPrivateCells();
} else if (needsReconfigureCells) {
setMaxPrefBreadth(-1);
lastWidth = -1;
lastHeight = -1;
}
if (! dirtyCells.isEmpty()) {
int index;
final int cellsSize = cells.size();
while ((index = dirtyCells.nextSetBit(0)) != -1 && index < cellsSize) {
T cell = cells.get(index);
// updateIndex(-1) works for TableView, but breaks ListView.
// For now, the TableView just does not use the dirtyCells API
// cell.updateIndex(-1);
if (cell != null) {
cell.requestLayout();
}
dirtyCells.clear(index);
}
setMaxPrefBreadth(-1);
lastWidth = -1;
lastHeight = -1;
}
final boolean hasSizeChange = sizeChanged;
boolean recreatedOrRebuilt = needsRebuildCells || needsRecreateCells || sizeChanged;
needsRecreateCells = false;
needsReconfigureCells = false;
needsRebuildCells = false;
sizeChanged = false;
if (needsCellsLayout) {
for (int i = 0, max = cells.size(); i < max; i++) {
T cell = cells.get(i);
if (cell != null) {
cell.requestLayout();
}
}
needsCellsLayout = false;
// yes, we return here - if needsCellsLayout was set to true, we
// only did it to do the above - not rerun the entire layout.
return;
}
final double width = getWidth();
final double height = getHeight();
final boolean isVertical = isVertical();
final double position = getPosition();
// if the width and/or height is 0, then there is no point doing
// any of this work. In particular, this can happen during startup
if (width <= 0 || height <= 0) {
addAllToPile();
lastWidth = width;
lastHeight = height;
hbar.setVisible(false);
vbar.setVisible(false);
corner.setVisible(false);
return;
}
// we check if any of the cells in the cells list need layout. This is a
// sign that they are perhaps animating their sizes. Without this check,
// we may not perform a layout here, meaning that the cell will likely
// 'jump' (in height normally) when the user drags the virtual thumb as
// that is the first time the layout would occur otherwise.
boolean cellNeedsLayout = false;
boolean thumbNeedsLayout = false;
if (Properties.IS_TOUCH_SUPPORTED) {
if ((tempVisibility == true && (hbar.isVisible() == false || vbar.isVisible() == false)) ||
(tempVisibility == false && (hbar.isVisible() == true || vbar.isVisible() == true))) {
thumbNeedsLayout = true;
}
}
if (!cellNeedsLayout) {
for (int i = 0; i < cells.size(); i++) {
T cell = cells.get(i);
cellNeedsLayout = cell.isNeedsLayout();
if (cellNeedsLayout) break;
}
}
final int cellCount = getCellCount();
final T firstCell = getFirstVisibleCell();
// If no cells need layout, we check other criteria to see if this
// layout call is even necessary. If it is found that no layout is
// needed, we just punt.
if (! cellNeedsLayout && !thumbNeedsLayout) {
boolean cellSizeChanged = false;
if (firstCell != null) {
double breadth = getCellBreadth(firstCell);
double length = getCellLength(firstCell);
cellSizeChanged = (breadth != lastCellBreadth) || (length != lastCellLength);
lastCellBreadth = breadth;
lastCellLength = length;
}
if (width == lastWidth &&
height == lastHeight &&
cellCount == lastCellCount &&
isVertical == lastVertical &&
position == lastPosition &&
! cellSizeChanged)
{
// TODO this happens to work around the problem tested by
// testCellLayout_LayoutWithoutChangingThingsUsesCellsInSameOrderAsBefore
// but isn't a proper solution. Really what we need to do is, when
// laying out cells, we need to make sure that if a cell is pressed
// AND we are doing a full rebuild then we need to make sure we
// use that cell in the same physical location as before so that
// it gets the mouse release event.
return;
}
}
/*
* This function may get called under a variety of circumstances.
* It will determine what has changed from the last time it was laid
* out, and will then take one of several execution paths based on
* what has changed so as to perform minimal layout work and also to
* give the expected behavior. One or more of the following may have
* happened:
*
* 1) width/height has changed
* - If the width and/or height has been reduced (but neither of
* them has been expanded), then we simply have to reposition and
* resize the scroll bars
* - If the width (in the vertical case) has expanded, then we
* need to resize the existing cells and reposition and resize
* the scroll bars
* - If the height (in the vertical case) has expanded, then we
* need to resize and reposition the scroll bars and add
* any trailing cells
*
* 2) cell count has changed
* - If the number of cells is bigger, or it is smaller but not
* so small as to move the position then we can just update the
* cells in place without performing layout and update the
* scroll bars.
* - If the number of cells has been reduced and it affects the
* position, then move the position and rebuild all the cells
* and update the scroll bars
*
* 3) size of the cell has changed
* - If the size changed in the virtual direction (ie: height
* in the case of vertical) then layout the cells, adding
* trailing cells as necessary and updating the scroll bars
* - If the size changed in the non virtual direction (ie: width
* in the case of vertical) then simply adjust the widths of
* the cells as appropriate and adjust the scroll bars
*
* 4) vertical changed, cells is empty, maxPrefBreadth == -1, etc
* - Full rebuild.
*
* Each of the conditions really resolves to several of a handful of
* possible outcomes:
* a) reposition & rebuild scroll bars
* b) resize cells in non-virtual direction
* c) add trailing cells
* d) update cells
* e) resize cells in the virtual direction
* f) all of the above
*
* So this function first determines what outcomes need to occur, and
* then will execute all the ones that really need to happen. Every code
* path ends up touching the "reposition & rebuild scroll bars" outcome,
* so that one will be executed every time.
*/
boolean needTrailingCells = false;
boolean rebuild = cellNeedsLayout ||
isVertical != lastVertical ||
cells.isEmpty() ||
getMaxPrefBreadth() == -1 ||
position != lastPosition ||
cellCount != lastCellCount ||
hasSizeChange ||
(isVertical && height < lastHeight) || (! isVertical && width < lastWidth);
if (!rebuild) {
// Check if maxPrefBreadth didn't change
double maxPrefBreadth = getMaxPrefBreadth();
boolean foundMax = false;
for (int i = 0; i < cells.size(); ++i) {
double breadth = getCellBreadth(cells.get(i));
if (maxPrefBreadth == breadth) {
foundMax = true;
} else if (breadth > maxPrefBreadth) {
rebuild = true;
break;
}
}
if (!foundMax) { // All values were lower
rebuild = true;
}
}
if (! rebuild) {
if ((isVertical && height > lastHeight) || (! isVertical && width > lastWidth)) {
// resized in the virtual direction
needTrailingCells = true;
}
}
initViewport();
// Get the index of the "current" cell
int currentIndex = computeCurrentIndex();
if (lastCellCount != cellCount) {
// The cell count has changed. We want to keep the viewport
// stable if possible. If position was 0 or 1, we want to keep
// the position in the same place. If the new cell count is >=
// the currentIndex, then we will adjust the position to be 1.
// Otherwise, our goal is to leave the index of the cell at the
// top consistent, with the same translation etc.
if (position != 0 && position != 1 && (currentIndex >= cellCount)) {
setPosition(1.0f);
}
// Update the current index
currentIndex = computeCurrentIndex();
}
if (rebuild) {
setMaxPrefBreadth(-1);
// Start by dumping all the cells into the pile
addAllToPile();
// The distance from the top of the viewport to the top of the
// cell for the current index.
double offset = -computeViewportOffset(getPosition());
// Add all the leading and trailing cells (the call to add leading
// cells will add the current cell as well -- that is, the one that
// represents the current position on the mapper).
addLeadingCells(currentIndex, offset);
// Force filling of space with empty cells if necessary
addTrailingCells(true);
} else if (needTrailingCells) {
addTrailingCells(true);
}
computeBarVisiblity();
recalculateAndImproveEstimatedSize(0);
recreatedOrRebuilt = recreatedOrRebuilt || rebuild;
updateScrollBarsAndCells(recreatedOrRebuilt);
lastWidth = getWidth();
lastHeight = getHeight();
lastCellCount = getCellCount();
lastVertical = isVertical();
lastPosition = getPosition();
recalculateEstimatedSize();
cleanPile();
}
/**
* Resets the index to -1 to all cells.
* This is to properly clean them up and ensure that no listeners are called because they retain their old index.
*
* @param cells the cells
*/
private void resetIndex(ArrayLinkedList cells) {
for (T cell : cells) {
cell.updateIndex(-1);
}
}
/** {@inheritDoc} */
@Override protected void setWidth(double value) {
if (value != lastWidth) {
super.setWidth(value);
sizeChanged = true;
setNeedsLayout(true);
requestLayout();
}
}
/** {@inheritDoc} */
@Override protected void setHeight(double value) {
if (value != lastHeight) {
super.setHeight(value);
sizeChanged = true;
setNeedsLayout(true);
requestLayout();
}
}
/**
* Get a cell which can be used in the layout. This function will reuse
* cells from the pile where possible, and will create new cells when
* necessary.
* @param prefIndex the preferred index
* @return the available cell
*/
protected T getAvailableCell(int prefIndex) {
T cell = null;
// Fix for RT-12822. We try to retrieve the cell from the pile rather
// than just grab a random cell from the pile (or create another cell).
for (int i = 0, max = pile.size(); i < max; i++) {
T _cell = pile.get(i);
assert _cell != null;
if (getCellIndex(_cell) == prefIndex) {
cell = _cell;
pile.remove(i);
break;
}
}
if (cell == null && !pile.isEmpty()) {
cell = pile.removeLast();
}
if (cell == null) {
cell = getCellFactory().call(this);
cell.getProperties().put(NEW_CELL, null);
}
if (cell.getParent() == null) {
sheetChildren.add(cell);
}
return cell;
}
/**
* This method will remove all cells from the VirtualFlow and remove them,
* adding them to the 'pile' (that is, a place from where cells can be used
* at a later date). This method is protected to allow subclasses to clean up
* appropriately.
*/
protected void addAllToPile() {
for (int i = 0, max = cells.size(); i < max; i++) {
addToPile(cells.removeFirst());
}
}
/**
* Gets a cell for the given index if the cell has been created and laid out.
* "Visible" is a bit of a misnomer, the cell might not be visible in the
* viewport (it may be clipped), but does distinguish between cells that
* have been created and are in use vs. those that are in the pile or
* not created.
* @param index the index
* @return the visible cell
*/
public T getVisibleCell(int index) {
if (cells.isEmpty()) return null;
// check the last index
T lastCell = cells.getLast();
int lastIndex = getCellIndex(lastCell);
if (index == lastIndex) return lastCell;
// check the first index
T firstCell = cells.getFirst();
int firstIndex = getCellIndex(firstCell);
if (index == firstIndex) return firstCell;
// if index is > firstIndex and < lastIndex then we can get the index
if (index > firstIndex && index < lastIndex) {
T cell = cells.get(index - firstIndex);
if (getCellIndex(cell) == index) return cell;
}
// there is no visible cell for the specified index
return null;
}
/**
* Locates and returns the last non-empty IndexedCell that is currently
* partially or completely visible. This function may return null if there
* are no cells, or if the viewport length is 0.
* @return the last visible cell
*/
public T getLastVisibleCell() {
if (cells.isEmpty() || getViewportLength() <= 0) return null;
T cell;
for (int i = cells.size() - 1; i >= 0; i--) {
cell = cells.get(i);
if (! cell.isEmpty()) {
return cell;
}
}
return null;
}
/**
* Locates and returns the first non-empty IndexedCell that is partially or
* completely visible. This really only ever returns null if there are no
* cells or the viewport length is 0.
* @return the first visible cell
*/
public T getFirstVisibleCell() {
if (cells.isEmpty() || getViewportLength() <= 0) return null;
T cell = cells.getFirst();
return cell.isEmpty() ? null : cell;
}
/**
* Adjust the position of cells so that the specified cell
* will be positioned at the start of the viewport. The given cell must
* already be "live".
* @param firstCell the first cell
*/
public void scrollToTop(T firstCell) {
if (firstCell != null) {
scrollPixels(getCellPosition(firstCell));
}
}
/**
* Adjust the position of cells so that the specified cell
* will be positioned at the end of the viewport. The given cell must
* already be "live".
* @param lastCell the last cell
*/
public void scrollToBottom(T lastCell) {
if (lastCell != null) {
scrollPixels(getCellPosition(lastCell) + getCellLength(lastCell) - getViewportLength());
}
}
/**
* Adjusts the cells such that the selected cell will be fully visible in
* the viewport (but only just).
* @param cell the cell
*/
public void scrollTo(T cell) {
if (cell != null) {
final double start = getCellPosition(cell);
final double length = getCellLength(cell);
final double end = start + length;
final double viewportLength = getViewportLength();
if (start < 0) {
scrollPixels(start);
} else if (end > viewportLength) {
scrollPixels(end - viewportLength);
}
}
}
/**
* Adjusts the cells such that the cell in the given index will be fully visible in
* the viewport.
* @param index the index
*/
public void scrollTo(int index) {
T cell = getVisibleCell(index);
if (cell != null) {
scrollTo(cell);
} else {
// see JDK-8197536
if (tryScrollOneCell(index, true)) {
return;
} else if (tryScrollOneCell(index, false)) {
return;
}
adjustPositionToIndex(index);
addAllToPile();
requestLayout();
}
}
// will return true if scroll is successful
private boolean tryScrollOneCell(int targetIndex, boolean downOrRight) {
// if going down, cell diff is -1, because it will get the target cell index and check if previous
// cell is visible to base the position
int indexDiff = downOrRight ? -1 : 1;
T targetCell = getVisibleCell(targetIndex + indexDiff);
if (targetCell != null) {
T cell = getAvailableCell(targetIndex);
setCellIndex(cell, targetIndex);
resizeCell(cell);
setMaxPrefBreadth(Math.max(getMaxPrefBreadth(), getCellBreadth(cell)));
cell.setVisible(true);
if (downOrRight) {
cells.addLast(cell);
scrollPixels(getCellLength(cell));
} else {
// up or left
cells.addFirst(cell);
scrollPixels(-getCellLength(cell));
}
return true;
}
return false;
}
/**
* Adjusts the cells such that the cell in the given index will be fully visible in
* the viewport, and positioned at the very top of the viewport.
* @param index the index
*/
public void scrollToTop(int index) {
// make sure we have the real size of cells that are likely to be rendered
getCellSizesInExpectedViewport(index);
boolean posSet = false;
if (index > getCellCount() - 1) {
setPosition(1);
posSet = true;
} else if (index < 0) {
setPosition(0);
posSet = true;
}
if (! posSet) {
adjustPositionToIndex(index);
}
requestLayout();
}
// //TODO We assume all the cell have the same length. We will need to support
// // cells of different lengths.
// public void scrollToOffset(int offset) {
// scrollPixels(offset * getCellLength(0));
// }
/**
* Given a delta value representing a number of pixels, this method attempts
* to move the VirtualFlow in the given direction (positive is down/right,
* negative is up/left) the given number of pixels. It returns the number of
* pixels actually moved.
* @param delta the delta value
* @return the number of pixels actually moved
*/
public double scrollPixels(final double delta) {
int oldIndex = computeCurrentIndex();
// Short cut this method for cases where nothing should be done
if (delta == 0) return 0;
final boolean isVertical = isVertical();
if (((isVertical && (tempVisibility ? !needLengthBar : !vbar.isVisible())) ||
(! isVertical && (tempVisibility ? !needLengthBar : !hbar.isVisible())))) return 0;
double pos = getPosition();
if (pos == 0.0f && delta < 0) return 0;
if (pos == 1.0f && delta > 0) return 0;
getCellSizesInExpectedViewport(oldIndex);
recalculateEstimatedSize();
double answer = adjustByPixelAmount(delta);
if (pos == getPosition()) {
// The pos hasn't changed, there's nothing to do. This is likely
// to occur when we hit either extremity
return 0;
}
// Now move stuff around. Translating by pixels fundamentally means
// moving the cells by the delta. However, after having
// done that, we need to go through the cells and see which cells,
// after adding in the translation factor, now fall off the viewport.
// Also, we need to add cells as appropriate to the end (or beginning,
// depending on the direction of travel).
//
// One simplifying assumption (that had better be true!) is that we
// will only make it this far in the function if the virtual scroll
// bar is visible. Otherwise, we never will pixel scroll. So as we go,
// if we find that the maxPrefBreadth exceeds the viewportBreadth,
// then we will be sure to show the breadthBar and update it
// accordingly.
if (cells.size() > 0) {
for (int i = 0; i < cells.size(); i++) {
T cell = cells.get(i);
assert cell != null;
positionCell(cell, getCellPosition(cell) - delta);
}
// Fix for RT-32908
T firstCell = cells.getFirst();
double layoutY = firstCell == null ? 0 : getCellPosition(firstCell);
for (int i = 0; i < cells.size(); i++) {
T cell = cells.get(i);
assert cell != null;
double actualLayoutY = getCellPosition(cell);
if (Math.abs(actualLayoutY - layoutY) > 0.001) {
// we need to shift the cell to layoutY
positionCell(cell, layoutY);
}
layoutY += getCellLength(cell);
}
// end of fix for RT-32908
cull();
firstCell = cells.getFirst();
// Add any necessary leading cells
if (firstCell != null) {
// NOTE: The index might be -1, but the call to addLeadingCells() is still required.
int previousIndex = getCellIndex(firstCell) - 1;
double prevIndexSize = getCellLength(previousIndex);
addLeadingCells(previousIndex, getCellPosition(firstCell) - prevIndexSize);
} else {
int currentIndex = computeCurrentIndex();
// The distance from the top of the viewport to the top of the
// cell for the current index.
double offset = -computeViewportOffset(getPosition());
// Add all the leading and trailing cells (the call to add leading
// cells will add the current cell as well -- that is, the one that
// represents the current position on the mapper).
addLeadingCells(currentIndex, offset);
}
// Starting at the tail of the list, loop adding cells until
// all the space on the table is filled up. We want to make
// sure that we DO NOT add empty trailing cells (since we are
// in the full virtual case and so there are no trailing empty
// cells).
if (! addTrailingCells(false)) {
// Reached the end, but not enough cells to fill up to
// the end. So, remove the trailing empty space, and translate
// the cells down
final T lastCell = getLastVisibleCell();
final double lastCellSize = getCellLength(lastCell);
final double cellEnd = getCellPosition(lastCell) + lastCellSize;
final double viewportLength = getViewportLength();
if (cellEnd < viewportLength) {
// Reposition the nodes
double emptySize = viewportLength - cellEnd;
for (int i = 0; i < cells.size(); i++) {
T cell = cells.get(i);
positionCell(cell, getCellPosition(cell) + emptySize);
}
setPosition(1.0f);
// fill the leading empty space
firstCell = cells.getFirst();
int firstIndex = getCellIndex(firstCell);
double prevIndexSize = getCellLength(firstIndex - 1);
addLeadingCells(firstIndex - 1, getCellPosition(firstCell) - prevIndexSize);
}
}
}
// Now throw away any cells that don't fit
cull();
// Finally, update the scroll bars
updateScrollBarsAndCells(false);
// notify
return answer;
}
/** {@inheritDoc} */
@Override protected double computePrefWidth(double height) {
double w = isVertical() ? getPrefBreadth(height) : getPrefLength();
return w + vbar.prefWidth(-1);
}
/** {@inheritDoc} */
@Override protected double computePrefHeight(double width) {
double h = isVertical() ? getPrefLength() : getPrefBreadth(width);
return h + hbar.prefHeight(-1);
}
/**
* Return a cell for the given index. This may be called for any cell,
* including beyond the range defined by cellCount, in which case an
* empty cell will be returned. The returned value should not be stored for
* any reason.
* @param index the index
* @return the cell
*/
public T getCell(int index) {
// If there are cells, then we will attempt to get an existing cell
if (! cells.isEmpty()) {
// First check the cells that have already been created and are
// in use. If this call returns a value, then we can use it
T cell = getVisibleCell(index);
if (cell != null) return cell;
}
// check the pile
for (int i = 0; i < pile.size(); i++) {
T cell = pile.get(i);
if (getCellIndex(cell) == index) {
// Note that we don't remove from the pile: if we do it leads
// to a severe performance decrease. This seems to be OK, as
// getCell() is only used for cell measurement purposes.
// pile.remove(i);
resizeCell(cell);
return cell;
}
}
// We need to use the accumCell and return that
if (accumCell == null) {
Callback,T> cellFactory = getCellFactory();
if (cellFactory != null) {
accumCell = cellFactory.call(this);
accumCell.getProperties().put(NEW_CELL, null);
accumCellParent.getChildren().setAll(accumCell);
// Note the screen reader will attempt to find all
// the items inside the view to calculate the item count.
// Having items under different parents (sheet and accumCellParent)
// leads the screen reader to compute wrong values.
// The regular scheme to provide items to the screen reader
// uses getPrivateCell(), which places the item in the sheet.
// The accumCell, and its children, should be ignored by the
// screen reader.
accumCell.setAccessibleRole(AccessibleRole.NODE);
accumCell.getChildrenUnmodifiable().addListener((Observable c) -> {
for (Node n : accumCell.getChildrenUnmodifiable()) {
n.setAccessibleRole(AccessibleRole.NODE);
}
});
}
}
setCellIndex(accumCell, index);
resizeCell(accumCell);
return accumCell;
}
/**
* The VirtualFlow uses this method to set a cells index (rather than calling
* {@link IndexedCell#updateIndex(int)} directly), so it is a perfect place
* for subclasses to override if this if of interest.
*
* @param cell The cell whose index will be updated.
* @param index The new index for the cell.
*/
protected void setCellIndex(T cell, int index) {
assert cell != null;
cell.updateIndex(index);
// make sure the cell is sized correctly. This is important for both
// general layout of cells in a VirtualFlow, but also in cases such as
// RT-34333, where the sizes were being reported incorrectly to the
// ComboBox popup.
if ((cell.isNeedsLayout() && cell.getScene() != null) || cell.getProperties().containsKey(NEW_CELL)) {
cell.applyCss();
cell.getProperties().remove(NEW_CELL);
}
}
/**
* Return the index for a given cell. This allows subclasses to customise
* how cell indices are retrieved.
* @param cell the cell
* @return the index
*/
protected int getCellIndex(T cell){
return cell.getIndex();
}
/* *************************************************************************
* *
* Private implementation *
* *
**************************************************************************/
/**
* Returns the scroll bar used for scrolling horizontally. A developer who needs to be notified when a scroll is
* happening could attach a listener to the {@link ScrollBar#valueProperty()}.
*
* @return the scroll bar used for scrolling horizontally
* @since 12
*/
protected final ScrollBar getHbar() {
return hbar;
}
/**
* Returns the scroll bar used for scrolling vertically. A developer who needs to be notified when a scroll is
* happening could attach a listener to the {@link ScrollBar#valueProperty()}. The {@link ScrollBar#getWidth()} is
* also useful when adding a component over the {@code TableView} in order to clip it so that it doesn't overlap the
* {@code ScrollBar}.
*
* @return the scroll bar used for scrolling vertically
* @since 12
*/
protected final ScrollBar getVbar() {
return vbar;
}
/**
* The maximum preferred size in the non-virtual direction. For example,
* if vertical, then this is the max pref width of all cells encountered.
*
* In general, this is the largest preferred size in the non-virtual
* direction that we have ever encountered. We don't reduce this size
* unless instructed to do so, so as to reduce the amount of scroll bar
* jitter. The access on this variable is package ONLY FOR TESTING.
*/
private double maxPrefBreadth;
private final void setMaxPrefBreadth(double value) {
this.maxPrefBreadth = value;
}
final double getMaxPrefBreadth() {
return maxPrefBreadth;
}
/**
* The breadth of the viewport portion of the VirtualFlow as computed during
* the layout pass. In a vertical flow this would be the same as the clip
* view width. In a horizontal flow this is the clip view height.
* The access on this variable is package ONLY FOR TESTING.
*/
private double viewportBreadth;
private final void setViewportBreadth(double value) {
this.viewportBreadth = value;
}
private final double getViewportBreadth() {
return viewportBreadth;
}
/**
* The length of the viewport portion of the VirtualFlow as computed
* during the layout pass. In a vertical flow this would be the same as the
* clip view height. In a horizontal flow this is the clip view width.
*/
private double viewportLength;
void setViewportLength(double value) {
if (value == this.viewportLength) {
return;
}
this.viewportLength = value;
this.absoluteOffset = getPosition() * (estimatedSize - viewportLength);
recalculateEstimatedSize();
}
/**
* Returns the length of the viewport portion of the {@code VirtualFlow} as computed during the layout pass.
* For a vertical flow this is based on the height and for a horizontal flow on the width of the clip view.
*
* @return the viewport length in pixels
* @since 23
*/
public double getViewportLength() {
return viewportLength;
}
/**
* Compute and return the length of the cell for the given index. This is
* called both internally when adjusting by pixels, and also at times
* by PositionMapper (see the getItemSize callback). When called by
* PositionMapper, it is possible that it will be called for some index
* which is not associated with any cell, so we have to do a bit of work
* to use a cell as a helper for computing cell size in some cases.
*/
double getCellLength(int index) {
if (fixedCellSizeEnabled) return getFixedCellSize();
T cell = getCell(index);
double length = getCellLength(cell);
releaseCell(cell);
return length;
}
/**
*/
double getCellBreadth(int index) {
T cell = getCell(index);
double b = getCellBreadth(cell);
releaseCell(cell);
return b;
}
/**
* Gets the length of a specific cell
*/
double getCellLength(T cell) {
if (cell == null) return 0;
if (fixedCellSizeEnabled) return getFixedCellSize();
return isVertical() ?
cell.getLayoutBounds().getHeight()
: cell.getLayoutBounds().getWidth();
}
/**
* Gets the breadth of a specific cell
*/
double getCellBreadth(T cell) {
return isVertical() ?
cell.prefWidth(-1)
: cell.prefHeight(-1);
}
/**
* Gets the layout position of the cell along the length axis
*/
double getCellPosition(T cell) {
if (cell == null) return 0;
return isVertical() ?
cell.getLayoutY()
: cell.getLayoutX();
}
private void positionCell(T cell, double position) {
updateCellSize(cell);
if (isVertical()) {
cell.setLayoutX(0);
cell.setLayoutY(snapSpaceY(position));
} else {
cell.setLayoutX(snapSpaceX(position));
cell.setLayoutY(0);
}
}
/**
* Resizes the given cell. If {@link #isVertical()} is set to {@code true}, the cell width will be the maximum
* between the viewport width and the sum of all the cells' {@code prefWidth}. The cell height will be computed by
* the cell itself unless {@code fixedCellSizeEnabled} is set to {@code true}, then {@link #getFixedCellSize()} is
* used. If {@link #isVertical()} is set to {@code false}, the width and height calculations are reversed.
*
* @param cell the cell to resize
* @since 12
*/
protected void resizeCell(T cell) {
if (cell == null) return;
double size = Math.max(getMaxPrefBreadth(), getViewportBreadth());
if (isVertical()) {
cell.resize(size, fixedCellSizeEnabled ? getFixedCellSize() : Utils.boundedSize(cell.prefHeight(size), cell.minHeight(size), cell.maxHeight(size)));
} else {
cell.resize(fixedCellSizeEnabled ? getFixedCellSize() : Utils.boundedSize(cell.prefWidth(size), cell.minWidth(size), cell.maxWidth(size)), size);
}
}
/**
* Returns the list of cells displayed in the current viewport.
*
* The cells are ordered such that the first cell in this list is the first in the view, and the last cell is the
* last in the view. When pixel scrolling, the list is simply shifted and items drop off the beginning or the end,
* depending on the order of scrolling.
*
* @return the cells displayed in the current viewport
* @since 12
*/
protected List getCells() {
return cells;
}
/**
* Returns the last visible cell whose bounds are entirely within the viewport. When manually inserting rows, one
* may need to know which cell indices are visible in the viewport.
*
* @return last visible cell whose bounds are entirely within the viewport
* @since 12
*/
protected T getLastVisibleCellWithinViewport() {
if (cells.isEmpty() || getViewportLength() <= 0) return null;
T cell;
final double max = getViewportLength();
for (int i = cells.size() - 1; i >= 0; i--) {
cell = cells.get(i);
if (cell.isEmpty()) continue;
final double cellStart = getCellPosition(cell);
final double cellEnd = cellStart + getCellLength(cell);
// we use the magic +2 to allow for a little bit of fuzziness,
// this is to help in situations such as RT-34407
if (cellEnd <= (max + 2)) {
return cell;
}
}
return null;
}
/**
* Returns the first visible cell whose bounds are entirely within the viewport. When manually inserting rows, one
* may need to know which cell indices are visible in the viewport.
*
* @return first visible cell whose bounds are entirely within the viewport
* @since 12
*/
protected T getFirstVisibleCellWithinViewport() {
if (cells.isEmpty() || getViewportLength() <= 0) return null;
T cell;
for (int i = 0; i < cells.size(); i++) {
cell = cells.get(i);
if (cell.isEmpty()) continue;
final double cellStart = getCellPosition(cell);
if (cellStart >= 0) {
return cell;
}
}
return null;
}
/**
* Adds all the cells prior to and including the given currentIndex, until
* no more can be added without falling off the flow. The startOffset
* indicates the distance from the leading edge (top) of the viewport to
* the leading edge (top) of the currentIndex.
*/
void addLeadingCells(int currentIndex, double startOffset) {
// The offset will keep track of the distance from the top of the
// viewport to the top of the current index. We will decrement it
// as we lay out leading cells.
double offset = startOffset;
// The index is the absolute index of the cell being laid out
int index = currentIndex;
// Offset should really be the bottom of the current index
boolean first = true; // first time in, we just fudge the offset and let
// it be the top of the current index then redefine
// it as the bottom of the current index thereafter
// while we have not yet laid out so many cells that they would fall
// off the flow, we will continue to create and add cells. The
// offset is our indication of whether we can lay out additional
// cells. If the offset is ever < 0, except in the case of the very
// first cell, then we must quit.
T cell = null;
// special case for the position == 1.0, skip adding last invisible cell
if (index == getCellCount() && offset == getViewportLength()) {
index--;
first = false;
}
while (index >= 0 && (offset > 0 || first)) {
cell = getAvailableCell(index);
setCellIndex(cell, index);
resizeCell(cell); // resize must be after config
cells.addFirst(cell);
// A little gross but better than alternatives because it reduces
// the number of times we have to update a cell or compute its
// size. The first time into this loop "offset" is actually the
// top of the current index. On all subsequent visits, it is the
// bottom of the current index.
if (first) {
first = false;
} else {
offset -= getCellLength(cell);
}
// Position the cell, and update the maxPrefBreadth variable as we go.
positionCell(cell, offset);
setMaxPrefBreadth(Math.max(getMaxPrefBreadth(), getCellBreadth(cell)));
cell.setVisible(true);
--index;
}
// There are times when after laying out the cells we discover that
// the top of the first cell which represents index 0 is below the top
// of the viewport. In these cases, we have to adjust the cells up
// and reset the mapper position. This might happen when items got
// removed at the top or when the viewport size increased.
if (cells.size() > 0) {
cell = cells.getFirst();
int firstIndex = getCellIndex(cell);
double firstCellPos = getCellPosition(cell);
if (firstIndex == 0 && firstCellPos > 0) {
setPosition(0.0f);
offset = 0;
for (int i = 0; i < cells.size(); i++) {
cell = cells.get(i);
positionCell(cell, offset);
offset += getCellLength(cell);
}
}
} else {
// reset scrollbar to top, so if the flow sees cells again it starts at the top
vbar.setValue(0);
hbar.setValue(0);
}
}
/**
* Adds all the trailing cells that come after the last index in
* the cells ObservableList.
*/
boolean addTrailingCells(boolean fillEmptyCells) {
// If cells is empty then addLeadingCells bailed for some reason and
// we're hosed, so just punt
if (cells.isEmpty()) return false;
// While we have not yet laid out so many cells that they would fall
// off the flow, so we will continue to create and add cells. When the
// offset becomes greater than the width/height of the flow, then we
// know we cannot add any more cells.
T startCell = cells.getLast();
double offset = getCellPosition(startCell) + getCellLength(startCell);
int index = getCellIndex(startCell) + 1;
final int cellCount = getCellCount();
boolean filledWithNonEmpty = index <= cellCount;
final double viewportLength = getViewportLength();
// Fix for RT-37421, which was a regression caused by RT-36556
if (offset < 0 && !fillEmptyCells) {
return false;
}
//
// RT-36507: viewportLength gives the maximum number of
// additional cells that should ever be able to fit in the viewport if
// every cell had a height of 1. If index ever exceeds this count,
// then offset is not incrementing fast enough, or at all, which means
// there is something wrong with the cell size calculation.
//
final double maxCellCount = viewportLength;
while (offset < viewportLength) {
if (index >= cellCount) {
if (offset < viewportLength) filledWithNonEmpty = false;
if (! fillEmptyCells) return filledWithNonEmpty;
// RT-36507 - return if we've exceeded the maximum
if (index > maxCellCount) {
final PlatformLogger logger = Logging.getControlsLogger();
if (logger.isLoggable(PlatformLogger.Level.INFO)) {
logger.info("index exceeds maxCellCount. Check size calculations for " + startCell.getClass());
}
return filledWithNonEmpty;
}
}
T cell = getAvailableCell(index);
setCellIndex(cell, index);
resizeCell(cell); // resize happens after config!
cells.addLast(cell);
// Position the cell and update the max pref
positionCell(cell, offset);
setMaxPrefBreadth(Math.max(getMaxPrefBreadth(), getCellBreadth(cell)));
offset += getCellLength(cell);
cell.setVisible(true);
++index;
}
// Discover whether the first cell coincides with index #0. If after
// adding all the trailing cells we find that a) the first cell was
// not index #0 and b) there are trailing cells, then we have a
// problem. We need to shift all the cells down and add leading cells,
// one at a time, until either the very last non-empty cells is aligned
// with the bottom OR we have laid out cell index #0 at the first
// position.
T firstCell = cells.getFirst();
index = getCellIndex(firstCell);
T lastNonEmptyCell = getLastVisibleCell();
double start = getCellPosition(firstCell);
double end = getCellPosition(lastNonEmptyCell) + getCellLength(lastNonEmptyCell);
if ((index != 0 || (index == 0 && start < 0)) && fillEmptyCells &&
lastNonEmptyCell != null && getCellIndex(lastNonEmptyCell) == cellCount - 1 && end < viewportLength) {
double prospectiveEnd = end;
double distance = viewportLength - end;
while (prospectiveEnd < viewportLength && index != 0 && (-start) < distance) {
index--;
T cell = getAvailableCell(index);
setCellIndex(cell, index);
resizeCell(cell); // resize must be after config
cells.addFirst(cell);
double cellLength = getCellLength(cell);
start -= cellLength;
prospectiveEnd += cellLength;
positionCell(cell, start);
setMaxPrefBreadth(Math.max(getMaxPrefBreadth(), getCellBreadth(cell)));
cell.setVisible(true);
}
// The amount by which to translate the cells down
firstCell = cells.getFirst();
start = getCellPosition(firstCell);
double delta = viewportLength - end;
if (getCellIndex(firstCell) == 0 && delta > (-start)) {
delta = (-start);
}
// Move things
for (int i = 0; i < cells.size(); i++) {
T cell = cells.get(i);
positionCell(cell, getCellPosition(cell) + delta);
}
// Check whether the first cell, subsequent to our adjustments, is
// now index #0 and aligned with the top. If so, change the position
// to be at 0 instead of 1.
start = getCellPosition(firstCell);
if (getCellIndex(firstCell) == 0 && start == 0) {
setPosition(0);
} else if (getPosition() != 1) {
setPosition(1);
}
}
return filledWithNonEmpty;
}
/**
* Informs the {@code VirtualFlow} that a layout pass should be done, and the cell contents have not changed. For
* example, this might be called from a {@code TableView} or {@code ListView} when a layout is needed and no cells
* have been added or removed.
*
* @since 12
*/
protected void reconfigureCells() {
needsReconfigureCells = true;
requestLayout();
}
/**
* Informs the {@code VirtualFlow} that a layout pass should be done, and that the cell factory has changed. All
* cells in the viewport are recreated with the new cell factory.
*
* @since 12
*/
protected void recreateCells() {
needsRecreateCells = true;
requestLayout();
}
/**
* Informs the {@code VirtualFlow} that a layout pass should be done, and cell contents have changed. All cells are
* removed and then added properly in the viewport.
*
* @since 12
*/
protected void rebuildCells() {
needsRebuildCells = true;
requestLayout();
}
/**
* Informs the {@code VirtualFlow} that a layout pass should be done and only the cell layout will be requested.
*
* @since 12
*/
protected void requestCellLayout() {
needsCellsLayout = true;
requestLayout();
}
void setCellDirty(int index) {
dirtyCells.set(index);
requestLayout();
}
/**
* Make sure the sizes of the cells that are likely to be visible are known.
* When updates to the cell size estimates are occurring, we don't want the current
* visible content to be modified. The existing offset and index are respected.
* @param index the index of the cell that should be positioned at the top of
* the viewport in the next layout cycle.
*/
void getCellSizesInExpectedViewport(int index) {
double oldOffset = computeViewportOffset(getPosition());
int oldIndex = computeCurrentIndex();
double cellLength = getOrCreateCellSize(index);
if (index > 0) {
getOrCreateCellSize(index - 1);
}
if (index < getCellCount() - 1) {
getOrCreateCellSize(index + 1);
}
double estlength = cellLength;
int i = index;
while ((estlength < viewportLength) && (++i < getCellCount())) {
estlength = estlength + getOrCreateCellSize(i);
}
estlength = cellLength;
if (estlength < viewportLength) {
int j = index;
while ((estlength < viewportLength) && (j-- > 0)) {
estlength = estlength + getOrCreateCellSize(j);
}
}
recalculateAndImproveEstimatedSize(0, oldIndex, oldOffset);
}
private void startSBReleasedAnimation() {
if (sbTouchTimeline == null) {
/*
** timeline to leave the scrollbars visible for a short
** while after a scroll/drag
*/
sbTouchTimeline = new Timeline();
sbTouchKF1 = new KeyFrame(Duration.millis(0), event -> {
tempVisibility = true;
requestLayout();
});
sbTouchKF2 = new KeyFrame(Duration.millis(1000), event -> {
if (touchDetected == false && mouseDown == false) {
tempVisibility = false;
requestLayout();
}
});
sbTouchTimeline.getKeyFrames().addAll(sbTouchKF1, sbTouchKF2);
}
sbTouchTimeline.playFromStart();
}
private void scrollBarOn() {
tempVisibility = true;
requestLayout();
}
void updateHbar() {
if (! isVisible() || getScene() == null) return;
// Bring the clipView.clipX back to 0 if control is vertical or
// the hbar isn't visible (fix for RT-11666)
if (isVertical()) {
if (needBreadthBar) {
clipView.setClipX(hbar.getValue());
} else {
// all cells are now less than the width of the flow,
// so we should shift the hbar/clip such that
// everything is visible in the viewport.
clipView.setClipX(0);
hbar.setValue(0);
}
}
}
/**
* Suppresses the breadth bar from appearing.
*/
void setSuppressBreadthBar(boolean suppress) {
this.suppressBreadthBar = suppress;
}
/**
* @return true if bar visibility changed
*/
private boolean computeBarVisiblity() {
if (cells.isEmpty()) {
// In case no cells are set yet, we assume no bars are needed
needLengthBar = false;
needBreadthBar = false;
return true;
}
final boolean isVertical = isVertical();
boolean barVisibilityChanged = false;
VirtualScrollBar breadthBar = isVertical ? hbar : vbar;
VirtualScrollBar lengthBar = isVertical ? vbar : hbar;
final double viewportBreadth = getViewportBreadth();
final int cellsSize = cells.size();
final int cellCount = getCellCount();
for (int i = 0; i < 2; i++) {
final boolean lengthBarVisible = getPosition() > 0
|| cellCount > cellsSize
|| (cellCount == cellsSize && (getCellPosition(cells.getLast()) + getCellLength(cells.getLast())) > getViewportLength())
|| (cellCount == cellsSize - 1 && barVisibilityChanged && needBreadthBar);
if (lengthBarVisible ^ needLengthBar) {
needLengthBar = lengthBarVisible;
barVisibilityChanged = true;
}
final boolean breadthBarVisible = !suppressBreadthBar && (maxPrefBreadth > viewportBreadth);
if (breadthBarVisible ^ needBreadthBar) {
needBreadthBar = breadthBarVisible;
barVisibilityChanged = true;
}
}
// Start by optimistically deciding whether the length bar and
// breadth bar are needed and adjust the viewport dimensions
// accordingly. If during layout we find that one or the other of the
// bars actually is needed, then we will perform a cleanup pass
if (!Properties.IS_TOUCH_SUPPORTED) {
updateViewportDimensions();
breadthBar.setVisible(needBreadthBar);
lengthBar.setVisible(needLengthBar);
} else {
breadthBar.setVisible(needBreadthBar && tempVisibility);
lengthBar.setVisible(needLengthBar && tempVisibility);
}
return barVisibilityChanged;
}
private void updateViewportDimensions() {
final boolean isVertical = isVertical();
final double breadthBarLength = isVertical ? snapSizeY(hbar.prefHeight(-1)) : snapSizeX(vbar.prefWidth(-1));
final double lengthBarBreadth = isVertical ? snapSizeX(vbar.prefWidth(-1)) : snapSizeY(hbar.prefHeight(-1));
if (!Properties.IS_TOUCH_SUPPORTED) {
setViewportBreadth((isVertical ? getWidth() : getHeight()) - (needLengthBar ? lengthBarBreadth : 0));
setViewportLength((isVertical ? getHeight() : getWidth()) - (needBreadthBar ? breadthBarLength : 0));
} else {
setViewportBreadth((isVertical ? getWidth() : getHeight()));
setViewportLength((isVertical ? getHeight() : getWidth()));
}
}
private void initViewport() {
// Initialize the viewportLength and viewportBreadth to match the
// width/height of the flow
final boolean isVertical = isVertical();
updateViewportDimensions();
VirtualScrollBar breadthBar = isVertical ? hbar : vbar;
VirtualScrollBar lengthBar = isVertical ? vbar : hbar;
// If there has been a switch between the virtualized bar, then we
// will want to do some stuff TODO.
breadthBar.setVirtual(false);
lengthBar.setVirtual(true);
}
/**
* In case we are not rendering the first cell
* AND
* there is empty room after the last cell,
* the cells need to be shifted down to fill the empty area.
*/
private void shiftDown() {
T lastNonEmptyCell = getLastVisibleCell();
T firstCell = cells.getFirst();
int index = getCellIndex(firstCell);
double end = getCellPosition(lastNonEmptyCell) + getCellLength(lastNonEmptyCell);
double delta = viewportLength - end;
if ((index > 0) && (delta > 0)) {
for (int i = 0; i < cells.size(); i++) {
T cell = cells.get(i);
positionCell(cell, getCellPosition(cell) + delta);
}
}
}
private void updateScrollBarsAndCells(boolean recreate) {
// Assign the hbar and vbar to the breadthBar and lengthBar so as
// to make some subsequent calculations easier.
final boolean isVertical = isVertical();
VirtualScrollBar breadthBar = isVertical ? hbar : vbar;
VirtualScrollBar lengthBar = isVertical ? vbar : hbar;
// We may have adjusted the viewport length and breadth after the
// layout due to scroll bars becoming visible. So we need to perform
// a follow up pass and resize and shift all the cells to fit the
// viewport. Note that the prospective viewport size is always >= the
// final viewport size, so we don't have to worry about adding
// cells during this cleanup phase.
fitCells();
// Update cell positions.
// When rebuilding the cells, we add the cells and along the way compute
// the maxPrefBreadth. Based on the computed value, we may add
// the breadth scrollbar which changes viewport length, so we need
// to re-position the cells.
if (!cells.isEmpty()) {
final double currOffset = -computeViewportOffset(getPosition());
final int currIndex = computeCurrentIndex() - cells.getFirst().getIndex();
final int size = cells.size();
// position leading cells
double offset = currOffset;
for (int i = currIndex - 1; i >= 0 && i < size; i--) {
final T cell = cells.get(i);
offset -= getCellLength(cell);
positionCell(cell, offset);
}
// position trailing cells
offset = currOffset;
for (int i = currIndex; i >= 0 && i < size; i++) {
final T cell = cells.get(i);
positionCell(cell, offset);
offset += getCellLength(cell);
}
shiftDown();
}
// Toggle visibility on the corner
corner.setVisible(breadthBar.isVisible() && lengthBar.isVisible());
double sumCellLength = 0;
double flowLength = (isVertical ? getHeight() : getWidth()) -
(breadthBar.isVisible() ? breadthBar.prefHeight(-1) : 0);
final double viewportBreadth = getViewportBreadth();
final double viewportLength = getViewportLength();
// Now position and update the scroll bars
if (breadthBar.isVisible()) {
/*
** Positioning the ScrollBar
*/
if (!Properties.IS_TOUCH_SUPPORTED) {
if (isVertical) {
hbar.resizeRelocate(0, viewportLength,
viewportBreadth, hbar.prefHeight(viewportBreadth));
} else {
vbar.resizeRelocate(viewportLength, 0,
vbar.prefWidth(viewportBreadth), viewportBreadth);
}
}
else {
if (isVertical) {
double prefHeight = hbar.prefHeight(viewportBreadth);
hbar.resizeRelocate(0, viewportLength - prefHeight,
viewportBreadth, prefHeight);
} else {
double prefWidth = vbar.prefWidth(viewportBreadth);
vbar.resizeRelocate(viewportLength - prefWidth, 0,
prefWidth, viewportBreadth);
}
}
if (getMaxPrefBreadth() != -1) {
double newMax = Math.max(1, getMaxPrefBreadth() - viewportBreadth);
if (newMax != breadthBar.getMax()) {
breadthBar.setMax(newMax);
double breadthBarValue = breadthBar.getValue();
boolean maxed = breadthBarValue != 0 && newMax == breadthBarValue;
if (maxed || breadthBarValue > newMax) {
breadthBar.setValue(newMax);
}
breadthBar.setVisibleAmount((viewportBreadth / getMaxPrefBreadth()) * newMax);
}
}
}
// determine how many cells there are on screen so that the scrollbar
// thumb can be appropriately sized
if (recreate && (lengthBar.isVisible() || Properties.IS_TOUCH_SUPPORTED)) {
final int cellCount = getCellCount();
int numCellsVisibleOnScreen = 0;
for (int i = 0, max = cells.size(); i < max; i++) {
T cell = cells.get(i);
if (cell != null && !cell.isEmpty()) {
sumCellLength += (isVertical ? cell.getHeight() : cell.getWidth());
if (sumCellLength > flowLength) {
break;
}
numCellsVisibleOnScreen++;
}
}
lengthBar.setMax(1);
if (numCellsVisibleOnScreen == 0 && cellCount == 1) {
// special case to help resolve RT-17701 and the case where we have
// only a single row and it is bigger than the viewport
lengthBar.setVisibleAmount(flowLength / sumCellLength);
} else {
lengthBar.setVisibleAmount(viewportLength / estimatedSize);
}
}
if (lengthBar.isVisible()) {
// Fix for RT-11873. If this isn't here, we can have a situation where
// the scrollbar scrolls endlessly. This is possible when the cell
// count grows as the user hits the maximal position on the scrollbar
// (i.e. the list size dynamically grows as the user needs more).
//
// This code was commented out to resolve RT-14477 after testing
// whether RT-11873 can be recreated. It could not, and therefore
// for now this code will remained uncommented until it is deleted
// following further testing.
// if (lengthBar.getValue() == 1.0 && lastCellCount != cellCount) {
// lengthBar.setValue(0.99);
// }
/*
** Positioning the ScrollBar
*/
if (!Properties.IS_TOUCH_SUPPORTED) {
if (isVertical) {
vbar.resizeRelocate(viewportBreadth, 0, vbar.prefWidth(viewportLength), viewportLength);
} else {
hbar.resizeRelocate(0, viewportBreadth, viewportLength, hbar.prefHeight(-1));
}
}
else {
if (isVertical) {
double prefWidth = vbar.prefWidth(viewportLength);
vbar.resizeRelocate(viewportBreadth - prefWidth, 0, prefWidth, viewportLength);
} else {
double prefHeight = hbar.prefHeight(-1);
hbar.resizeRelocate(0, viewportBreadth - prefHeight, viewportLength, prefHeight);
}
}
}
if (corner.isVisible()) {
if (!Properties.IS_TOUCH_SUPPORTED) {
corner.resize(vbar.getWidth(), hbar.getHeight());
corner.relocate(hbar.getLayoutX() + hbar.getWidth(), vbar.getLayoutY() + vbar.getHeight());
}
else {
corner.resize(vbar.getWidth(), hbar.getHeight());
corner.relocate(hbar.getLayoutX() + (hbar.getWidth()-vbar.getWidth()), vbar.getLayoutY() + (vbar.getHeight()-hbar.getHeight()));
hbar.resize(hbar.getWidth()-vbar.getWidth(), hbar.getHeight());
vbar.resize(vbar.getWidth(), vbar.getHeight()-hbar.getHeight());
}
}
clipView.resize(snapSizeX(isVertical ? viewportBreadth : viewportLength),
snapSizeY(isVertical ? viewportLength : viewportBreadth));
// If the viewportLength becomes large enough that all cells fit
// within the viewport, then we want to update the value to match.
if (getPosition() != lengthBar.getValue()) {
lengthBar.setValue(getPosition());
}
}
/**
* Adjusts the cells location and size if necessary. The breadths of all
* cells will be adjusted to fit the viewportWidth or maxPrefBreadth, and
* the layout position will be updated if necessary based on index and
* offset.
*/
private void fitCells() {
// Note: Do not optimise this loop by pre-calculating the cells size and
// storing that into a int value - this can lead to RT-32828
for (int i = 0; i < cells.size(); i++) {
T cell = cells.get(i);
resizeCell(cell);
}
}
private void cull() {
final double viewportLength = getViewportLength();
for (int i = cells.size() - 1; i >= 0; i--) {
T cell = cells.get(i);
double cellSize = getCellLength(cell);
double cellStart = getCellPosition(cell);
double cellEnd = cellStart + cellSize;
if (cellStart >= viewportLength || cellEnd < 0) {
addToPile(cells.remove(i));
}
}
}
/**
* After using the accum cell, it needs to be released!
*/
private void releaseCell(T cell) {
if (accumCell != null && cell == accumCell) {
accumCell.setVisible(false);
accumCell.updateIndex(-1);
}
}
/**
* Creates and returns a new cell for the given index.
*
* If the requested index is not already an existing visible cell, it will create a cell for the given index and
* insert it into the {@code VirtualFlow} container. If the index exists, simply returns the visible cell. From that
* point on, it will be unmanaged, and is up to the caller of this method to manage it.
*
* This is useful if a row that should not be visible must be accessed (a row that always stick to the top for
* example). It can then be easily created, correctly initialized and inserted in the {@code VirtualFlow}
* container.
*
* @param index the cell index
* @return a cell for the given index inserted in the VirtualFlow container
* @since 12
*/
protected T getPrivateCell(int index) {
T cell = null;
// If there are cells, then we will attempt to get an existing cell
if (! cells.isEmpty()) {
// First check the cells that have already been created and are
// in use. If this call returns a value, then we can use it
cell = getVisibleCell(index);
if (cell != null) {
// Force the underlying text inside the cell to be updated
// so that when the screen reader runs, it will match the
// text in the cell (force updateDisplayedText())
cell.layout();
return cell;
}
}
// check the existing sheet children
if (cell == null) {
for (int i = 0; i < sheetChildren.size(); i++) {
T _cell = (T) sheetChildren.get(i);
if (getCellIndex(_cell) == index) {
return _cell;
}
}
}
Callback, T> cellFactory = getCellFactory();
if (cellFactory != null) {
cell = cellFactory.call(this);
}
if (cell != null) {
setCellIndex(cell, index);
resizeCell(cell);
cell.setVisible(false);
sheetChildren.add(cell);
privateCells.add(cell);
}
return cell;
}
private final List privateCells = new ArrayList<>();
private void releaseAllPrivateCells() {
sheetChildren.removeAll(privateCells);
privateCells.clear();
}
/**
* Puts the given cell onto the pile. This is called whenever a cell has
* fallen off the flow's start.
*/
private void addToPile(T cell) {
assert cell != null;
pile.addLast(cell);
}
private void cleanPile() {
boolean wasFocusOwner = false;
for (int i = 0, max = pile.size(); i < max; i++) {
T cell = pile.get(i);
wasFocusOwner = wasFocusOwner || doesCellContainFocus(cell);
cell.setVisible(false);
}
// Fix for RT-35876: Rather than have the cells do weird things with
// focus (in particular, have focus jump between cells), we return focus
// to the VirtualFlow itself.
if (wasFocusOwner) {
requestFocus();
}
}
private boolean doesCellContainFocus(T c) {
Scene scene = c.getScene();
final Node focusOwner = scene == null ? null : scene.getFocusOwner();
if (focusOwner != null) {
if (c.equals(focusOwner)) {
return true;
}
Parent p = focusOwner.getParent();
while (p != null && ! (p instanceof VirtualFlow)) {
if (c.equals(p)) {
return true;
}
p = p.getParent();
}
}
return false;
}
private double getPrefBreadth(double oppDimension) {
double max = getMaxCellWidth(10);
// This primarily exists for the case where we do not want the breadth
// to grow to ensure a golden ratio between width and height (for example,
// when a ListView is used in a ComboBox - the width should not grow
// just because items are being added to the ListView)
if (oppDimension > -1) {
double prefLength = getPrefLength();
max = Math.max(max, prefLength * GOLDEN_RATIO_MULTIPLIER);
}
return max;
}
private double getPrefLength() {
double sum = 0.0;
int rows = Math.min(10, getCellCount());
for (int i = 0; i < rows; i++) {
sum += getCellLength(i);
}
return sum;
}
double getMaxCellWidth(int rowsToCount) {
double max = 0.0;
// we always measure at least one row
int rows = Math.max(1, rowsToCount == -1 ? getCellCount() : rowsToCount);
for (int i = 0; i < rows; i++) {
max = Math.max(max, getCellBreadth(i));
}
return max;
}
// Old PositionMapper
/**
* Given a position value between 0 and 1, compute and return the viewport
* offset from the "current" cell associated with that position value.
* That is, if the return value of this function where used as a translation
* factor for a sheet that contained all the items, then the current
* item would end up positioned correctly.
* We calculate the total size until the absoluteoffset is reached.
* For this calculation, we use the cached sizes for each item, or an
* educated guess in case we don't have a cached size yet. While we could
* fill the cache with the size here, we do not do it as it will affect
* performance.
*/
private double computeViewportOffset(double position) {
return computeViewportOffset(position, getCellCount());
}
private double computeViewportOffset(double position, int localCellCount) {
double p = com.sun.javafx.util.Utils.clamp(0, position, 1);
double bound = 0d;
double estSize = estimatedSize / localCellCount;
double maxOff = estimatedSize - getViewportLength();
if ((maxOff > 0) && (absoluteOffset > maxOff)) {
return maxOff - absoluteOffset;
}
for (int i = 0; i < localCellCount; i++) {
double h = getCellSize(i);
if (h < 0) h = estSize;
if (bound + h > absoluteOffset) {
return absoluteOffset - bound;
}
bound += h;
}
return 0d;
}
private void adjustPositionToIndex(int index) {
if (index > 0) getOrCreateCellSize(index-1);
getOrCreateCellSize(index);
recalculateEstimatedSize();
int cellCount = getCellCount();
if (cellCount <= 0) {
setPosition(0.0f);
} else {
double targetOffset = 0;
double estSize = estimatedSize/cellCount;
for (int i = 0; i < index; i++) {
double cz = getCellSize(i);
if (cz < 0) cz = estSize;
targetOffset = targetOffset+ cz;
}
this.absoluteOffset = (estimatedSize < viewportLength) ? 0 : targetOffset;
adjustPosition();
}
}
/**
* Adjust the position based on a delta of pixels. If negative, then the
* position will be adjusted negatively. If positive, then the position will
* be adjusted positively. If the pixel amount is too great for the range of
* the position, then it will be clamped such that position is always
* strictly between 0 and 1
* @return the actual number of pixels that have been applied
*/
private double adjustByPixelAmount(double numPixels) {
if (numPixels == 0) return 0;
// When we're at the top already, we can't move back further, unless we
// want to allow for gravity-alike effects.
if ((absoluteOffset <= 0) && (numPixels < 0)) return 0;
// start with applying the requested modification
double origAbsoluteOffset = this.absoluteOffset;
this.absoluteOffset = Math.max(0.d, this.absoluteOffset + numPixels);
double newPosition = Math.min(1.0d, absoluteOffset / (estimatedSize - viewportLength));
// estimatedSize changes may result in opposite effect on position
// in that case, modify current position 1% in the requested direction
if ((numPixels > 0) && (newPosition < getPosition())) {
newPosition = getPosition()*1.01;
}
if ((numPixels < 0) && (newPosition > getPosition())) {
newPosition = getPosition()*.99;
}
// once at 95% of the total estimated size, we want a correct size, not
// an estimated size anymore.
if (newPosition > .95) {
int cci = computeCurrentIndex();
while (cci < getCellCount()) {
getOrCreateCellSize(cci); cci++;
}
recalculateEstimatedSize();
}
// if we are at or beyond the edge, correct the absoluteOffset
if (newPosition >= 1.d) {
absoluteOffset = estimatedSize - viewportLength;
}
setPosition(newPosition);
return absoluteOffset - origAbsoluteOffset;
}
private double computeBaseOffset(int index) {
double baseOffset = 0d;
int currentCellCount = getCellCount();
double estSize = estimatedSize / currentCellCount;
for (int i = 0; i < index; i++) {
double nextSize = getCellSize(i);
if (nextSize < 0) nextSize = estSize;
baseOffset += nextSize;
}
return baseOffset;
}
/**
* Compute the index of the first visible cell
* This has package access ONLY FOR TESTING.
*/
int computeCurrentIndex() {
return computeCurrentIndex(getCellCount());
}
private int computeCurrentIndex(int currentCellCount) {
double total = 0;
double estSize = estimatedSize / currentCellCount;
for (int i = 0; i < currentCellCount; i++) {
double nextSize = getCellSize(i);
if (nextSize < 0) nextSize = estSize;
total = total + nextSize;
if (total > absoluteOffset) {
return i;
}
}
return currentCellCount == 0 ? 0 : currentCellCount - 1;
}
/**
* Given an item index, this function will compute and return the viewport
* offset from the beginning of the specified item. Notice that because each
* item has the same percentage of the position dedicated to it, and since
* we are measuring from the start of each item, this is a very simple
* calculation.
*/
private double computeOffsetForCell(int itemIndex) {
double cellCount = getCellCount();
double p = com.sun.javafx.util.Utils.clamp(0, itemIndex, cellCount) / cellCount;
return -(getViewportLength() * p);
}
double getCellSize(int idx) {
return getOrCreateCellSize(idx, false);
}
/**
* Get the size of the considered element.
* If the requested element has a size that is not yet in the cache,
* it will be computed and cached now.
* @return the size of the element; or 1 in case there are no cells yet
*/
double getOrCreateCellSize(int idx) {
return getOrCreateCellSize (idx, true);
}
private double getOrCreateCellSize (int idx, boolean create) {
if (idx < 0) return -1;
// is the current cache long enough to contain idx?
if (itemSizeCache.size() > idx) {
// is there a non-null value stored in the cache?
if (itemSizeCache.get(idx) != null) {
return itemSizeCache.get(idx);
}
}
if (!create) return -1;
boolean doRelease = false;
// Make sure we have enough space in the cache to store this index
while (idx >= itemSizeCache.size()) {
itemSizeCache.add(itemSizeCache.size(), null);
}
double answer = 1d;
if (getFixedCellSize() > 0) {
answer = getFixedCellSize();
itemSizeCache.set(idx, answer);
} else {
// Do we have a visible cell for this index?
T cell = getVisibleCell(idx);
if (cell == null) { // we might get the accumcell here
cell = getCell(idx);
doRelease = true;
}
answer = getCellLength(cell);
itemSizeCache.set(idx, answer);
if (doRelease) { // we need to release the accumcell
releaseCell(cell);
}
}
return answer;
}
/**
* Update the size of a specific cell.
* If this cell was already in the cache, its old value is replaced by the
* new size. The total size of the flow will be recalculated, respecting the
* current index and offset.
* If the specific cell is the "current" cell (which is the first cell that is
* at least partially visible), the offset used for the viewport needs to be
* recalculated in case the new size is different from the cached size. This way,
* we keep the end of the current cell (and start of the cell at current + 1)
* constant. An exception to this is when the current cell starts at offset 0,
* in which case we keep the (0) offset as is.
* @param cell the cell which size has to be calculated
*/
void updateCellSize(T cell) {
int cellIndex = cell.getIndex();
if (itemSizeCache.size() > cellIndex) {
Double oldSize = itemSizeCache.get(cellIndex);
double newSize = getCellLength(cell);
itemSizeCache.set(cellIndex, newSize);
if ((oldSize != null) && !oldSize.equals(newSize)) {
int currentIndex = computeCurrentIndex();
double oldOffset = computeViewportOffset(getPosition());
if ((cellIndex == currentIndex) && (oldOffset != 0)) {
oldOffset = oldOffset + newSize - oldSize;
}
recalculateAndImproveEstimatedSize(0, currentIndex, oldOffset);
}
}
}
/**
* Recalculate the estimated size for this list based on what we have in the
* cache.
*/
private void recalculateEstimatedSize() {
recalculateAndImproveEstimatedSize(DEFAULT_IMPROVEMENT);
}
private boolean recalculating = false;
private void recalculateAndImproveEstimatedSize(int improve) {
recalculateAndImproveEstimatedSize(improve, -1, computeViewportOffset(getPosition()));
}
/**
* Recalculate the estimated size. If an oldIndex different from -1 is supplied, that value will
* be respected:
* at the end of this calculation, we make sure that if the current index is calculated, it will
* be the same as the old index. If the oldIndex is -1, there is no guarantee about the new index.
*/
private void recalculateAndImproveEstimatedSize(int improve, int oldIndex, double oldOffset) {
if (recalculating) return;
recalculating = true;
try {
int itemCount = getCellCount();
int cacheCount = itemSizeCache.size();
boolean keepRatio = ((cacheCount > 0) && !Double.isInfinite(this.absoluteOffset));
if (oldIndex < 0) oldIndex = computeCurrentIndex();
int added = 0;
while ((itemCount > itemSizeCache.size()) && (added < improve)) {
getOrCreateCellSize(itemSizeCache.size());
added++;
}
cacheCount = itemSizeCache.size();
int cnt = 0;
double tot = 0d;
for (int i = 0; (i < itemCount && i < cacheCount); i++) {
Double il = itemSizeCache.get(i);
if (il != null) {
tot = tot + il;
cnt++;
}
}
this.estimatedSize = cnt == 0 ? 1d : tot * itemCount / cnt;
double estSize = estimatedSize / itemCount;
if (keepRatio) {
double newOffset = 0;
for (int i = 0; i < oldIndex; i++) {
double h = getCellSize(i);
if (h < 0) {
h = estSize;
}
newOffset += h;
}
this.absoluteOffset = newOffset + oldOffset;
adjustPosition();
}
} finally {
recalculating = false;
}
}
private void resetSizeEstimates() {
itemSizeCache.clear();
this.estimatedSize = 1d;
}
// /**
// * Adjust the position based on a chunk of pixels. The position is based
// * on the start of the scrollbar position.
// */
// private void adjustByPixelChunk(double numPixels) {
// setPosition(0);
// adjustByPixelAmount(numPixels);
// }
// end of old PositionMapper code
/* *************************************************************************
* *
* Support classes *
* *
**************************************************************************/
/**
* A simple extension to Region that ensures that anything wanting to flow
* outside of the bounds of the Region is clipped.
*/
static class ClippedContainer extends Region {
/**
* The Node which is embedded within this {@code ClipView}.
*/
private Node node;
public Node getNode() { return this.node; }
public void setNode(Node n) {
this.node = n;
getChildren().clear();
getChildren().add(node);
}
public void setClipX(double clipX) {
setLayoutX(-clipX);
clipRect.setLayoutX(clipX);
}
public void setClipY(double clipY) {
setLayoutY(-clipY);
clipRect.setLayoutY(clipY);
}
private final Rectangle clipRect;
public ClippedContainer(final VirtualFlow> flow) {
if (flow == null) {
throw new IllegalArgumentException("VirtualFlow can not be null");
}
getStyleClass().add("clipped-container");
// clipping
clipRect = new Rectangle();
clipRect.setSmooth(false);
setClip(clipRect);
// --- clipping
super.widthProperty().addListener(valueModel -> {
clipRect.setWidth(getWidth());
});
super.heightProperty().addListener(valueModel -> {
clipRect.setHeight(getHeight());
});
}
}
/**
* A List-like implementation that is exceedingly efficient for the purposes
* of the VirtualFlow. Typically there is not much variance in the number of
* cells -- it is always some reasonably consistent number. Yet for efficiency
* in code, we like to use a linked list implementation so as to append to
* start or append to end. However, at times when we need to iterate, LinkedList
* is expensive computationally as well as requiring the construction of
* temporary iterators.
*
* This linked list like implementation is done using an array. It begins by
* putting the first item in the center of the allocated array, and then grows
* outward (either towards the first or last of the array depending on whether
* we are inserting at the head or tail). It maintains an index to the start
* and end of the array, so that it can efficiently expose iteration.
*
* This class is package private solely for the sake of testing.
*/
static class ArrayLinkedList extends AbstractList {
/**
* The array list backing this class. We default the size of the array
* list to be fairly large so as not to require resizing during normal
* use, and since that many ArrayLinkedLists won't be created it isn't
* very painful to do so.
*/
private final ArrayList array;
private int firstIndex = -1;
private int lastIndex = -1;
public ArrayLinkedList() {
array = new ArrayList<>(50);
for (int i = 0; i < 50; i++) {
array.add(null);
}
}
@Override
public T getFirst() {
return firstIndex == -1 ? null : array.get(firstIndex);
}
@Override
public T getLast() {
return lastIndex == -1 ? null : array.get(lastIndex);
}
@Override
public void addFirst(T cell) {
// if firstIndex == -1 then that means this is the first item in the
// list and we need to initialize firstIndex and lastIndex
if (firstIndex == -1) {
firstIndex = lastIndex = array.size() / 2;
array.set(firstIndex, cell);
} else if (firstIndex == 0) {
// we're already at the head of the array, so insert at position
// 0 and then increment the lastIndex to compensate
array.add(0, cell);
lastIndex++;
} else {
// we're not yet at the head of the array, so insert at the
// firstIndex - 1 position and decrement first position
array.set(--firstIndex, cell);
}
}
@Override
public void addLast(T cell) {
// if lastIndex == -1 then that means this is the first item in the
// list and we need to initialize the firstIndex and lastIndex
if (firstIndex == -1) {
firstIndex = lastIndex = array.size() / 2;
array.set(lastIndex, cell);
} else if (lastIndex == array.size() - 1) {
// we're at the end of the array so need to "add" so as to force
// the array to be expanded in size
array.add(++lastIndex, cell);
} else {
array.set(++lastIndex, cell);
}
}
@Override
public int size() {
return firstIndex == -1 ? 0 : lastIndex - firstIndex + 1;
}
@Override
public boolean isEmpty() {
return firstIndex == -1;
}
@Override
public T get(int index) {
if (index > (lastIndex - firstIndex) || index < 0) {
// Commented out exception due to RT-29111
// throw new java.lang.ArrayIndexOutOfBoundsException();
return null;
}
return array.get(firstIndex + index);
}
@Override
public void clear() {
for (int i = 0; i < array.size(); i++) {
array.set(i, null);
}
firstIndex = lastIndex = -1;
}
@Override
public T removeFirst() {
if (isEmpty()) return null;
return remove(0);
}
@Override
public T removeLast() {
if (isEmpty()) return null;
return remove(lastIndex - firstIndex);
}
@Override
public T remove(int index) {
if (index > lastIndex - firstIndex || index < 0) {
throw new ArrayIndexOutOfBoundsException();
}
// if the index == 0, then we're removing the first
// item and can simply set it to null in the array and increment
// the firstIndex unless there is only one item, in which case
// we have to also set first & last index to -1.
if (index == 0) {
T cell = array.get(firstIndex);
array.set(firstIndex, null);
if (firstIndex == lastIndex) {
firstIndex = lastIndex = -1;
} else {
firstIndex++;
}
return cell;
} else if (index == lastIndex - firstIndex) {
// if the index == lastIndex - firstIndex, then we're removing the
// last item and can simply set it to null in the array and
// decrement the lastIndex
T cell = array.get(lastIndex);
array.set(lastIndex--, null);
return cell;
} else {
// if the index is somewhere in between, then we have to remove the
// item and decrement the lastIndex
T cell = array.get(firstIndex + index);
array.set(firstIndex + index, null);
for (int i = (firstIndex + index + 1); i <= lastIndex; i++) {
array.set(i - 1, array.get(i));
}
array.set(lastIndex--, null);
return cell;
}
}
}
}