modules/controls/src/main/java/javafx/scene/control/skin/VirtualFlow.java
Print this page
rev 9240 : 8076423: JEP 253: Prepare JavaFX UI Controls & CSS APIs for Modularization
*** 21,43 ****
* 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 com.sun.javafx.scene.control.skin;
import com.sun.javafx.scene.control.Logging;
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.value.ChangeListener;
import javafx.collections.ObservableList;
import javafx.event.EventDispatcher;
import javafx.event.EventHandler;
import javafx.geometry.Orientation;
--- 21,53 ----
* 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.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;
*** 46,74 ****
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 sun.util.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.
*/
public class VirtualFlow<T extends IndexedCell> extends Region {
/**
* 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
--- 56,97 ----
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.ListCell;
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 sun.util.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}.
! *
! * @since 9
*/
public class VirtualFlow<T extends IndexedCell> 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
*** 76,348 ****
* 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;
- private boolean touchDetected = false;
- private boolean mouseDown = false;
-
/**
! * 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.
! */
! /**
! * 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() {
! 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;
! }
!
! /**
! * Indicates whether the VirtualFlow viewport is capable of being panned
! * by the user (either via the mouse or touch events).
! */
! private boolean pannable = true;
! public boolean isPannable() { return pannable; }
! public void setPannable(boolean value) { this.pannable = value; }
!
! /**
! * 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 int cellCount;
! public int getCellCount() { return cellCount; }
! public void setCellCount(int i) {
! int oldCount = cellCount;
! this.cellCount = i;
!
! boolean countChanged = 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(i);
! }
!
! // 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();
!
! // Fix for RT-13965: Without this line of code, the number of items in
! // the sheet would constantly grow, leaking memory for the life of the
! // application. This was especially apparent when the total number of
! // cells changes - regardless of whether it became bigger or smaller.
! sheetChildren.clear();
!
! Parent parent = getParent();
! if (parent != null) parent.requestLayout();
! }
! // 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".
! }
!
! /**
! * The position of the VirtualFlow within its list of cells. This is a value
! * between 0 and 1.
! */
! private double position;
!
! public double getPosition() {
! return position;
! }
!
! public void setPosition(double newPosition) {
! boolean needsUpdate = this.position != newPosition;
! this.position = com.sun.javafx.util.Utils.clamp(0, newPosition, 1);;
! if (needsUpdate) {
! requestLayout();
! }
! }
!
! /**
! * 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 double fixedCellSize = 0;
! private boolean fixedCellSizeEnabled = false;
!
! public void setFixedCellSize(final double value) {
! this.fixedCellSize = value;
! this.fixedCellSizeEnabled = fixedCellSize > 0;
! needsCellsLayout = true;
! layoutChildren();
! }
!
! /**
! * Callback which is invoked whenever the VirtualFlow needs a new
! * IndexedCell. The VirtualFlow attempts to reuse cells whenever possible
! * and only creates the minimal number of cells necessary.
! */
! private Callback<VirtualFlow, T> createCell;
! public Callback<VirtualFlow, T> getCreateCell() { return createCell; }
! public void setCreateCell(Callback<VirtualFlow, T> cc) {
! this.createCell = cc;
! if (createCell != null) {
! accumCell = null;
! setNeedsLayout(true);
! recreateCells();
! if (getParent() != null) getParent().requestLayout();
! }
! }
- /**
- * The maximum preferred size in the non-virtual direction. For example,
- * if vertical, then this is the max pref width of all cells encountered.
- * <p>
- * 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;
- protected final void setMaxPrefBreadth(double value) {
- this.maxPrefBreadth = value;
- }
- protected 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;
- protected final void setViewportBreadth(double value) {
- this.viewportBreadth = value;
- }
- protected 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.
! * The access on this variable is package ONLY FOR TESTING.
! */
! private double viewportLength;
! void setViewportLength(double value) {
! this.viewportLength = value;
! }
! protected double getViewportLength() {
! return viewportLength;
! }
/**
* The width of the VirtualFlow the last time it was laid out. We
* use this information for several fast paths during the layout pass.
*/
--- 99,127 ----
* 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 impl_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;
! /***************************************************************************
! * *
! * 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.
*/
*** 395,407 ****
* off the beginning or the end, depending on the order of scrolling.
* <p>
* This is package private ONLY FOR TESTING
*/
final ArrayLinkedList<T> cells = new ArrayLinkedList<T>();
- protected List<T> getCells() {
- return cells;
- }
/**
* 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.
--- 174,183 ----
*** 436,458 ****
* The scroll bar used for scrolling horizontally. This has package access
* ONLY for testing.
*/
private VirtualScrollBar hbar = new VirtualScrollBar(this);
- protected final VirtualScrollBar getHbar() {
- return hbar;
- }
/**
* The scroll bar used to scrolling vertically. This has package access
* ONLY for testing.
*/
private VirtualScrollBar vbar = new VirtualScrollBar(this);
- protected final VirtualScrollBar getVbar() {
- return vbar;
- }
-
/**
* 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.
*/
--- 212,227 ----
*** 469,489 ****
// used for panning the virtual flow
private double lastX;
private double lastY;
private boolean isPanning = false;
! 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);
--- 238,286 ----
// 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;
!
!
!
! /***************************************************************************
! * *
! * 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);
*** 526,538 ****
/*
** listen for ScrollEvents over the whole of the VirtualFlow
** area, the above dispatcher having removed the ScrollBars
** scroll event handling.
*/
! setOnScroll(new EventHandler<javafx.scene.input.ScrollEvent>() {
@Override public void handle(ScrollEvent event) {
! if (BehaviorSkinBase.IS_TOUCH_SUPPORTED) {
if (touchDetected == false && mouseDown == false ) {
startSBReleasedAnimation();
}
}
/*
--- 323,335 ----
/*
** listen for ScrollEvents over the whole of the VirtualFlow
** area, the above dispatcher having removed the ScrollBars
** scroll event handling.
*/
! setOnScroll(new EventHandler<ScrollEvent>() {
@Override public void handle(ScrollEvent event) {
! if (Properties.IS_TOUCH_SUPPORTED) {
if (touchDetected == false && mouseDown == false ) {
startSBReleasedAnimation();
}
}
/*
*** 545,555 ****
virtualDelta = event.getTextDeltaY() * lastHeight;
break;
case LINES:
double lineSize;
if (fixedCellSizeEnabled) {
! lineSize = fixedCellSize;
} 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();
--- 342,352 ----
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();
*** 584,594 ****
if (virtualDelta != 0.0) {
/*
** only consume it if we use it
*/
! double result = adjustPixels(-virtualDelta);
if (result != 0.0) {
event.consume();
}
}
--- 381,391 ----
if (virtualDelta != 0.0) {
/*
** only consume it if we use it
*/
! double result = scrollPixels(-virtualDelta);
if (result != 0.0) {
event.consume();
}
}
*** 613,623 ****
addEventFilter(MouseEvent.MOUSE_PRESSED, new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent e) {
mouseDown = true;
! if (BehaviorSkinBase.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
--- 410,420 ----
addEventFilter(MouseEvent.MOUSE_PRESSED, new EventHandler<MouseEvent>() {
@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
*** 657,672 ****
|| hbar.getBoundsInParent().contains(e.getX(), e.getY()));
}
});
addEventFilter(MouseEvent.MOUSE_RELEASED, e -> {
mouseDown = false;
! if (BehaviorSkinBase.IS_TOUCH_SUPPORTED) {
startSBReleasedAnimation();
}
});
addEventFilter(MouseEvent.MOUSE_DRAGGED, e -> {
! if (BehaviorSkinBase.IS_TOUCH_SUPPORTED) {
scrollBarOn();
}
if (! isPanning || ! isPannable()) return;
// With panning enabled, we support panning in both vertical
--- 454,469 ----
|| 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
*** 677,687 ****
// 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 = adjustPixels(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
--- 474,484 ----
// 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
*** 872,905 ****
}
if (lastCell.isFocusTraversable()) return lastCell;
return selectPreviousBeforeIndex(lastCell.getIndex(), context);
}
}));
}
! void updateHbar() {
! // Bring the clipView.clipX back to 0 if control is vertical or
! // the hbar isn't visible (fix for RT-11666)
! if (! isVisible() || getScene() == null) return;
! if (isVertical()) {
! if (hbar.isVisible()) {
! 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);
}
}
}
/***************************************************************************
* *
! * Layout Functionality *
* *
**************************************************************************/
/**
* Overridden to implement somewhat more efficient support for layout. The
--- 669,949 ----
}
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() {
! 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 cellCount = get();
+
+ boolean countChanged = oldCount != cellCount;
+ 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();
+
+ // Fix for RT-13965: Without this line of code, the number of items in
+ // the sheet would constantly grow, leaking memory for the life of the
+ // application. This was especially apparent when the total number of
+ // cells changes - regardless of whether it became bigger or smaller.
+ sheetChildren.clear();
+
+ Parent parent = getParent();
+ if (parent != null) parent.requestLayout();
+ }
+ // 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();
+ 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<Callback<VirtualFlow<T>, 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.
+ */
+ public final void setCellFactory(Callback<VirtualFlow<T>, T> value) {
+ cellFactoryProperty().set(value);
+ }
+
+ /**
+ * Returns the current cell factory.
+ */
+ public final Callback<VirtualFlow<T>, T> getCellFactory() {
+ return cellFactory == null ? null : cellFactory.get();
+ }
+
+ /**
+ * <p>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.
+ *
+ * <p>Refer to the {@link Cell} class documentation for more detail.
+ */
+ public final ObjectProperty<Callback<VirtualFlow<T>, T>> cellFactoryProperty() {
+ if (cellFactory == null) {
+ cellFactory = new SimpleObjectProperty<Callback<VirtualFlow<T>, T>>(this, "cellFactory") {
+ @Override protected void invalidated() {
+ if (get() != null) {
+ accumCell = null;
+ setNeedsLayout(true);
+ recreateCells();
+ if (getParent() != null) getParent().requestLayout();
+ }
}
+ };
}
+ return cellFactory;
}
+
+
/***************************************************************************
* *
! * Public API *
* *
**************************************************************************/
/**
* Overridden to implement somewhat more efficient support for layout. The
*** 915,924 ****
--- 959,969 ----
// appear to impact performance (indeed, it may help), and resolves the
// issue identified in RT-21417.
setNeedsLayout(true);
}
+ /** {@inheritDoc} */
@Override protected void layoutChildren() {
if (needsRecreateCells) {
lastWidth = -1;
lastHeight = -1;
releaseCell(accumCell);
*** 1010,1020 ****
// '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 (BehaviorSkinBase.IS_TOUCH_SUPPORTED) {
if ((tempVisibility == true && (hbar.isVisible() == false || vbar.isVisible() == false)) ||
(tempVisibility == false && (hbar.isVisible() == true || vbar.isVisible() == true))) {
thumbNeedsLayout = true;
}
}
--- 1055,1065 ----
// '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;
}
}
*** 1025,1036 ****
cellNeedsLayout = cell.isNeedsLayout();
if (cellNeedsLayout) break;
}
}
!
! 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) {
--- 1070,1081 ----
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) {
*** 1209,1745 ****
lastPosition = getPosition();
cleanPile();
}
! /**
! * 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.
! */
! protected 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 increment 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 == cellCount && offset == getViewportLength()) {
! index--;
! first = false;
! }
! while (index >= 0 && (offset > 0 || first)) {
! cell = getAvailableCell(index);
! setCellIndex(cell, index);
! resizeCellSize(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 <em>after</em> the last index in
! * the cells ObservableList.
! */
! protected 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;
! 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 - offset 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 - offset;
! 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)) {
! if (startCell != null) {
! logger.info("index exceeds maxCellCount. Check size calculations for " + startCell.getClass());
! } else {
! logger.info("index exceeds maxCellCount");
! }
! }
! return filledWithNonEmpty;
! }
! }
! T cell = getAvailableCell(index);
! setCellIndex(cell, index);
! resizeCellSize(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);
! resizeCellSize(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;
! }
!
! /**
! * @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();
! 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;
! }
!
! // second conditional removed for RT-36669.
! final boolean breadthBarVisible = (maxPrefBreadth > viewportBreadth);// || (needLengthBar && maxPrefBreadth > (viewportBreadth - lengthBarBreadth));
! 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 (!BehaviorSkinBase.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 = snapSize(isVertical ? hbar.prefHeight(-1) : vbar.prefWidth(-1));
! final double lengthBarBreadth = snapSize(isVertical ? vbar.prefWidth(-1) : hbar.prefHeight(-1));
!
! setViewportBreadth((isVertical ? getWidth() : getHeight()) - (needLengthBar ? lengthBarBreadth : 0));
! setViewportLength((isVertical ? getHeight() : getWidth()) - (needBreadthBar ? breadthBarLength : 0));
! }
!
! 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);
! }
!
@Override protected void setWidth(double value) {
if (value != lastWidth) {
super.setWidth(value);
sizeChanged = true;
setNeedsLayout(true);
requestLayout();
}
}
@Override protected void setHeight(double value) {
if (value != lastHeight) {
super.setHeight(value);
sizeChanged = true;
setNeedsLayout(true);
requestLayout();
}
}
! 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);
}
}
! // 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 (!BehaviorSkinBase.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) {
- hbar.resizeRelocate(0, (viewportLength-hbar.getHeight()),
- viewportBreadth, hbar.prefHeight(viewportBreadth));
} else {
! vbar.resizeRelocate((viewportLength-vbar.getWidth()), 0,
! vbar.prefWidth(viewportBreadth), 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() || BehaviorSkinBase.IS_TOUCH_SUPPORTED)) {
! 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(numCellsVisibleOnScreen / (float) cellCount);
}
}
! 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 (!BehaviorSkinBase.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) {
! vbar.resizeRelocate((viewportBreadth-vbar.getWidth()), 0, vbar.prefWidth(viewportLength), viewportLength);
! } else {
! hbar.resizeRelocate(0, (viewportBreadth-hbar.getHeight()), viewportLength, hbar.prefHeight(-1));
}
}
}
! if (corner.isVisible()) {
! if (!BehaviorSkinBase.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(snapSize(isVertical ? viewportBreadth : viewportLength),
! snapSize(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() {
! double size = Math.max(getMaxPrefBreadth(), getViewportBreadth());
! boolean isVertical = isVertical();
! // 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++) {
! Cell<?> cell = cells.get(i);
! if (isVertical) {
! cell.resize(size, cell.prefHeight(size));
! } else {
! cell.resize(cell.prefWidth(size), size);
}
}
}
! 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));
}
}
}
! /***************************************************************************
! * *
! * Helper functions for working with cells *
! * *
! **************************************************************************/
! /**
! * Return the index for a given cell. This allows subclasses to customise
! * how cell indices are retrieved.
! */
! protected int getCellIndex(T cell){
! return cell.getIndex();
}
/**
* 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
--- 1254,1635 ----
lastPosition = getPosition();
cleanPile();
}
! /** {@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.
! */
! 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;
! }
! cell = null;
}
! if (cell == null) {
! if (pile.size() > 0) {
! // we try to get a cell with an index that is the same even/odd
! // as the prefIndex. This saves us from having to run so much
! // css on the cell as it will not change from even to odd, or
! // vice versa
! final boolean prefIndexIsEven = (prefIndex & 1) == 0;
! for (int i = 0, max = pile.size(); i < max; i++) {
! final T c = pile.get(i);
! final int cellIndex = getCellIndex(c);
! if ((cellIndex & 1) == 0 && prefIndexIsEven) {
! cell = c;
! pile.remove(i);
! break;
! } else if ((cellIndex & 1) == 1 && ! prefIndexIsEven) {
! cell = c;
! pile.remove(i);
! break;
}
}
! if (cell == null) {
! cell = pile.removeFirst();
}
} else {
! 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.
+ */
+ 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.
! */
! 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.
+ */
+ 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".
! */
! 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".
*/
! 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).
! */
! 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.
! */
! public void scrollTo(int index) {
! T cell = getVisibleCell(index);
! if (cell != null) {
! scrollTo(cell);
! } else {
! adjustPositionToIndex(index);
! addAllToPile();
! requestLayout();
}
}
! /**
! * 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.
! */
! public void scrollToTop(int index) {
! boolean posSet = false;
! if (index >= getCellCount() - 1) {
! setPosition(1);
! posSet = true;
! } else if (index < 0) {
! setPosition(0);
! posSet = true;
! }
!
! if (! posSet) {
! adjustPositionToIndex(index);
! double offset = - computeOffsetForCell(index);
! adjustByPixelAmount(offset);
}
+
+ 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.
*/
! public double scrollPixels(final double delta) {
! // 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;
!
! 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 (actualLayoutY != layoutY) {
! // 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) {
+ int firstIndex = getCellIndex(firstCell);
+ double prevIndexSize = getCellLength(firstIndex - 1);
+ addLeadingCells(firstIndex - 1, 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);
! lastPosition = getPosition();
!
! // notify
! return delta; // TODO fake
! }
!
! /** {@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
*** 1770,1782 ****
return pile.get(0);
}
// We need to use the accumCell and return that
if (accumCell == null) {
! Callback<VirtualFlow,T> createCell = getCreateCell();
! if (createCell != null) {
! accumCell = createCell.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.
--- 1660,1672 ----
return pile.get(0);
}
// We need to use the accumCell and return that
if (accumCell == null) {
! Callback<VirtualFlow<T>,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.
*** 1798,2471 ****
resizeCellSize(accumCell);
return accumCell;
}
/**
! * After using the accum cell, it needs to be released!
*/
! private void releaseCell(T cell) {
! if (accumCell != null && cell == accumCell) {
! accumCell.updateIndex(-1);
}
}
/**
! * This method is an experts-only method - 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 sheet. From that point on it will be
! * unmanaged, and is up to the caller of this method to manage it.
*/
! 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;
! }
}
}
! if (cell == null) {
! Callback<VirtualFlow, T> createCell = getCreateCell();
! if (createCell != null) {
! cell = createCell.call(this);
}
}
! if (cell != null) {
! setCellIndex(cell, index);
! resizeCellSize(cell);
! cell.setVisible(false);
! sheetChildren.add(cell);
! privateCells.add(cell);
}
!
! return cell;
}
! private final List<T> privateCells = new ArrayList<>();
!
! private void releaseAllPrivateCells() {
! sheetChildren.removeAll(privateCells);
}
/**
* 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.
*/
! protected double getCellLength(int index) {
! if (fixedCellSizeEnabled) return fixedCellSize;
T cell = getCell(index);
double length = getCellLength(cell);
releaseCell(cell);
return length;
}
/**
*/
! protected double getCellBreadth(int index) {
T cell = getCell(index);
double b = getCellBreadth(cell);
releaseCell(cell);
return b;
}
/**
* Gets the length of a specific cell
*/
! protected double getCellLength(T cell) {
if (cell == null) return 0;
! if (fixedCellSizeEnabled) return fixedCellSize;
return isVertical() ?
cell.getLayoutBounds().getHeight()
: cell.getLayoutBounds().getWidth();
}
- // private double getCellPrefLength(T cell) {
- // return isVertical() ?
- // cell.prefHeight(-1)
- // : cell.prefWidth(-1);
- // }
-
/**
* Gets the breadth of a specific cell
*/
! protected double getCellBreadth(Cell cell) {
return isVertical() ?
cell.prefWidth(-1)
: cell.prefHeight(-1);
}
/**
* Gets the layout position of the cell along the length axis
*/
! protected double getCellPosition(T cell) {
if (cell == null) return 0;
return isVertical() ?
cell.getLayoutY()
: cell.getLayoutX();
}
! protected void positionCell(T cell, double position) {
if (isVertical()) {
cell.setLayoutX(0);
cell.setLayoutY(snapSize(position));
} else {
cell.setLayoutX(snapSize(position));
cell.setLayoutY(0);
}
}
! protected void resizeCellSize(T cell) {
if (cell == null) return;
! if (isVertical()) {
! double width = Math.max(getMaxPrefBreadth(), getViewportBreadth());
! cell.resize(width, fixedCellSizeEnabled ? fixedCellSize : Utils.boundedSize(cell.prefHeight(width), cell.minHeight(width), cell.maxHeight(width)));
! } else {
! double height = Math.max(getMaxPrefBreadth(), getViewportBreadth());
! cell.resize(fixedCellSizeEnabled ? fixedCellSize : Utils.boundedSize(cell.prefWidth(height), cell.minWidth(height), cell.maxWidth(height)), height);
}
}
! 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);
}
}
! /***************************************************************************
! * *
! * Helper functions for cell management *
! * *
! **************************************************************************/
! /**
! * Indicates that this is a newly created cell and we need call impl_processCSS for it.
! *
! * See RT-23616 for more details.
! */
! private static final String NEW_CELL = "newcell";
! /**
! * 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.
*/
! 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;
! }
! cell = null;
}
!
! if (cell == null) {
! if (pile.size() > 0) {
! // we try to get a cell with an index that is the same even/odd
! // as the prefIndex. This saves us from having to run so much
! // css on the cell as it will not change from even to odd, or
! // vice versa
! final boolean prefIndexIsEven = (prefIndex & 1) == 0;
! for (int i = 0, max = pile.size(); i < max; i++) {
! final T c = pile.get(i);
! final int cellIndex = getCellIndex(c);
!
! if ((cellIndex & 1) == 0 && prefIndexIsEven) {
! cell = c;
! pile.remove(i);
! break;
! } else if ((cellIndex & 1) == 1 && ! prefIndexIsEven) {
! cell = c;
! pile.remove(i);
! break;
}
}
! if (cell == null) {
! cell = pile.removeFirst();
! }
! } else {
! cell = getCreateCell().call(this);
! cell.getProperties().put(NEW_CELL, null);
! }
}
! if (cell.getParent() == null) {
! sheetChildren.add(cell);
! }
! return cell;
}
-
- // protected to allow subclasses to clean up
- protected void addAllToPile() {
- for (int i = 0, max = cells.size(); i < max; i++) {
- addToPile(cells.removeFirst());
}
}
/**
! * 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(Cell<?> 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;
}
! /**
! * 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.
! */
! 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.
! */
! 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.
- */
- public T getFirstVisibleCell() {
- if (cells.isEmpty() || getViewportLength() <= 0) return null;
- T cell = cells.getFirst();
- return cell.isEmpty() ? null : cell;
}
! // Returns last visible cell whose bounds are entirely within the viewport
! public 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 first visible cell whose bounds are entirely within the viewport
! public 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;
}
! /**
! * 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". This is bad public API!
! */
! public void showAsFirst(T firstCell) {
! if (firstCell != null) {
! adjustPixels(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". This is bad public API!
! */
! public void showAsLast(T lastCell) {
! if (lastCell != null) {
! adjustPixels(getCellPosition(lastCell) + getCellLength(lastCell) - getViewportLength());
}
}
! /**
! * Adjusts the cells such that the selected cell will be fully visible in
! * the viewport (but only just).
! */
! public void show(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) {
! adjustPixels(start);
! } else if (end > viewportLength) {
! adjustPixels(end - viewportLength);
}
}
}
! public void show(int index) {
! T cell = getVisibleCell(index);
! if (cell != null) {
! show(cell);
! } else {
! // See if the previous index is a visible cell
! T prev = getVisibleCell(index - 1);
! if (prev != null) {
! // Need to add a new cell and then we can show it
! // layingOut = true;
! cell = getAvailableCell(index);
! setCellIndex(cell, index);
! resizeCellSize(cell); // resize must be after config
! cells.addLast(cell);
! positionCell(cell, getCellPosition(prev) + getCellLength(prev));
! setMaxPrefBreadth(Math.max(getMaxPrefBreadth(), getCellBreadth(cell)));
! cell.setVisible(true);
! show(cell);
! // layingOut = false;
! return;
}
- // See if the next index is a visible cell
- T next = getVisibleCell(index + 1);
- if (next != null) {
- // layingOut = true;
- cell = getAvailableCell(index);
- setCellIndex(cell, index);
- resizeCellSize(cell); // resize must be after config
- cells.addFirst(cell);
- positionCell(cell, getCellPosition(next) - getCellLength(cell));
- setMaxPrefBreadth(Math.max(getMaxPrefBreadth(), getCellBreadth(cell)));
- cell.setVisible(true);
- show(cell);
- // layingOut = false;
- return;
}
! // In this case, we're asked to show a random cell
! // layingOut = true;
! adjustPositionToIndex(index);
! addAllToPile();
! requestLayout();
! // layingOut = false;
}
}
! public void scrollTo(int index) {
! boolean posSet = false;
! if (index >= cellCount - 1) {
! setPosition(1);
! posSet = true;
! } else if (index < 0) {
! setPosition(0);
! posSet = true;
}
! if (! posSet) {
! adjustPositionToIndex(index);
! double offset = - computeOffsetForCell(index);
! adjustByPixelAmount(offset);
}
-
- 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) {
- adjustPixels(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.
*/
! public double adjustPixels(final double delta) {
! // 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;
! 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 (actualLayoutY != layoutY) {
! // 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) {
- int firstIndex = getCellIndex(firstCell);
- double prevIndexSize = getCellLength(firstIndex - 1);
- addLeadingCells(firstIndex - 1, 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);
! lastPosition = getPosition();
! // notify
! return delta; // TODO fake
}
! 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();
! public void reconfigureCells() {
! needsReconfigureCells = true;
! requestLayout();
}
! public void recreateCells() {
! needsRecreateCells = true;
! requestLayout();
}
-
- public void rebuildCells() {
- needsRebuildCells = true;
- requestLayout();
}
! public void requestCellLayout() {
! needsCellsLayout = true;
! requestLayout();
}
! public void setCellDirty(int index) {
! dirtyCells.set(index);
! requestLayout();
}
! private static final double GOLDEN_RATIO_MULTIPLIER = 0.618033987;
private double getPrefBreadth(double oppDimension) {
double max = getMaxCellWidth(10);
// This primarily exists for the case where we do not want the breadth
--- 1688,2593 ----
resizeCellSize(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.
*/
! protected int getCellIndex(T cell){
! return cell.getIndex();
}
!
!
! /***************************************************************************
! * *
! * Private implementation *
! * *
! **************************************************************************/
!
! final VirtualScrollBar getHbar() {
! return hbar;
}
+ final VirtualScrollBar 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.
! * <p>
! * 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.
! * The access on this variable is package ONLY FOR TESTING.
! */
! private double viewportLength;
! void setViewportLength(double value) {
! this.viewportLength = value;
! }
! 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(Cell 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) {
if (isVertical()) {
cell.setLayoutX(0);
cell.setLayoutY(snapSize(position));
} else {
cell.setLayoutX(snapSize(position));
cell.setLayoutY(0);
}
}
! private void resizeCellSize(T cell) {
if (cell == null) return;
! if (isVertical()) {
! double width = Math.max(getMaxPrefBreadth(), getViewportBreadth());
! cell.resize(width, fixedCellSizeEnabled ? getFixedCellSize() : Utils.boundedSize(cell.prefHeight(width), cell.minHeight(width), cell.maxHeight(width)));
! } else {
! double height = Math.max(getMaxPrefBreadth(), getViewportBreadth());
! cell.resize(fixedCellSizeEnabled ? getFixedCellSize() : Utils.boundedSize(cell.prefWidth(height), cell.minWidth(height), cell.maxWidth(height)), height);
! }
! }
!
! private List<T> getCells() {
! return cells;
! }
!
! // Returns last visible cell whose bounds are entirely within the viewport
! 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 first visible cell whose bounds are entirely within the viewport
! 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 increment 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);
! resizeCellSize(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 <em>after</em> 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 - offset 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 - offset;
! 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)) {
! if (startCell != null) {
! logger.info("index exceeds maxCellCount. Check size calculations for " + startCell.getClass());
! } else {
! logger.info("index exceeds maxCellCount");
! }
! }
! return filledWithNonEmpty;
! }
! }
! T cell = getAvailableCell(index);
! setCellIndex(cell, index);
! resizeCellSize(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);
! resizeCellSize(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;
! }
! void reconfigureCells() {
! needsReconfigureCells = true;
! requestLayout();
}
+
+ void recreateCells() {
+ needsRecreateCells = true;
+ requestLayout();
}
! void rebuildCells() {
! needsRebuildCells = true;
! requestLayout();
! }
+ void requestCellLayout() {
+ needsCellsLayout = true;
+ requestLayout();
+ }
! void setCellDirty(int index) {
! dirtyCells.set(index);
! requestLayout();
! }
! 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() {
! // Bring the clipView.clipX back to 0 if control is vertical or
! // the hbar isn't visible (fix for RT-11666)
! if (! isVisible() || getScene() == null) return;
! if (isVertical()) {
! if (hbar.isVisible()) {
! 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);
}
}
}
/**
! * @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;
}
! // second conditional removed for RT-36669.
! final boolean breadthBarVisible = (maxPrefBreadth > viewportBreadth);// || (needLengthBar && maxPrefBreadth > (viewportBreadth - lengthBarBreadth));
! 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 = snapSize(isVertical ? hbar.prefHeight(-1) : vbar.prefWidth(-1));
+ final double lengthBarBreadth = snapSize(isVertical ? vbar.prefWidth(-1) : hbar.prefHeight(-1));
+
+ setViewportBreadth((isVertical ? getWidth() : getHeight()) - (needLengthBar ? lengthBarBreadth : 0));
+ setViewportLength((isVertical ? getHeight() : getWidth()) - (needBreadthBar ? breadthBarLength : 0));
}
! 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);
}
! 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);
}
}
! // 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) {
! hbar.resizeRelocate(0, (viewportLength-hbar.getHeight()),
! viewportBreadth, hbar.prefHeight(viewportBreadth));
! } else {
! vbar.resizeRelocate((viewportLength-vbar.getWidth()), 0,
! vbar.prefWidth(viewportBreadth), 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(numCellsVisibleOnScreen / (float) cellCount);
}
}
! 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) {
! vbar.resizeRelocate((viewportBreadth-vbar.getWidth()), 0, vbar.prefWidth(viewportLength), viewportLength);
! } else {
! hbar.resizeRelocate(0, (viewportBreadth-hbar.getHeight()), viewportLength, hbar.prefHeight(-1));
}
}
}
! 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(snapSize(isVertical ? viewportBreadth : viewportLength),
! snapSize(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() {
! double size = Math.max(getMaxPrefBreadth(), getViewportBreadth());
! boolean isVertical = isVertical();
! // 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++) {
! Cell<?> cell = cells.get(i);
! if (isVertical) {
! cell.resize(size, cell.prefHeight(size));
! } else {
! cell.resize(cell.prefWidth(size), size);
! }
! }
}
! 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.updateIndex(-1);
! }
! }
! /**
! * This method is an experts-only method - 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 sheet. From that point on it will be
! * unmanaged, and is up to the caller of this method to manage it.
! */
! 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;
}
}
}
! if (cell == null) {
! Callback<VirtualFlow<T>, T> cellFactory = getCellFactory();
! if (cellFactory != null) {
! cell = cellFactory.call(this);
}
}
+
+ if (cell != null) {
+ setCellIndex(cell, index);
+ resizeCellSize(cell);
+ cell.setVisible(false);
+ sheetChildren.add(cell);
+ privateCells.add(cell);
}
+
+ return cell;
}
! private final List<T> privateCells = new ArrayList<>();
! private void releaseAllPrivateCells() {
! sheetChildren.removeAll(privateCells);
! }
! /**
! * 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(Cell<?> 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
*** 2480,2519 ****
return max;
}
private double getPrefLength() {
double sum = 0.0;
! int rows = Math.min(10, cellCount);
for (int i = 0; i < rows; i++) {
sum += getCellLength(i);
}
return sum;
}
- @Override protected double computePrefWidth(double height) {
- double w = isVertical() ? getPrefBreadth(height) : getPrefLength();
- return w + vbar.prefWidth(-1);
- }
-
- @Override protected double computePrefHeight(double width) {
- double h = isVertical() ? getPrefLength() : getPrefBreadth(width);
- return h + hbar.prefHeight(-1);
- }
-
double getMaxCellWidth(int rowsToCount) {
double max = 0.0;
// we always measure at least one row
! int rows = Math.max(1, rowsToCount == -1 ? cellCount : 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
--- 2602,2629 ----
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
*** 2644,2653 ****
--- 2754,2771 ----
// 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 {
*** 2713,2723 ****
* 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.
* <p>
* This class is package private solely for the sake of testing.
*/
! public static class ArrayLinkedList<T> extends AbstractList<T> {
/**
* 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.
--- 2831,2841 ----
* 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.
* <p>
* This class is package private solely for the sake of testing.
*/
! static class ArrayLinkedList<T> extends AbstractList<T> {
/**
* 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.
*** 2812,2822 ****
return remove(lastIndex - firstIndex);
}
public T remove(int index) {
if (index > lastIndex - firstIndex || index < 0) {
! throw new java.lang.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
--- 2930,2940 ----
return remove(lastIndex - firstIndex);
}
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
*** 2848,2889 ****
array.set(lastIndex--, null);
return cell;
}
}
}
-
- Timeline sbTouchTimeline;
- KeyFrame sbTouchKF1;
- KeyFrame sbTouchKF2;
-
- private boolean needBreadthBar;
- private boolean needLengthBar;
- private boolean tempVisibility = false;
-
- protected 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();
- }
-
- protected void scrollBarOn() {
- tempVisibility = true;
- requestLayout();
- }
}
--- 2966,2971 ----