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 ----