1 /*
   2  * Copyright (c) 2010, 2015, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javafx.scene.control.skin;
  27 
  28 import com.sun.javafx.scene.control.Logging;
  29 import com.sun.javafx.scene.control.Properties;
  30 import com.sun.javafx.scene.control.VirtualScrollBar;
  31 import com.sun.javafx.scene.control.skin.Utils;
  32 import com.sun.javafx.scene.traversal.Algorithm;
  33 import com.sun.javafx.scene.traversal.Direction;
  34 import com.sun.javafx.scene.traversal.ParentTraversalEngine;
  35 import com.sun.javafx.scene.traversal.TraversalContext;
  36 import javafx.animation.KeyFrame;
  37 import javafx.animation.Timeline;
  38 import javafx.beans.InvalidationListener;
  39 import javafx.beans.Observable;
  40 import javafx.beans.property.BooleanProperty;
  41 import javafx.beans.property.BooleanPropertyBase;
  42 import javafx.beans.property.DoubleProperty;
  43 import javafx.beans.property.IntegerProperty;
  44 import javafx.beans.property.ObjectProperty;
  45 import javafx.beans.property.SimpleBooleanProperty;
  46 import javafx.beans.property.SimpleDoubleProperty;
  47 import javafx.beans.property.SimpleIntegerProperty;
  48 import javafx.beans.property.SimpleObjectProperty;
  49 import javafx.beans.value.ChangeListener;
  50 import javafx.collections.ObservableList;
  51 import javafx.event.EventDispatcher;
  52 import javafx.event.EventHandler;
  53 import javafx.geometry.Orientation;
  54 import javafx.scene.AccessibleRole;
  55 import javafx.scene.Group;
  56 import javafx.scene.Node;
  57 import javafx.scene.Parent;
  58 import javafx.scene.Scene;
  59 import javafx.scene.control.Cell;
  60 import javafx.scene.control.IndexedCell;
  61 import javafx.scene.control.ListCell;
  62 import javafx.scene.control.ScrollBar;
  63 import javafx.scene.input.MouseEvent;
  64 import javafx.scene.input.ScrollEvent;
  65 import javafx.scene.layout.Region;
  66 import javafx.scene.layout.StackPane;
  67 import javafx.scene.shape.Rectangle;
  68 import javafx.util.Callback;
  69 import javafx.util.Duration;
  70 import sun.util.logging.PlatformLogger;
  71 
  72 import java.util.AbstractList;
  73 import java.util.ArrayList;
  74 import java.util.BitSet;
  75 import java.util.List;
  76 
  77 /**
  78  * Implementation of a virtualized container using a cell based mechanism. This
  79  * is used by the skin implementations for UI controls such as
  80  * {@link javafx.scene.control.ListView}, {@link javafx.scene.control.TreeView},
  81  * {@link javafx.scene.control.TableView}, and {@link javafx.scene.control.TreeTableView}.
  82  *
  83  * @since 9
  84  */
  85 public class VirtualFlow<T extends IndexedCell> extends Region {
  86 
  87     /***************************************************************************
  88      *                                                                         *
  89      * Static fields                                                           *
  90      *                                                                         *
  91      **************************************************************************/
  92 
  93     /**
  94      * Scroll events may request to scroll about a number of "lines". We first
  95      * decide how big one "line" is - for fixed cell size it's clear,
  96      * for variable cell size we settle on a single number so that the scrolling
  97      * speed is consistent. Now if the line is so big that
  98      * MIN_SCROLLING_LINES_PER_PAGE of them don't fit into one page, we make
  99      * them smaller to prevent the scrolling step to be too big (perhaps
 100      * even more than one page).
 101      */
 102     private static final int MIN_SCROLLING_LINES_PER_PAGE = 8;
 103 
 104     /**
 105      * Indicates that this is a newly created cell and we need call impl_processCSS for it.
 106      *
 107      * See RT-23616 for more details.
 108      */
 109     private static final String NEW_CELL = "newcell";
 110 
 111     private static final double GOLDEN_RATIO_MULTIPLIER = 0.618033987;
 112 
 113 
 114 
 115     /***************************************************************************
 116      *                                                                         *
 117      * Private fields                                                          *
 118      *                                                                         *
 119      **************************************************************************/
 120                     
 121     private boolean touchDetected = false;
 122     private boolean mouseDown = false;
 123 
 124     /**
 125      * The width of the VirtualFlow the last time it was laid out. We
 126      * use this information for several fast paths during the layout pass.
 127      */
 128     double lastWidth = -1;
 129 
 130     /**
 131      * The height of the VirtualFlow the last time it was laid out. We
 132      * use this information for several fast paths during the layout pass.
 133      */
 134     double lastHeight = -1;
 135 
 136     /**
 137      * The number of "virtual" cells in the flow the last time it was laid out.
 138      * For example, there may have been 1000 virtual cells, but only 20 actual
 139      * cells created and in use. In that case, lastCellCount would be 1000.
 140      */
 141     int lastCellCount = 0;
 142 
 143     /**
 144      * We remember the last value for vertical the last time we laid out the
 145      * flow. If vertical has changed, we will want to change the max & value
 146      * for the different scroll bars. Since we do all the scroll bar update
 147      * work in the layoutChildren function, we need to know what the old value for
 148      * vertical was.
 149      */
 150     boolean lastVertical;
 151 
 152     /**
 153      * The position last time we laid out. If none of the lastXXX vars have
 154      * changed respective to their values in layoutChildren, then we can just punt
 155      * out of the method (I hope...)
 156      */
 157     double lastPosition;
 158 
 159     /**
 160      * The breadth of the first visible cell last time we laid out.
 161      */
 162     double lastCellBreadth = -1;
 163 
 164     /**
 165      * The length of the first visible cell last time we laid out.
 166      */
 167     double lastCellLength = -1;
 168 
 169     /**
 170      * The list of cells representing those cells which actually make up the
 171      * current view. The cells are ordered such that the first cell in this
 172      * list is the first in the view, and the last cell is the last in the
 173      * view. When pixel scrolling, the list is simply shifted and items drop
 174      * off the beginning or the end, depending on the order of scrolling.
 175      * <p>
 176      * This is package private ONLY FOR TESTING
 177      */
 178     final ArrayLinkedList<T> cells = new ArrayLinkedList<T>();
 179 
 180     /**
 181      * A structure containing cells that can be reused later. These are cells
 182      * that at one time were needed to populate the view, but now are no longer
 183      * needed. We keep them here until they are needed again.
 184      * <p>
 185      * This is package private ONLY FOR TESTING
 186      */
 187     final ArrayLinkedList<T> pile = new ArrayLinkedList<T>();
 188 
 189     /**
 190      * A special cell used to accumulate bounds, such that we reduce object
 191      * churn. This cell must be recreated whenever the cell factory function
 192      * changes. This has package access ONLY for testing.
 193      */
 194     T accumCell;
 195 
 196     /**
 197      * This group is used for holding the 'accumCell'. 'accumCell' must
 198      * be added to the skin for it to be styled. Otherwise, it doesn't
 199      * report the correct width/height leading to issues when scrolling
 200      * the flow
 201      */
 202     Group accumCellParent;
 203 
 204     /**
 205      * The group which holds the cells.
 206      */
 207     final Group sheet;
 208 
 209     final ObservableList<Node> sheetChildren;
 210 
 211     /**
 212      * The scroll bar used for scrolling horizontally. This has package access
 213      * ONLY for testing.
 214      */
 215     private VirtualScrollBar hbar = new VirtualScrollBar(this);
 216 
 217     /**
 218      * The scroll bar used to scrolling vertically. This has package access
 219      * ONLY for testing.
 220      */
 221     private VirtualScrollBar vbar = new VirtualScrollBar(this);
 222 
 223     /**
 224      * Control in which the cell's sheet is placed and forms the viewport. The
 225      * viewportBreadth and viewportLength are simply the dimensions of the
 226      * clipView. This has package access ONLY for testing.
 227      */
 228     ClippedContainer clipView;
 229 
 230     /**
 231      * When both the horizontal and vertical scroll bars are visible,
 232      * we have to 'fill in' the bottom right corner where the two scroll bars
 233      * meet. This is handled by this corner region. This has package access
 234      * ONLY for testing.
 235      */
 236     StackPane corner;
 237 
 238     // used for panning the virtual flow
 239     private double lastX;
 240     private double lastY;
 241     private boolean isPanning = false;
 242 
 243     private boolean fixedCellSizeEnabled = false;
 244 
 245     private boolean needsReconfigureCells = false; // when cell contents are the same
 246     private boolean needsRecreateCells = false; // when cell factory changed
 247     private boolean needsRebuildCells = false; // when cell contents have changed
 248     private boolean needsCellsLayout = false;
 249     private boolean sizeChanged = false;
 250     private final BitSet dirtyCells = new BitSet();
 251 
 252     Timeline sbTouchTimeline;
 253     KeyFrame sbTouchKF1;
 254     KeyFrame sbTouchKF2;
 255 
 256     private boolean needBreadthBar;
 257     private boolean needLengthBar;
 258     private boolean tempVisibility = false;
 259 
 260 
 261 
 262     /***************************************************************************
 263      *                                                                         *
 264      * Constructors                                                            *
 265      *                                                                         *
 266      **************************************************************************/
 267 
 268     /**
 269      * Creates a new VirtualFlow instance.
 270      */
 271     public VirtualFlow() {
 272         getStyleClass().add("virtual-flow");
 273         setId("virtual-flow");
 274 
 275         // initContent
 276         // --- sheet
 277         sheet = new Group();
 278         sheet.getStyleClass().add("sheet");
 279         sheet.setAutoSizeChildren(false);
 280         
 281         sheetChildren = sheet.getChildren();
 282 
 283         // --- clipView
 284         clipView = new ClippedContainer(this);
 285         clipView.setNode(sheet);
 286         getChildren().add(clipView);
 287 
 288         // --- accumCellParent
 289         accumCellParent = new Group();
 290         accumCellParent.setVisible(false);
 291         getChildren().add(accumCellParent);
 292 
 293 
 294         /*
 295         ** don't allow the ScrollBar to handle the ScrollEvent,
 296         ** In a VirtualFlow a vertical scroll should scroll on the vertical only,
 297         ** whereas in a horizontal ScrollBar it can scroll horizontally.
 298         */ 
 299         // block the event from being passed down to children
 300         final EventDispatcher blockEventDispatcher = (event, tail) -> event;
 301         // block ScrollEvent from being passed down to scrollbar's skin
 302         final EventDispatcher oldHsbEventDispatcher = hbar.getEventDispatcher();
 303         hbar.setEventDispatcher((event, tail) -> {
 304             if (event.getEventType() == ScrollEvent.SCROLL &&
 305                     !((ScrollEvent)event).isDirect()) {
 306                 tail = tail.prepend(blockEventDispatcher);
 307                 tail = tail.prepend(oldHsbEventDispatcher);
 308                 return tail.dispatchEvent(event);
 309             }
 310             return oldHsbEventDispatcher.dispatchEvent(event, tail);
 311         });
 312         // block ScrollEvent from being passed down to scrollbar's skin
 313         final EventDispatcher oldVsbEventDispatcher = vbar.getEventDispatcher();
 314         vbar.setEventDispatcher((event, tail) -> {
 315             if (event.getEventType() == ScrollEvent.SCROLL &&
 316                     !((ScrollEvent)event).isDirect()) {
 317                 tail = tail.prepend(blockEventDispatcher);
 318                 tail = tail.prepend(oldVsbEventDispatcher);
 319                 return tail.dispatchEvent(event);
 320             }
 321             return oldVsbEventDispatcher.dispatchEvent(event, tail);
 322         });
 323         /*
 324         ** listen for ScrollEvents over the whole of the VirtualFlow
 325         ** area, the above dispatcher having removed the ScrollBars
 326         ** scroll event handling.
 327         */
 328         setOnScroll(new EventHandler<ScrollEvent>() {
 329             @Override public void handle(ScrollEvent event) {
 330                 if (Properties.IS_TOUCH_SUPPORTED) {
 331                     if (touchDetected == false &&  mouseDown == false ) {
 332                         startSBReleasedAnimation();
 333                     }
 334                 }
 335                 /*
 336                 ** calculate the delta in the direction of the flow.
 337                 */
 338                 double virtualDelta = 0.0;
 339                 if (isVertical()) {
 340                     switch(event.getTextDeltaYUnits()) {
 341                         case PAGES:
 342                             virtualDelta = event.getTextDeltaY() * lastHeight;
 343                             break;
 344                         case LINES:
 345                             double lineSize;
 346                             if (fixedCellSizeEnabled) {
 347                                 lineSize = getFixedCellSize();
 348                             } else {
 349                                 // For the scrolling to be reasonably consistent
 350                                 // we set the lineSize to the average size
 351                                 // of all currently loaded lines.
 352                                 T lastCell = cells.getLast();
 353                                 lineSize =
 354                                         (getCellPosition(lastCell)
 355                                             + getCellLength(lastCell)
 356                                             - getCellPosition(cells.getFirst()))
 357                                         / cells.size();
 358                             }
 359 
 360                             if (lastHeight / lineSize < MIN_SCROLLING_LINES_PER_PAGE) {
 361                                 lineSize = lastHeight / MIN_SCROLLING_LINES_PER_PAGE;
 362                             }
 363 
 364                             virtualDelta = event.getTextDeltaY() * lineSize;
 365                             break;
 366                         case NONE:
 367                             virtualDelta = event.getDeltaY();
 368                     }
 369                 } else { // horizontal
 370                     switch(event.getTextDeltaXUnits()) {
 371                         case CHARACTERS:
 372                             // can we get character size here?
 373                             // for now, fall through to pixel values
 374                         case NONE:
 375                             double dx = event.getDeltaX();
 376                             double dy = event.getDeltaY();
 377 
 378                             virtualDelta = (Math.abs(dx) > Math.abs(dy) ? dx : dy);
 379                     }
 380                 }
 381 
 382                 if (virtualDelta != 0.0) {
 383                     /*
 384                     ** only consume it if we use it
 385                     */
 386                     double result = scrollPixels(-virtualDelta);
 387                     if (result != 0.0) {
 388                         event.consume();
 389                     }
 390                 }
 391 
 392                 ScrollBar nonVirtualBar = isVertical() ? hbar : vbar;
 393                 if (needBreadthBar) {
 394                     double nonVirtualDelta = isVertical() ? event.getDeltaX() : event.getDeltaY();
 395                     if (nonVirtualDelta != 0.0) {
 396                         double newValue = nonVirtualBar.getValue() - nonVirtualDelta;
 397                         if (newValue < nonVirtualBar.getMin()) {
 398                             nonVirtualBar.setValue(nonVirtualBar.getMin());
 399                         } else if (newValue > nonVirtualBar.getMax()) {
 400                             nonVirtualBar.setValue(nonVirtualBar.getMax());
 401                         } else {
 402                             nonVirtualBar.setValue(newValue);
 403                         }
 404                         event.consume();
 405                     }
 406                 }
 407             }
 408         });
 409 
 410 
 411         addEventFilter(MouseEvent.MOUSE_PRESSED, new EventHandler<MouseEvent>() {
 412             @Override
 413             public void handle(MouseEvent e) {
 414                 mouseDown = true;
 415                 if (Properties.IS_TOUCH_SUPPORTED) {
 416                     scrollBarOn();
 417                 }
 418                 if (isFocusTraversable()) {
 419                     // We check here to see if the current focus owner is within
 420                     // this VirtualFlow, and if so we back-off from requesting
 421                     // focus back to the VirtualFlow itself. This is particularly
 422                     // relevant given the bug identified in RT-32869. In this
 423                     // particular case TextInputControl was clearing selection
 424                     // when the focus on the TextField changed, meaning that the
 425                     // right-click context menu was not showing the correct
 426                     // options as there was no selection in the TextField.
 427                     boolean doFocusRequest = true;
 428                     Node focusOwner = getScene().getFocusOwner();
 429                     if (focusOwner != null) {
 430                         Parent parent = focusOwner.getParent();
 431                         while (parent != null) {
 432                             if (parent.equals(VirtualFlow.this)) {
 433                                 doFocusRequest = false;
 434                                 break;
 435                             }
 436                             parent = parent.getParent();
 437                         }
 438                     }
 439 
 440                     if (doFocusRequest) {
 441                         requestFocus();
 442                     }
 443                 }
 444 
 445                 lastX = e.getX();
 446                 lastY = e.getY();
 447 
 448                 // determine whether the user has push down on the virtual flow,
 449                 // or whether it is the scrollbar. This is done to prevent
 450                 // mouse events being 'doubled up' when dragging the scrollbar
 451                 // thumb - it has the side-effect of also starting the panning
 452                 // code, leading to flicker
 453                 isPanning = ! (vbar.getBoundsInParent().contains(e.getX(), e.getY())
 454                         || hbar.getBoundsInParent().contains(e.getX(), e.getY()));
 455             }
 456         });
 457         addEventFilter(MouseEvent.MOUSE_RELEASED, e -> {
 458             mouseDown = false;
 459             if (Properties.IS_TOUCH_SUPPORTED) {
 460                 startSBReleasedAnimation();
 461             }
 462         });
 463         addEventFilter(MouseEvent.MOUSE_DRAGGED, e -> {
 464             if (Properties.IS_TOUCH_SUPPORTED) {
 465                 scrollBarOn();
 466             }
 467             if (! isPanning || ! isPannable()) return;
 468 
 469             // With panning enabled, we support panning in both vertical
 470             // and horizontal directions, regardless of the fact that
 471             // VirtualFlow is virtual in only one direction.
 472             double xDelta = lastX - e.getX();
 473             double yDelta = lastY - e.getY();
 474 
 475             // figure out the distance that the mouse moved in the virtual
 476             // direction, and then perform the movement along that axis
 477             // virtualDelta will contain the amount we actually did move
 478             double virtualDelta = isVertical() ? yDelta : xDelta;
 479             double actual = scrollPixels(virtualDelta);
 480             if (actual != 0) {
 481                 // update last* here, as we know we've just adjusted the
 482                 // scrollbar. This means we don't get the situation where a
 483                 // user presses-and-drags a long way past the min or max
 484                 // values, only to change directions and see the scrollbar
 485                 // start moving immediately.
 486                 if (isVertical()) lastY = e.getY();
 487                 else lastX = e.getX();
 488             }
 489 
 490             // similarly, we do the same in the non-virtual direction
 491             double nonVirtualDelta = isVertical() ? xDelta : yDelta;
 492             ScrollBar nonVirtualBar = isVertical() ? hbar : vbar;
 493             if (nonVirtualBar.isVisible()) {
 494                 double newValue = nonVirtualBar.getValue() + nonVirtualDelta;
 495                 if (newValue < nonVirtualBar.getMin()) {
 496                     nonVirtualBar.setValue(nonVirtualBar.getMin());
 497                 } else if (newValue > nonVirtualBar.getMax()) {
 498                     nonVirtualBar.setValue(nonVirtualBar.getMax());
 499                 } else {
 500                     nonVirtualBar.setValue(newValue);
 501 
 502                     // same as the last* comment above
 503                     if (isVertical()) lastX = e.getX();
 504                     else lastY = e.getY();
 505                 }
 506             }
 507         });
 508 
 509         /*
 510          * We place the scrollbars _above_ the rectangle, such that the drag
 511          * operations often used in conjunction with scrollbars aren't
 512          * misinterpreted as drag operations on the rectangle as well (which
 513          * would be the case if the scrollbars were underneath it as the
 514          * rectangle itself doesn't block the mouse.
 515          */
 516         // --- vbar
 517         vbar.setOrientation(Orientation.VERTICAL);
 518         vbar.addEventHandler(MouseEvent.ANY, event -> {
 519             event.consume();
 520         });
 521         getChildren().add(vbar);
 522 
 523         // --- hbar
 524         hbar.setOrientation(Orientation.HORIZONTAL);
 525         hbar.addEventHandler(MouseEvent.ANY, event -> {
 526             event.consume();
 527         });
 528         getChildren().add(hbar);
 529 
 530         // --- corner
 531         corner = new StackPane();
 532         corner.getStyleClass().setAll("corner");
 533         getChildren().add(corner);
 534 
 535 
 536 
 537         // initBinds
 538         // clipView binds
 539         InvalidationListener listenerX = valueModel -> {
 540             updateHbar();
 541         };
 542         verticalProperty().addListener(listenerX);
 543         hbar.valueProperty().addListener(listenerX);
 544         hbar.visibleProperty().addListener(listenerX);
 545 
 546 //        ChangeListener listenerY = new ChangeListener() {
 547 //            @Override public void handle(Bean bean, PropertyReference property) {
 548 //                clipView.setClipY(isVertical() ? 0 : vbar.getValue());
 549 //            }
 550 //        };
 551 //        addChangedListener(VERTICAL, listenerY);
 552 //        vbar.addChangedListener(ScrollBar.VALUE, listenerY);
 553 
 554         ChangeListener<Number> listenerY = (ov, t, t1) -> {
 555             clipView.setClipY(isVertical() ? 0 : vbar.getValue());
 556         };
 557         vbar.valueProperty().addListener(listenerY);
 558 
 559         super.heightProperty().addListener((observable, oldHeight, newHeight) -> {
 560             // Fix for RT-8480, where the VirtualFlow does not show its content
 561             // after changing size to 0 and back.
 562             if (oldHeight.doubleValue() == 0 && newHeight.doubleValue() > 0) {
 563                 recreateCells();
 564             }
 565         });
 566 
 567 
 568         /*
 569         ** there are certain animations that need to know if the touch is
 570         ** happening.....
 571         */
 572         setOnTouchPressed(e -> {
 573             touchDetected = true;
 574             scrollBarOn();
 575         });
 576 
 577         setOnTouchReleased(e -> {
 578             touchDetected = false;
 579             startSBReleasedAnimation();
 580         });
 581 
 582         setImpl_traversalEngine(new ParentTraversalEngine(this, new Algorithm() {
 583 
 584             Node selectNextAfterIndex(int index, TraversalContext context) {
 585                 T nextCell;
 586                 while ((nextCell = getVisibleCell(++index)) != null) {
 587                     if (nextCell.isFocusTraversable()) {
 588                         return nextCell;
 589                     }
 590                     Node n = context.selectFirstInParent(nextCell);
 591                     if (n != null) {
 592                         return n;
 593                     }
 594                 }
 595                 return null;
 596             }
 597 
 598             Node selectPreviousBeforeIndex(int index, TraversalContext context) {
 599                 T prevCell;
 600                 while ((prevCell = getVisibleCell(--index)) != null) {
 601                     Node prev = context.selectLastInParent(prevCell);
 602                     if (prev != null) {
 603                         return prev;
 604                     }
 605                     if (prevCell.isFocusTraversable()) {
 606                         return prevCell;
 607                     }
 608                 }
 609                 return null;
 610             }
 611 
 612             @Override
 613             public Node select(Node owner, Direction dir, TraversalContext context) {
 614                 T cell;
 615                 if (cells.isEmpty()) return null;
 616                 if (cells.contains(owner)) {
 617                     cell = (T) owner;
 618                 } else {
 619                     cell = findOwnerCell(owner);
 620                     Node next = context.selectInSubtree(cell, owner, dir);
 621                     if (next != null) {
 622                         return next;
 623                     }
 624                     if (dir == Direction.NEXT) dir = Direction.NEXT_IN_LINE;
 625                 }
 626                 int cellIndex = cell.getIndex();
 627                 switch(dir) {
 628                     case PREVIOUS:
 629                         return selectPreviousBeforeIndex(cellIndex, context);
 630                     case NEXT:
 631                         Node n = context.selectFirstInParent(cell);
 632                         if (n != null) {
 633                             return n;
 634                         }
 635                         // Intentional fall-through
 636                     case NEXT_IN_LINE:
 637                         return selectNextAfterIndex(cellIndex, context);
 638                 }
 639                 return null;
 640             }
 641 
 642             private T findOwnerCell(Node owner) {
 643                 Parent p = owner.getParent();
 644                 while (!cells.contains(p)) {
 645                     p = p.getParent();
 646                 }
 647                 return (T)p;
 648             }
 649 
 650             @Override
 651             public Node selectFirst(TraversalContext context) {
 652                 T firstCell = cells.getFirst();
 653                 if (firstCell == null) return null;
 654                 if (firstCell.isFocusTraversable()) return firstCell;
 655                 Node n = context.selectFirstInParent(firstCell);
 656                 if (n != null) {
 657                     return n;
 658                 }
 659                 return selectNextAfterIndex(firstCell.getIndex(), context);
 660             }
 661 
 662             @Override
 663             public Node selectLast(TraversalContext context) {
 664                 T lastCell = cells.getLast();
 665                 if (lastCell == null) return null;
 666                 Node p = context.selectLastInParent(lastCell);
 667                 if (p != null) {
 668                     return p;
 669                 }
 670                 if (lastCell.isFocusTraversable()) return lastCell;
 671                 return selectPreviousBeforeIndex(lastCell.getIndex(), context);
 672             }
 673         }));
 674     }
 675 
 676 
 677 
 678     /***************************************************************************
 679      *                                                                         *
 680      * Properties                                                              *
 681      *                                                                         *
 682      **************************************************************************/
 683 
 684     /**
 685      * There are two main complicating factors in the implementation of the
 686      * VirtualFlow, which are made even more complicated due to the performance
 687      * sensitive nature of this code. The first factor is the actual
 688      * virtualization mechanism, wired together with the PositionMapper.
 689      * The second complicating factor is the desire to do minimal layout
 690      * and minimal updates to CSS.
 691      *
 692      * Since the layout mechanism runs at most once per pulse, we want to hook
 693      * into this mechanism for minimal recomputation. Whenever a layout pass
 694      * is run we record the width/height that the virtual flow was last laid
 695      * out to. In subsequent passes, if the width/height has not changed then
 696      * we know we only have to rebuild the cells. If the width or height has
 697      * changed, then we can make appropriate decisions based on whether the
 698      * width / height has been reduced or expanded.
 699      *
 700      * In various places, if requestLayout is called it is generally just
 701      * used to indicate that some form of layout needs to happen (either the
 702      * entire thing has to be reconstructed, or just the cells need to be
 703      * reconstructed, generally).
 704      *
 705      * The accumCell is a special cell which is used in some computations
 706      * when an actual cell for that item isn't currently available. However,
 707      * the accumCell must be cleared whenever the cellFactory function is
 708      * changed because we need to use the cells that come from the new factory.
 709      *
 710      * In addition to storing the lastWidth and lastHeight, we also store the
 711      * number of cells that existed last time we performed a layout. In this
 712      * way if the number of cells change, we can request a layout and when it
 713      * occurs we can tell that the number of cells has changed and react
 714      * accordingly.
 715      *
 716      * Because the VirtualFlow can be laid out horizontally or vertically a
 717      * naming problem is present when trying to conceptualize and implement
 718      * the flow. In particular, the words "width" and "height" are not
 719      * precise when describing the unit of measure along the "virtualized"
 720      * axis and the "orthogonal" axis. For example, the height of a cell when
 721      * the flow is vertical is the magnitude along the "virtualized axis",
 722      * and the width is along the axis orthogonal to it.
 723      *
 724      * Since "height" and "width" are not reliable terms, we use the words
 725      * "length" and "breadth" to describe the magnitude of a cell along
 726      * the virtualized axis and orthogonal axis. For example, in a vertical
 727      * flow, the height=length and the width=breadth. In a horizontal axis,
 728      * the height=breadth and the width=length.
 729      *
 730      * These terms are somewhat arbitrary, but chosen so that when reading
 731      * most of the below code you can think in just one dimension, with
 732      * helper functions converting width/height in to length/breadth, while
 733      * also being different from width/height so as not to get confused with
 734      * the actual width/height of a cell.
 735      */
 736 
 737     // --- vertical
 738     /**
 739      * Indicates the primary direction of virtualization. If true, then the
 740      * primary direction of virtualization is vertical, meaning that cells will
 741      * stack vertically on top of each other. If false, then they will stack
 742      * horizontally next to each other.
 743      */
 744     private BooleanProperty vertical;
 745     public final void setVertical(boolean value) {
 746         verticalProperty().set(value);
 747     }
 748 
 749     public final boolean isVertical() {
 750         return vertical == null ? true : vertical.get();
 751     }
 752 
 753     public final BooleanProperty verticalProperty() {
 754         if (vertical == null) {
 755             vertical = new BooleanPropertyBase(true) {
 756                 @Override protected void invalidated() {
 757                     pile.clear();
 758                     sheetChildren.clear();
 759                     cells.clear();
 760                     lastWidth = lastHeight = -1;
 761                     setMaxPrefBreadth(-1);
 762                     setViewportBreadth(0);
 763                     setViewportLength(0);
 764                     lastPosition = 0;
 765                     hbar.setValue(0);
 766                     vbar.setValue(0);
 767                     setPosition(0.0f);
 768                     setNeedsLayout(true);
 769                     requestLayout();
 770                 }
 771 
 772                 @Override
 773                 public Object getBean() {
 774                     return VirtualFlow.this;
 775                 }
 776 
 777                 @Override
 778                 public String getName() {
 779                     return "vertical";
 780                 }
 781             };
 782         }
 783         return vertical;
 784     }
 785 
 786     // --- pannable
 787     /**
 788      * Indicates whether the VirtualFlow viewport is capable of being panned
 789      * by the user (either via the mouse or touch events).
 790      */
 791     private BooleanProperty pannable = new SimpleBooleanProperty(this, "pannable", true);
 792     public final boolean isPannable() { return pannable.get(); }
 793     public final void setPannable(boolean value) { pannable.set(value); }
 794     public final BooleanProperty pannableProperty() { return pannable; }
 795 
 796     // --- cell count
 797     /**
 798      * Indicates the number of cells that should be in the flow. The user of
 799      * the VirtualFlow must set this appropriately. When the cell count changes
 800      * the VirtualFlow responds by updating the visuals. If the items backing
 801      * the cells change, but the count has not changed, you must call the
 802      * reconfigureCells() function to update the visuals.
 803      */
 804     private IntegerProperty cellCount = new SimpleIntegerProperty(this, "cellCount", 0) {
 805         private int oldCount = 0;
 806 
 807         @Override protected void invalidated() {
 808             int cellCount = get();
 809 
 810             boolean countChanged = oldCount != cellCount;
 811             oldCount = cellCount;
 812 
 813             // ensure that the virtual scrollbar adjusts in size based on the current
 814             // cell count.
 815             if (countChanged) {
 816                 VirtualScrollBar lengthBar = isVertical() ? vbar : hbar;
 817                 lengthBar.setMax(cellCount);
 818             }
 819 
 820             // I decided *not* to reset maxPrefBreadth here for the following
 821             // situation. Suppose I have 30 cells and then I add 10 more. Just
 822             // because I added 10 more doesn't mean the max pref should be
 823             // reset. Suppose the first 3 cells were extra long, and I was
 824             // scrolled down such that they weren't visible. If I were to reset
 825             // maxPrefBreadth when subsequent cells were added or removed, then the
 826             // scroll bars would erroneously reset as well. So I do not reset
 827             // the maxPrefBreadth here.
 828 
 829             // Fix for RT-12512, RT-14301 and RT-14864.
 830             // Without this, the VirtualFlow length-wise scrollbar would not change
 831             // as expected. This would leave items unable to be shown, as they
 832             // would exist outside of the visible area, even when the scrollbar
 833             // was at its maximum position.
 834             // FIXME this should be only executed on the pulse, so this will likely
 835             // lead to performance degradation until it is handled properly.
 836             if (countChanged) {
 837                 layoutChildren();
 838 
 839                 // Fix for RT-13965: Without this line of code, the number of items in
 840                 // the sheet would constantly grow, leaking memory for the life of the
 841                 // application. This was especially apparent when the total number of
 842                 // cells changes - regardless of whether it became bigger or smaller.
 843                 sheetChildren.clear();
 844 
 845                 Parent parent = getParent();
 846                 if (parent != null) parent.requestLayout();
 847             }
 848             // TODO suppose I had 100 cells and I added 100 more. Further
 849             // suppose I was scrolled to the bottom when that happened. I
 850             // actually want to update the position of the mapper such that
 851             // the view remains "stable".
 852         }
 853     };
 854     public final int getCellCount() { return cellCount.get(); }
 855     public final void setCellCount(int value) { cellCount.set(value);  }
 856     public final IntegerProperty cellCountProperty() { return cellCount; }
 857 
 858 
 859     // --- position
 860     /**
 861      * The position of the VirtualFlow within its list of cells. This is a value
 862      * between 0 and 1.
 863      */
 864     private DoubleProperty position = new SimpleDoubleProperty(this, "position") {
 865         @Override public void setValue(Number v) {
 866             super.setValue(com.sun.javafx.util.Utils.clamp(0, get(), 1));
 867         }
 868 
 869         @Override protected void invalidated() {
 870             super.invalidated();
 871             requestLayout();
 872         }
 873     };
 874     public final double getPosition() { return position.get(); }
 875     public final void setPosition(double value) { position.set(value); }
 876     public final DoubleProperty positionProperty() { return position; }
 877 
 878     // --- fixed cell size
 879     /**
 880      * For optimisation purposes, some use cases can trade dynamic cell length
 881      * for speed - if fixedCellSize is greater than zero we'll use that rather
 882      * than determine it by querying the cell itself.
 883      */
 884     private DoubleProperty fixedCellSize = new SimpleDoubleProperty(this, "fixedCellSize") {
 885         @Override protected void invalidated() {
 886             fixedCellSizeEnabled = get() > 0;
 887             needsCellsLayout = true;
 888             layoutChildren();
 889         }
 890     };
 891     public final void setFixedCellSize(final double value) { fixedCellSize.set(value); }
 892     public final double getFixedCellSize() { return fixedCellSize.get(); }
 893     public final DoubleProperty fixedCellSizeProperty() { return fixedCellSize; }
 894 
 895 
 896     // --- Cell Factory
 897     private ObjectProperty<Callback<VirtualFlow<T>, T>> cellFactory;
 898 
 899     /**
 900      * Sets a new cell factory to use in the VirtualFlow. This forces all old
 901      * cells to be thrown away, and new cells to be created with
 902      * the new cell factory.
 903      */
 904     public final void setCellFactory(Callback<VirtualFlow<T>, T> value) {
 905         cellFactoryProperty().set(value);
 906     }
 907 
 908     /**
 909      * Returns the current cell factory.
 910      */
 911     public final Callback<VirtualFlow<T>, T> getCellFactory() {
 912         return cellFactory == null ? null : cellFactory.get();
 913     }
 914 
 915     /**
 916      * <p>Setting a custom cell factory has the effect of deferring all cell
 917      * creation, allowing for total customization of the cell. Internally, the
 918      * VirtualFlow is responsible for reusing cells - all that is necessary
 919      * is for the custom cell factory to return from this function a cell
 920      * which might be usable for representing any item in the VirtualFlow.
 921      *
 922      * <p>Refer to the {@link Cell} class documentation for more detail.
 923      */
 924     public final ObjectProperty<Callback<VirtualFlow<T>, T>> cellFactoryProperty() {
 925         if (cellFactory == null) {
 926             cellFactory = new SimpleObjectProperty<Callback<VirtualFlow<T>, T>>(this, "cellFactory") {
 927                 @Override protected void invalidated() {
 928                     if (get() != null) {
 929                         accumCell = null;
 930                         setNeedsLayout(true);
 931                         recreateCells();
 932                         if (getParent() != null) getParent().requestLayout();
 933                     }
 934                 }
 935             };
 936         }
 937         return cellFactory;
 938     }
 939 
 940 
 941 
 942     /***************************************************************************
 943      *                                                                         *
 944      * Public API                                                              *
 945      *                                                                         *
 946      **************************************************************************/
 947 
 948     /**
 949      * Overridden to implement somewhat more efficient support for layout. The
 950      * VirtualFlow can generally be considered as being unmanaged, in that
 951      * whenever the position changes, or other such things change, we need
 952      * to perform a layout but there is no reason to notify the parent. However
 953      * when things change which may impact the preferred size (such as
 954      * vertical, createCell, and configCell) then we need to notify the
 955      * parent.
 956      */
 957     @Override public void requestLayout() {
 958         // isNeedsLayout() is commented out due to RT-21417. This does not
 959         // appear to impact performance (indeed, it may help), and resolves the
 960         // issue identified in RT-21417.
 961         setNeedsLayout(true);
 962     }
 963 
 964     /** {@inheritDoc} */
 965     @Override protected void layoutChildren() {
 966         if (needsRecreateCells) {
 967             lastWidth = -1;
 968             lastHeight = -1;
 969             releaseCell(accumCell);
 970 //            accumCell = null;
 971 //            accumCellParent.getChildren().clear();
 972             sheet.getChildren().clear();
 973             for (int i = 0, max = cells.size(); i < max; i++) {
 974                 cells.get(i).updateIndex(-1);
 975             }
 976             cells.clear();
 977             pile.clear();
 978             releaseAllPrivateCells();
 979         } else if (needsRebuildCells) {
 980             lastWidth = -1;
 981             lastHeight = -1;
 982             releaseCell(accumCell);
 983             for (int i=0; i<cells.size(); i++) {
 984                 cells.get(i).updateIndex(-1);
 985             }
 986             addAllToPile();
 987             releaseAllPrivateCells();
 988         } else if (needsReconfigureCells) {
 989             setMaxPrefBreadth(-1);
 990             lastWidth = -1;
 991             lastHeight = -1;
 992         }
 993 
 994         if (! dirtyCells.isEmpty()) {
 995             int index;
 996             final int cellsSize = cells.size();
 997             while ((index = dirtyCells.nextSetBit(0)) != -1 && index < cellsSize) {
 998                 T cell = cells.get(index);
 999                 // updateIndex(-1) works for TableView, but breaks ListView.
1000                 // For now, the TableView just does not use the dirtyCells API
1001 //                cell.updateIndex(-1);
1002                 if (cell != null) {
1003                     cell.requestLayout();
1004                 }
1005                 dirtyCells.clear(index);
1006             }
1007 
1008             setMaxPrefBreadth(-1);
1009             lastWidth = -1;
1010             lastHeight = -1;
1011         }
1012 
1013         final boolean hasSizeChange = sizeChanged;
1014         boolean recreatedOrRebuilt = needsRebuildCells || needsRecreateCells || sizeChanged;
1015 
1016         needsRecreateCells = false;
1017         needsReconfigureCells = false;
1018         needsRebuildCells = false;
1019         sizeChanged = false;
1020 
1021         if (needsCellsLayout) {
1022             for (int i = 0, max = cells.size(); i < max; i++) {
1023                 Cell<?> cell = cells.get(i);
1024                 if (cell != null) {
1025                     cell.requestLayout();
1026                 }
1027             }
1028             needsCellsLayout = false;
1029 
1030             // yes, we return here - if needsCellsLayout was set to true, we
1031             // only did it to do the above - not rerun the entire layout.
1032             return;
1033         }
1034 
1035         final double width = getWidth();
1036         final double height = getHeight();
1037         final boolean isVertical = isVertical();
1038         final double position = getPosition();
1039 
1040         // if the width and/or height is 0, then there is no point doing
1041         // any of this work. In particular, this can happen during startup
1042         if (width <= 0 || height <= 0) {
1043             addAllToPile();
1044             lastWidth = width;
1045             lastHeight = height;
1046             hbar.setVisible(false);
1047             vbar.setVisible(false);
1048             corner.setVisible(false);
1049             return;
1050         }
1051 
1052         // we check if any of the cells in the cells list need layout. This is a
1053         // sign that they are perhaps animating their sizes. Without this check,
1054         // we may not perform a layout here, meaning that the cell will likely
1055         // 'jump' (in height normally) when the user drags the virtual thumb as
1056         // that is the first time the layout would occur otherwise.
1057         boolean cellNeedsLayout = false;
1058         boolean thumbNeedsLayout = false;
1059 
1060         if (Properties.IS_TOUCH_SUPPORTED) {
1061             if ((tempVisibility == true && (hbar.isVisible() == false || vbar.isVisible() == false)) ||
1062                 (tempVisibility == false && (hbar.isVisible() == true || vbar.isVisible() == true))) {
1063                 thumbNeedsLayout = true;
1064             }
1065         }
1066 
1067         if (!cellNeedsLayout) {
1068             for (int i = 0; i < cells.size(); i++) {
1069                 Cell<?> cell = cells.get(i);
1070                 cellNeedsLayout = cell.isNeedsLayout();
1071                 if (cellNeedsLayout) break;
1072             }
1073         }
1074 
1075         final int cellCount = getCellCount();
1076         final T firstCell = getFirstVisibleCell();
1077 
1078         // If no cells need layout, we check other criteria to see if this
1079         // layout call is even necessary. If it is found that no layout is
1080         // needed, we just punt.
1081         if (! cellNeedsLayout && !thumbNeedsLayout) {
1082             boolean cellSizeChanged = false;
1083             if (firstCell != null) {
1084                 double breadth = getCellBreadth(firstCell);
1085                 double length = getCellLength(firstCell);
1086                 cellSizeChanged = (breadth != lastCellBreadth) || (length != lastCellLength);
1087                 lastCellBreadth = breadth;
1088                 lastCellLength = length;
1089             }
1090 
1091             if (width == lastWidth &&
1092                 height == lastHeight &&
1093                 cellCount == lastCellCount &&
1094                 isVertical == lastVertical &&
1095                 position == lastPosition &&
1096                 ! cellSizeChanged)
1097             {
1098                 // TODO this happens to work around the problem tested by
1099                 // testCellLayout_LayoutWithoutChangingThingsUsesCellsInSameOrderAsBefore
1100                 // but isn't a proper solution. Really what we need to do is, when
1101                 // laying out cells, we need to make sure that if a cell is pressed
1102                 // AND we are doing a full rebuild then we need to make sure we
1103                 // use that cell in the same physical location as before so that
1104                 // it gets the mouse release event.
1105                 return;
1106             }
1107         }
1108 
1109         /*
1110          * This function may get called under a variety of circumstances.
1111          * It will determine what has changed from the last time it was laid
1112          * out, and will then take one of several execution paths based on
1113          * what has changed so as to perform minimal layout work and also to
1114          * give the expected behavior. One or more of the following may have
1115          * happened:
1116          *
1117          *  1) width/height has changed
1118          *      - If the width and/or height has been reduced (but neither of
1119          *        them has been expanded), then we simply have to reposition and
1120          *        resize the scroll bars
1121          *      - If the width (in the vertical case) has expanded, then we
1122          *        need to resize the existing cells and reposition and resize
1123          *        the scroll bars
1124          *      - If the height (in the vertical case) has expanded, then we
1125          *        need to resize and reposition the scroll bars and add
1126          *        any trailing cells
1127          *
1128          *  2) cell count has changed
1129          *      - If the number of cells is bigger, or it is smaller but not
1130          *        so small as to move the position then we can just update the
1131          *        cells in place without performing layout and update the
1132          *        scroll bars.
1133          *      - If the number of cells has been reduced and it affects the
1134          *        position, then move the position and rebuild all the cells
1135          *        and update the scroll bars
1136          *
1137          *  3) size of the cell has changed
1138          *      - If the size changed in the virtual direction (ie: height
1139          *        in the case of vertical) then layout the cells, adding
1140          *        trailing cells as necessary and updating the scroll bars
1141          *      - If the size changed in the non virtual direction (ie: width
1142          *        in the case of vertical) then simply adjust the widths of
1143          *        the cells as appropriate and adjust the scroll bars
1144          *
1145          *  4) vertical changed, cells is empty, maxPrefBreadth == -1, etc
1146          *      - Full rebuild.
1147          *
1148          * Each of the conditions really resolves to several of a handful of
1149          * possible outcomes:
1150          *  a) reposition & rebuild scroll bars
1151          *  b) resize cells in non-virtual direction
1152          *  c) add trailing cells
1153          *  d) update cells
1154          *  e) resize cells in the virtual direction
1155          *  f) all of the above
1156          *
1157          * So this function first determines what outcomes need to occur, and
1158          * then will execute all the ones that really need to happen. Every code
1159          * path ends up touching the "reposition & rebuild scroll bars" outcome,
1160          * so that one will be executed every time.
1161          */
1162         boolean needTrailingCells = false;
1163         boolean rebuild = cellNeedsLayout  ||
1164                 isVertical != lastVertical ||
1165                 cells.isEmpty()            ||
1166                 getMaxPrefBreadth() == -1  ||
1167                 position != lastPosition   ||
1168                 cellCount != lastCellCount ||
1169                 hasSizeChange ||
1170                 (isVertical && height < lastHeight) || (! isVertical && width < lastWidth);
1171 
1172         if (!rebuild) {
1173             // Check if maxPrefBreadth didn't change
1174             double maxPrefBreadth = getMaxPrefBreadth();
1175             boolean foundMax = false;
1176             for (int i = 0; i < cells.size(); ++i) {
1177                 double breadth = getCellBreadth(cells.get(i));
1178                 if (maxPrefBreadth == breadth) {
1179                     foundMax = true;
1180                 } else if (breadth > maxPrefBreadth) {
1181                     rebuild = true;
1182                     break;
1183                 }
1184             }
1185             if (!foundMax) { // All values were lower
1186                 rebuild = true;
1187             }
1188         }
1189 
1190         if (! rebuild) {
1191             if ((isVertical && height > lastHeight) || (! isVertical && width > lastWidth)) {
1192                 // resized in the virtual direction
1193                 needTrailingCells = true;
1194             }
1195         }
1196 
1197         initViewport();
1198 
1199         // Get the index of the "current" cell
1200         int currentIndex = computeCurrentIndex();
1201         if (lastCellCount != cellCount) {
1202             // The cell count has changed. We want to keep the viewport
1203             // stable if possible. If position was 0 or 1, we want to keep
1204             // the position in the same place. If the new cell count is >=
1205             // the currentIndex, then we will adjust the position to be 1.
1206             // Otherwise, our goal is to leave the index of the cell at the
1207             // top consistent, with the same translation etc.
1208             if (position == 0 || position == 1) {
1209                 // Update the item count
1210 //                setItemCount(cellCount);
1211             } else if (currentIndex >= cellCount) {
1212                 setPosition(1.0f);
1213 //                setItemCount(cellCount);
1214             } else if (firstCell != null) {
1215                 double firstCellOffset = getCellPosition(firstCell);
1216                 int firstCellIndex = getCellIndex(firstCell);
1217 //                setItemCount(cellCount);
1218                 adjustPositionToIndex(firstCellIndex);
1219                 double viewportTopToCellTop = -computeOffsetForCell(firstCellIndex);
1220                 adjustByPixelAmount(viewportTopToCellTop - firstCellOffset);
1221             }
1222 
1223             // Update the current index
1224             currentIndex = computeCurrentIndex();
1225         }
1226 
1227         if (rebuild) {
1228             setMaxPrefBreadth(-1);
1229             // Start by dumping all the cells into the pile
1230             addAllToPile();
1231 
1232             // The distance from the top of the viewport to the top of the
1233             // cell for the current index.
1234             double offset = -computeViewportOffset(getPosition());
1235 
1236             // Add all the leading and trailing cells (the call to add leading
1237             // cells will add the current cell as well -- that is, the one that
1238             // represents the current position on the mapper).
1239             addLeadingCells(currentIndex, offset);
1240 
1241             // Force filling of space with empty cells if necessary
1242             addTrailingCells(true);
1243         } else if (needTrailingCells) {
1244             addTrailingCells(true);
1245         }
1246 
1247         computeBarVisiblity();
1248         updateScrollBarsAndCells(recreatedOrRebuilt);
1249 
1250         lastWidth = getWidth();
1251         lastHeight = getHeight();
1252         lastCellCount = getCellCount();
1253         lastVertical = isVertical();
1254         lastPosition = getPosition();
1255 
1256         cleanPile();
1257     }
1258 
1259     /** {@inheritDoc} */
1260     @Override protected void setWidth(double value) {
1261         if (value != lastWidth) {
1262             super.setWidth(value);
1263             sizeChanged = true;
1264             setNeedsLayout(true);
1265             requestLayout();
1266         }
1267     }
1268 
1269     /** {@inheritDoc} */
1270     @Override protected void setHeight(double value) {
1271         if (value != lastHeight) {
1272             super.setHeight(value);
1273             sizeChanged = true;
1274             setNeedsLayout(true);
1275             requestLayout();
1276         }
1277     }
1278 
1279     /**
1280      * Get a cell which can be used in the layout. This function will reuse
1281      * cells from the pile where possible, and will create new cells when
1282      * necessary.
1283      */
1284     protected T getAvailableCell(int prefIndex) {
1285         T cell = null;
1286 
1287         // Fix for RT-12822. We try to retrieve the cell from the pile rather
1288         // than just grab a random cell from the pile (or create another cell).
1289         for (int i = 0, max = pile.size(); i < max; i++) {
1290             T _cell = pile.get(i);
1291             assert _cell != null;
1292 
1293             if (getCellIndex(_cell) == prefIndex) {
1294                 cell = _cell;
1295                 pile.remove(i);
1296                 break;
1297             }
1298             cell = null;
1299         }
1300 
1301         if (cell == null) {
1302             if (pile.size() > 0) {
1303                 // we try to get a cell with an index that is the same even/odd
1304                 // as the prefIndex. This saves us from having to run so much
1305                 // css on the cell as it will not change from even to odd, or
1306                 // vice versa
1307                 final boolean prefIndexIsEven = (prefIndex & 1) == 0;
1308                 for (int i = 0, max = pile.size(); i < max; i++) {
1309                     final T c = pile.get(i);
1310                     final int cellIndex = getCellIndex(c);
1311 
1312                     if ((cellIndex & 1) == 0 && prefIndexIsEven) {
1313                         cell = c;
1314                         pile.remove(i);
1315                         break;
1316                     } else if ((cellIndex & 1) == 1 && ! prefIndexIsEven) {
1317                         cell = c;
1318                         pile.remove(i);
1319                         break;
1320                     }
1321                 }
1322 
1323                 if (cell == null) {
1324                     cell = pile.removeFirst();
1325                 }
1326             } else {
1327                 cell = getCellFactory().call(this);
1328                 cell.getProperties().put(NEW_CELL, null);
1329             }
1330         }
1331 
1332         if (cell.getParent() == null) {
1333             sheetChildren.add(cell);
1334         }
1335 
1336         return cell;
1337     }
1338 
1339     /**
1340      * This method will remove all cells from the VirtualFlow and remove them,
1341      * adding them to the 'pile' (that is, a place from where cells can be used
1342      * at a later date). This method is protected to allow subclasses to clean up
1343      * appropriately.
1344      */
1345     protected void addAllToPile() {
1346         for (int i = 0, max = cells.size(); i < max; i++) {
1347             addToPile(cells.removeFirst());
1348         }
1349     }
1350 
1351     /**
1352      * Gets a cell for the given index if the cell has been created and laid out.
1353      * "Visible" is a bit of a misnomer, the cell might not be visible in the
1354      * viewport (it may be clipped), but does distinguish between cells that
1355      * have been created and are in use vs. those that are in the pile or
1356      * not created.
1357      */
1358     public T getVisibleCell(int index) {
1359         if (cells.isEmpty()) return null;
1360 
1361         // check the last index
1362         T lastCell = cells.getLast();
1363         int lastIndex = getCellIndex(lastCell);
1364         if (index == lastIndex) return lastCell;
1365 
1366         // check the first index
1367         T firstCell = cells.getFirst();
1368         int firstIndex = getCellIndex(firstCell);
1369         if (index == firstIndex) return firstCell;
1370 
1371         // if index is > firstIndex and < lastIndex then we can get the index
1372         if (index > firstIndex && index < lastIndex) {
1373             T cell = cells.get(index - firstIndex);
1374             if (getCellIndex(cell) == index) return cell;
1375         }
1376 
1377         // there is no visible cell for the specified index
1378         return null;
1379     }
1380 
1381     /**
1382      * Locates and returns the last non-empty IndexedCell that is currently
1383      * partially or completely visible. This function may return null if there
1384      * are no cells, or if the viewport length is 0.
1385      */
1386     public T getLastVisibleCell() {
1387         if (cells.isEmpty() || getViewportLength() <= 0) return null;
1388 
1389         T cell;
1390         for (int i = cells.size() - 1; i >= 0; i--) {
1391             cell = cells.get(i);
1392             if (! cell.isEmpty()) {
1393                 return cell;
1394             }
1395         }
1396 
1397         return null;
1398     }
1399 
1400     /**
1401      * Locates and returns the first non-empty IndexedCell that is partially or
1402      * completely visible. This really only ever returns null if there are no
1403      * cells or the viewport length is 0.
1404      */
1405     public T getFirstVisibleCell() {
1406         if (cells.isEmpty() || getViewportLength() <= 0) return null;
1407         T cell = cells.getFirst();
1408         return cell.isEmpty() ? null : cell;
1409     }
1410 
1411     /**
1412      * Adjust the position of cells so that the specified cell
1413      * will be positioned at the start of the viewport. The given cell must
1414      * already be "live".
1415      */
1416     public void scrollToTop(T firstCell) {
1417         if (firstCell != null) {
1418             scrollPixels(getCellPosition(firstCell));
1419         }
1420     }
1421 
1422     /**
1423      * Adjust the position of cells so that the specified cell
1424      * will be positioned at the end of the viewport. The given cell must
1425      * already be "live".
1426      */
1427     public void scrollToBottom(T lastCell) {
1428         if (lastCell != null) {
1429             scrollPixels(getCellPosition(lastCell) + getCellLength(lastCell) - getViewportLength());
1430         }
1431     }
1432 
1433     /**
1434      * Adjusts the cells such that the selected cell will be fully visible in
1435      * the viewport (but only just).
1436      */
1437     public void scrollTo(T cell) {
1438         if (cell != null) {
1439             final double start = getCellPosition(cell);
1440             final double length = getCellLength(cell);
1441             final double end = start + length;
1442             final double viewportLength = getViewportLength();
1443 
1444             if (start < 0) {
1445                 scrollPixels(start);
1446             } else if (end > viewportLength) {
1447                 scrollPixels(end - viewportLength);
1448             }
1449         }
1450     }
1451 
1452     /**
1453      * Adjusts the cells such that the cell in the given index will be fully visible in
1454      * the viewport.
1455      */
1456     public void scrollTo(int index) {
1457         T cell = getVisibleCell(index);
1458         if (cell != null) {
1459             scrollTo(cell);
1460         } else {
1461             adjustPositionToIndex(index);
1462             addAllToPile();
1463             requestLayout();
1464         }
1465     }
1466 
1467     /**
1468      * Adjusts the cells such that the cell in the given index will be fully visible in
1469      * the viewport, and positioned at the very top of the viewport.
1470      */
1471     public void scrollToTop(int index) {
1472         boolean posSet = false;
1473 
1474         if (index >= getCellCount() - 1) {
1475             setPosition(1);
1476             posSet = true;
1477         } else if (index < 0) {
1478             setPosition(0);
1479             posSet = true;
1480         }
1481 
1482         if (! posSet) {
1483             adjustPositionToIndex(index);
1484             double offset = - computeOffsetForCell(index);
1485             adjustByPixelAmount(offset);
1486         }
1487 
1488         requestLayout();
1489     }
1490 
1491 //    //TODO We assume all the cell have the same length.  We will need to support
1492 //    // cells of different lengths.
1493 //    public void scrollToOffset(int offset) {
1494 //        scrollPixels(offset * getCellLength(0));
1495 //    }
1496 
1497     /**
1498      * Given a delta value representing a number of pixels, this method attempts
1499      * to move the VirtualFlow in the given direction (positive is down/right,
1500      * negative is up/left) the given number of pixels. It returns the number of
1501      * pixels actually moved.
1502      */
1503     public double scrollPixels(final double delta) {
1504         // Short cut this method for cases where nothing should be done
1505         if (delta == 0) return 0;
1506 
1507         final boolean isVertical = isVertical();
1508         if (((isVertical && (tempVisibility ? !needLengthBar : !vbar.isVisible())) ||
1509                 (! isVertical && (tempVisibility ? !needLengthBar : !hbar.isVisible())))) return 0;
1510 
1511         double pos = getPosition();
1512         if (pos == 0.0f && delta < 0) return 0;
1513         if (pos == 1.0f && delta > 0) return 0;
1514 
1515         adjustByPixelAmount(delta);
1516         if (pos == getPosition()) {
1517             // The pos hasn't changed, there's nothing to do. This is likely
1518             // to occur when we hit either extremity
1519             return 0;
1520         }
1521 
1522         // Now move stuff around. Translating by pixels fundamentally means
1523         // moving the cells by the delta. However, after having
1524         // done that, we need to go through the cells and see which cells,
1525         // after adding in the translation factor, now fall off the viewport.
1526         // Also, we need to add cells as appropriate to the end (or beginning,
1527         // depending on the direction of travel).
1528         //
1529         // One simplifying assumption (that had better be true!) is that we
1530         // will only make it this far in the function if the virtual scroll
1531         // bar is visible. Otherwise, we never will pixel scroll. So as we go,
1532         // if we find that the maxPrefBreadth exceeds the viewportBreadth,
1533         // then we will be sure to show the breadthBar and update it
1534         // accordingly.
1535         if (cells.size() > 0) {
1536             for (int i = 0; i < cells.size(); i++) {
1537                 T cell = cells.get(i);
1538                 assert cell != null;
1539                 positionCell(cell, getCellPosition(cell) - delta);
1540             }
1541 
1542             // Fix for RT-32908
1543             T firstCell = cells.getFirst();
1544             double layoutY = firstCell == null ? 0 : getCellPosition(firstCell);
1545             for (int i = 0; i < cells.size(); i++) {
1546                 T cell = cells.get(i);
1547                 assert cell != null;
1548                 double actualLayoutY = getCellPosition(cell);
1549                 if (actualLayoutY != layoutY) {
1550                     // we need to shift the cell to layoutY
1551                     positionCell(cell, layoutY);
1552                 }
1553 
1554                 layoutY += getCellLength(cell);
1555             }
1556             // end of fix for RT-32908
1557             cull();
1558             firstCell = cells.getFirst();
1559 
1560             // Add any necessary leading cells
1561             if (firstCell != null) {
1562                 int firstIndex = getCellIndex(firstCell);
1563                 double prevIndexSize = getCellLength(firstIndex - 1);
1564                 addLeadingCells(firstIndex - 1, getCellPosition(firstCell) - prevIndexSize);
1565             } else {
1566                 int currentIndex = computeCurrentIndex();
1567 
1568                 // The distance from the top of the viewport to the top of the
1569                 // cell for the current index.
1570                 double offset = -computeViewportOffset(getPosition());
1571 
1572                 // Add all the leading and trailing cells (the call to add leading
1573                 // cells will add the current cell as well -- that is, the one that
1574                 // represents the current position on the mapper).
1575                 addLeadingCells(currentIndex, offset);
1576             }
1577 
1578             // Starting at the tail of the list, loop adding cells until
1579             // all the space on the table is filled up. We want to make
1580             // sure that we DO NOT add empty trailing cells (since we are
1581             // in the full virtual case and so there are no trailing empty
1582             // cells).
1583             if (! addTrailingCells(false)) {
1584                 // Reached the end, but not enough cells to fill up to
1585                 // the end. So, remove the trailing empty space, and translate
1586                 // the cells down
1587                 final T lastCell = getLastVisibleCell();
1588                 final double lastCellSize = getCellLength(lastCell);
1589                 final double cellEnd = getCellPosition(lastCell) + lastCellSize;
1590                 final double viewportLength = getViewportLength();
1591 
1592                 if (cellEnd < viewportLength) {
1593                     // Reposition the nodes
1594                     double emptySize = viewportLength - cellEnd;
1595                     for (int i = 0; i < cells.size(); i++) {
1596                         T cell = cells.get(i);
1597                         positionCell(cell, getCellPosition(cell) + emptySize);
1598                     }
1599                     setPosition(1.0f);
1600                     // fill the leading empty space
1601                     firstCell = cells.getFirst();
1602                     int firstIndex = getCellIndex(firstCell);
1603                     double prevIndexSize = getCellLength(firstIndex - 1);
1604                     addLeadingCells(firstIndex - 1, getCellPosition(firstCell) - prevIndexSize);
1605                 }
1606             }
1607         }
1608 
1609         // Now throw away any cells that don't fit
1610         cull();
1611 
1612         // Finally, update the scroll bars
1613         updateScrollBarsAndCells(false);
1614         lastPosition = getPosition();
1615 
1616         // notify
1617         return delta; // TODO fake
1618     }
1619 
1620     /** {@inheritDoc} */
1621     @Override protected double computePrefWidth(double height) {
1622         double w = isVertical() ? getPrefBreadth(height) : getPrefLength();
1623         return w + vbar.prefWidth(-1);
1624     }
1625 
1626     /** {@inheritDoc} */
1627     @Override protected double computePrefHeight(double width) {
1628         double h = isVertical() ? getPrefLength() : getPrefBreadth(width);
1629         return h + hbar.prefHeight(-1);
1630     }
1631 
1632     /**
1633      * Return a cell for the given index. This may be called for any cell,
1634      * including beyond the range defined by cellCount, in which case an
1635      * empty cell will be returned. The returned value should not be stored for
1636      * any reason.
1637      */
1638     public T getCell(int index) {
1639         // If there are cells, then we will attempt to get an existing cell
1640         if (! cells.isEmpty()) {
1641             // First check the cells that have already been created and are
1642             // in use. If this call returns a value, then we can use it
1643             T cell = getVisibleCell(index);
1644             if (cell != null) return cell;
1645         }
1646 
1647         // check the pile
1648         for (int i = 0; i < pile.size(); i++) {
1649             T cell = pile.get(i);
1650             if (getCellIndex(cell) == index) {
1651                 // Note that we don't remove from the pile: if we do it leads
1652                 // to a severe performance decrease. This seems to be OK, as
1653                 // getCell() is only used for cell measurement purposes.
1654                 // pile.remove(i);
1655                 return cell;
1656             }
1657         }
1658 
1659         if (pile.size() > 0) {
1660             return pile.get(0);
1661         }
1662 
1663         // We need to use the accumCell and return that
1664         if (accumCell == null) {
1665             Callback<VirtualFlow<T>,T> cellFactory = getCellFactory();
1666             if (cellFactory != null) {
1667                 accumCell = cellFactory.call(this);
1668                 accumCell.getProperties().put(NEW_CELL, null);
1669                 accumCellParent.getChildren().setAll(accumCell);
1670 
1671                 // Note the screen reader will attempt to find all
1672                 // the items inside the view to calculate the item count.
1673                 // Having items under different parents (sheet and accumCellParent)
1674                 // leads the screen reader to compute wrong values.
1675                 // The regular scheme to provide items to the screen reader
1676                 // uses getPrivateCell(), which places the item in the sheet.
1677                 // The accumCell, and its children, should be ignored by the
1678                 // screen reader.
1679                 accumCell.setAccessibleRole(AccessibleRole.NODE);
1680                 accumCell.getChildrenUnmodifiable().addListener((Observable c) -> {
1681                     for (Node n : accumCell.getChildrenUnmodifiable()) {
1682                         n.setAccessibleRole(AccessibleRole.NODE);
1683                     }
1684                 });
1685             }
1686         }
1687         setCellIndex(accumCell, index);
1688         resizeCellSize(accumCell);
1689         return accumCell;
1690     }
1691 
1692     /**
1693      * The VirtualFlow uses this method to set a cells index (rather than calling
1694      * {@link IndexedCell#updateIndex(int)} directly), so it is a perfect place
1695      * for subclasses to override if this if of interest.
1696      *
1697      * @param cell The cell whose index will be updated.
1698      * @param index The new index for the cell.
1699      */
1700     protected void setCellIndex(T cell, int index) {
1701         assert cell != null;
1702 
1703         cell.updateIndex(index);
1704 
1705         // make sure the cell is sized correctly. This is important for both
1706         // general layout of cells in a VirtualFlow, but also in cases such as
1707         // RT-34333, where the sizes were being reported incorrectly to the
1708         // ComboBox popup.
1709         if ((cell.isNeedsLayout() && cell.getScene() != null) || cell.getProperties().containsKey(NEW_CELL)) {
1710             cell.applyCss();
1711             cell.getProperties().remove(NEW_CELL);
1712         }
1713     }
1714 
1715     /**
1716      * Return the index for a given cell. This allows subclasses to customise
1717      * how cell indices are retrieved.
1718      */
1719     protected int getCellIndex(T cell){
1720         return cell.getIndex();
1721     }
1722 
1723 
1724 
1725     /***************************************************************************
1726      *                                                                         *
1727      * Private implementation                                                  *
1728      *                                                                         *
1729      **************************************************************************/
1730 
1731     final VirtualScrollBar getHbar() {
1732         return hbar;
1733     }
1734     final VirtualScrollBar getVbar() {
1735         return vbar;
1736     }
1737 
1738     /**
1739      * The maximum preferred size in the non-virtual direction. For example,
1740      * if vertical, then this is the max pref width of all cells encountered.
1741      * <p>
1742      * In general, this is the largest preferred size in the non-virtual
1743      * direction that we have ever encountered. We don't reduce this size
1744      * unless instructed to do so, so as to reduce the amount of scroll bar
1745      * jitter. The access on this variable is package ONLY FOR TESTING.
1746      */
1747     private double maxPrefBreadth;
1748     private final void setMaxPrefBreadth(double value) {
1749         this.maxPrefBreadth = value;
1750     }
1751     final double getMaxPrefBreadth() {
1752         return maxPrefBreadth;
1753     }
1754 
1755     /**
1756      * The breadth of the viewport portion of the VirtualFlow as computed during
1757      * the layout pass. In a vertical flow this would be the same as the clip
1758      * view width. In a horizontal flow this is the clip view height.
1759      * The access on this variable is package ONLY FOR TESTING.
1760      */
1761     private double viewportBreadth;
1762     private final void setViewportBreadth(double value) {
1763         this.viewportBreadth = value;
1764     }
1765     private final double getViewportBreadth() {
1766         return viewportBreadth;
1767     }
1768 
1769     /**
1770      * The length of the viewport portion of the VirtualFlow as computed
1771      * during the layout pass. In a vertical flow this would be the same as the
1772      * clip view height. In a horizontal flow this is the clip view width.
1773      * The access on this variable is package ONLY FOR TESTING.
1774      */
1775     private double viewportLength;
1776     void setViewportLength(double value) {
1777         this.viewportLength = value;
1778     }
1779     double getViewportLength() {
1780         return viewportLength;
1781     }
1782 
1783     /**
1784      * Compute and return the length of the cell for the given index. This is
1785      * called both internally when adjusting by pixels, and also at times
1786      * by PositionMapper (see the getItemSize callback). When called by
1787      * PositionMapper, it is possible that it will be called for some index
1788      * which is not associated with any cell, so we have to do a bit of work
1789      * to use a cell as a helper for computing cell size in some cases.
1790      */
1791     double getCellLength(int index) {
1792         if (fixedCellSizeEnabled) return getFixedCellSize();
1793 
1794         T cell = getCell(index);
1795         double length = getCellLength(cell);
1796         releaseCell(cell);
1797         return length;
1798     }
1799 
1800     /**
1801      */
1802     double getCellBreadth(int index) {
1803         T cell = getCell(index);
1804         double b = getCellBreadth(cell);
1805         releaseCell(cell);
1806         return b;
1807     }
1808 
1809     /**
1810      * Gets the length of a specific cell
1811      */
1812     double getCellLength(T cell) {
1813         if (cell == null) return 0;
1814         if (fixedCellSizeEnabled) return getFixedCellSize();
1815 
1816         return isVertical() ?
1817                 cell.getLayoutBounds().getHeight()
1818                 : cell.getLayoutBounds().getWidth();
1819     }
1820 
1821     /**
1822      * Gets the breadth of a specific cell
1823      */
1824     double getCellBreadth(Cell cell) {
1825         return isVertical() ?
1826                 cell.prefWidth(-1)
1827                 : cell.prefHeight(-1);
1828     }
1829 
1830     /**
1831      * Gets the layout position of the cell along the length axis
1832      */
1833     double getCellPosition(T cell) {
1834         if (cell == null) return 0;
1835 
1836         return isVertical() ?
1837                 cell.getLayoutY()
1838                 : cell.getLayoutX();
1839     }
1840 
1841     private void positionCell(T cell, double position) {
1842         if (isVertical()) {
1843             cell.setLayoutX(0);
1844             cell.setLayoutY(snapSize(position));
1845         } else {
1846             cell.setLayoutX(snapSize(position));
1847             cell.setLayoutY(0);
1848         }
1849     }
1850 
1851     private void resizeCellSize(T cell) {
1852         if (cell == null) return;
1853 
1854         if (isVertical()) {
1855             double width = Math.max(getMaxPrefBreadth(), getViewportBreadth());
1856             cell.resize(width, fixedCellSizeEnabled ? getFixedCellSize() : Utils.boundedSize(cell.prefHeight(width), cell.minHeight(width), cell.maxHeight(width)));
1857         } else {
1858             double height = Math.max(getMaxPrefBreadth(), getViewportBreadth());
1859             cell.resize(fixedCellSizeEnabled ? getFixedCellSize() : Utils.boundedSize(cell.prefWidth(height), cell.minWidth(height), cell.maxWidth(height)), height);
1860         }
1861     }
1862 
1863     private List<T> getCells() {
1864         return cells;
1865     }
1866 
1867     // Returns last visible cell whose bounds are entirely within the viewport
1868     T getLastVisibleCellWithinViewPort() {
1869         if (cells.isEmpty() || getViewportLength() <= 0) return null;
1870 
1871         T cell;
1872         final double max = getViewportLength();
1873         for (int i = cells.size() - 1; i >= 0; i--) {
1874             cell = cells.get(i);
1875             if (cell.isEmpty()) continue;
1876 
1877             final double cellStart = getCellPosition(cell);
1878             final double cellEnd = cellStart + getCellLength(cell);
1879 
1880             // we use the magic +2 to allow for a little bit of fuzziness,
1881             // this is to help in situations such as RT-34407
1882             if (cellEnd <= (max + 2)) {
1883                 return cell;
1884             }
1885         }
1886 
1887         return null;
1888     }
1889 
1890     // Returns first visible cell whose bounds are entirely within the viewport
1891     T getFirstVisibleCellWithinViewPort() {
1892         if (cells.isEmpty() || getViewportLength() <= 0) return null;
1893 
1894         T cell;
1895         for (int i = 0; i < cells.size(); i++) {
1896             cell = cells.get(i);
1897             if (cell.isEmpty()) continue;
1898 
1899             final double cellStart = getCellPosition(cell);
1900             if (cellStart >= 0) {
1901                 return cell;
1902             }
1903         }
1904 
1905         return null;
1906     }
1907 
1908     /**
1909      * Adds all the cells prior to and including the given currentIndex, until
1910      * no more can be added without falling off the flow. The startOffset
1911      * indicates the distance from the leading edge (top) of the viewport to
1912      * the leading edge (top) of the currentIndex.
1913      */
1914     void addLeadingCells(int currentIndex, double startOffset) {
1915         // The offset will keep track of the distance from the top of the
1916         // viewport to the top of the current index. We will increment it
1917         // as we lay out leading cells.
1918         double offset = startOffset;
1919         // The index is the absolute index of the cell being laid out
1920         int index = currentIndex;
1921 
1922         // Offset should really be the bottom of the current index
1923         boolean first = true; // first time in, we just fudge the offset and let
1924         // it be the top of the current index then redefine
1925         // it as the bottom of the current index thereafter
1926         // while we have not yet laid out so many cells that they would fall
1927         // off the flow, we will continue to create and add cells. The
1928         // offset is our indication of whether we can lay out additional
1929         // cells. If the offset is ever < 0, except in the case of the very
1930         // first cell, then we must quit.
1931         T cell = null;
1932 
1933         // special case for the position == 1.0, skip adding last invisible cell
1934         if (index == getCellCount() && offset == getViewportLength()) {
1935             index--;
1936             first = false;
1937         }
1938         while (index >= 0 && (offset > 0 || first)) {
1939             cell = getAvailableCell(index);
1940             setCellIndex(cell, index);
1941             resizeCellSize(cell); // resize must be after config
1942             cells.addFirst(cell);
1943 
1944             // A little gross but better than alternatives because it reduces
1945             // the number of times we have to update a cell or compute its
1946             // size. The first time into this loop "offset" is actually the
1947             // top of the current index. On all subsequent visits, it is the
1948             // bottom of the current index.
1949             if (first) {
1950                 first = false;
1951             } else {
1952                 offset -= getCellLength(cell);
1953             }
1954 
1955             // Position the cell, and update the maxPrefBreadth variable as we go.
1956             positionCell(cell, offset);
1957             setMaxPrefBreadth(Math.max(getMaxPrefBreadth(), getCellBreadth(cell)));
1958             cell.setVisible(true);
1959             --index;
1960         }
1961 
1962         // There are times when after laying out the cells we discover that
1963         // the top of the first cell which represents index 0 is below the top
1964         // of the viewport. In these cases, we have to adjust the cells up
1965         // and reset the mapper position. This might happen when items got
1966         // removed at the top or when the viewport size increased.
1967         if (cells.size() > 0) {
1968             cell = cells.getFirst();
1969             int firstIndex = getCellIndex(cell);
1970             double firstCellPos = getCellPosition(cell);
1971             if (firstIndex == 0 && firstCellPos > 0) {
1972                 setPosition(0.0f);
1973                 offset = 0;
1974                 for (int i = 0; i < cells.size(); i++) {
1975                     cell = cells.get(i);
1976                     positionCell(cell, offset);
1977                     offset += getCellLength(cell);
1978                 }
1979             }
1980         } else {
1981             // reset scrollbar to top, so if the flow sees cells again it starts at the top
1982             vbar.setValue(0);
1983             hbar.setValue(0);
1984         }
1985     }
1986 
1987     /**
1988      * Adds all the trailing cells that come <em>after</em> the last index in
1989      * the cells ObservableList.
1990      */
1991     boolean addTrailingCells(boolean fillEmptyCells) {
1992         // If cells is empty then addLeadingCells bailed for some reason and
1993         // we're hosed, so just punt
1994         if (cells.isEmpty()) return false;
1995 
1996         // While we have not yet laid out so many cells that they would fall
1997         // off the flow, so we will continue to create and add cells. When the
1998         // offset becomes greater than the width/height of the flow, then we
1999         // know we cannot add any more cells.
2000         T startCell = cells.getLast();
2001         double offset = getCellPosition(startCell) + getCellLength(startCell);
2002         int index = getCellIndex(startCell) + 1;
2003         final int cellCount = getCellCount();
2004         boolean filledWithNonEmpty = index <= cellCount;
2005 
2006         final double viewportLength = getViewportLength();
2007 
2008         // Fix for RT-37421, which was a regression caused by RT-36556
2009         if (offset < 0 && !fillEmptyCells) {
2010             return false;
2011         }
2012 
2013         //
2014         // RT-36507: viewportLength - offset gives the maximum number of
2015         // additional cells that should ever be able to fit in the viewport if
2016         // every cell had a height of 1. If index ever exceeds this count,
2017         // then offset is not incrementing fast enough, or at all, which means
2018         // there is something wrong with the cell size calculation.
2019         //
2020         final double maxCellCount = viewportLength - offset;
2021         while (offset < viewportLength) {
2022             if (index >= cellCount) {
2023                 if (offset < viewportLength) filledWithNonEmpty = false;
2024                 if (! fillEmptyCells) return filledWithNonEmpty;
2025                 // RT-36507 - return if we've exceeded the maximum
2026                 if (index > maxCellCount) {
2027                     final PlatformLogger logger = Logging.getControlsLogger();
2028                     if (logger.isLoggable(PlatformLogger.Level.INFO)) {
2029                         if (startCell != null) {
2030                             logger.info("index exceeds maxCellCount. Check size calculations for " + startCell.getClass());
2031                         } else {
2032                             logger.info("index exceeds maxCellCount");
2033                         }
2034                     }
2035                     return filledWithNonEmpty;
2036                 }
2037             }
2038             T cell = getAvailableCell(index);
2039             setCellIndex(cell, index);
2040             resizeCellSize(cell); // resize happens after config!
2041             cells.addLast(cell);
2042 
2043             // Position the cell and update the max pref
2044             positionCell(cell, offset);
2045             setMaxPrefBreadth(Math.max(getMaxPrefBreadth(), getCellBreadth(cell)));
2046 
2047             offset += getCellLength(cell);
2048             cell.setVisible(true);
2049             ++index;
2050         }
2051 
2052         // Discover whether the first cell coincides with index #0. If after
2053         // adding all the trailing cells we find that a) the first cell was
2054         // not index #0 and b) there are trailing cells, then we have a
2055         // problem. We need to shift all the cells down and add leading cells,
2056         // one at a time, until either the very last non-empty cells is aligned
2057         // with the bottom OR we have laid out cell index #0 at the first
2058         // position.
2059         T firstCell = cells.getFirst();
2060         index = getCellIndex(firstCell);
2061         T lastNonEmptyCell = getLastVisibleCell();
2062         double start = getCellPosition(firstCell);
2063         double end = getCellPosition(lastNonEmptyCell) + getCellLength(lastNonEmptyCell);
2064         if ((index != 0 || (index == 0 && start < 0)) && fillEmptyCells &&
2065                 lastNonEmptyCell != null && getCellIndex(lastNonEmptyCell) == cellCount - 1 && end < viewportLength) {
2066 
2067             double prospectiveEnd = end;
2068             double distance = viewportLength - end;
2069             while (prospectiveEnd < viewportLength && index != 0 && (-start) < distance) {
2070                 index--;
2071                 T cell = getAvailableCell(index);
2072                 setCellIndex(cell, index);
2073                 resizeCellSize(cell); // resize must be after config
2074                 cells.addFirst(cell);
2075                 double cellLength = getCellLength(cell);
2076                 start -= cellLength;
2077                 prospectiveEnd += cellLength;
2078                 positionCell(cell, start);
2079                 setMaxPrefBreadth(Math.max(getMaxPrefBreadth(), getCellBreadth(cell)));
2080                 cell.setVisible(true);
2081             }
2082 
2083             // The amount by which to translate the cells down
2084             firstCell = cells.getFirst();
2085             start = getCellPosition(firstCell);
2086             double delta = viewportLength - end;
2087             if (getCellIndex(firstCell) == 0 && delta > (-start)) {
2088                 delta = (-start);
2089             }
2090             // Move things
2091             for (int i = 0; i < cells.size(); i++) {
2092                 T cell = cells.get(i);
2093                 positionCell(cell, getCellPosition(cell) + delta);
2094             }
2095 
2096             // Check whether the first cell, subsequent to our adjustments, is
2097             // now index #0 and aligned with the top. If so, change the position
2098             // to be at 0 instead of 1.
2099             start = getCellPosition(firstCell);
2100             if (getCellIndex(firstCell) == 0 && start == 0) {
2101                 setPosition(0);
2102             } else if (getPosition() != 1) {
2103                 setPosition(1);
2104             }
2105         }
2106 
2107         return filledWithNonEmpty;
2108     }
2109 
2110     void reconfigureCells() {
2111         needsReconfigureCells = true;
2112         requestLayout();
2113     }
2114 
2115     void recreateCells() {
2116         needsRecreateCells = true;
2117         requestLayout();
2118     }
2119 
2120     void rebuildCells() {
2121         needsRebuildCells = true;
2122         requestLayout();
2123     }
2124 
2125     void requestCellLayout() {
2126         needsCellsLayout = true;
2127         requestLayout();
2128     }
2129 
2130     void setCellDirty(int index) {
2131         dirtyCells.set(index);
2132         requestLayout();
2133     }
2134 
2135     private void startSBReleasedAnimation() {
2136         if (sbTouchTimeline == null) {
2137             /*
2138             ** timeline to leave the scrollbars visible for a short
2139             ** while after a scroll/drag
2140             */
2141             sbTouchTimeline = new Timeline();
2142             sbTouchKF1 = new KeyFrame(Duration.millis(0), event -> {
2143                 tempVisibility = true;
2144                 requestLayout();
2145             });
2146 
2147             sbTouchKF2 = new KeyFrame(Duration.millis(1000), event -> {
2148                 if (touchDetected == false && mouseDown == false) {
2149                     tempVisibility = false;
2150                     requestLayout();
2151                 }
2152             });
2153             sbTouchTimeline.getKeyFrames().addAll(sbTouchKF1, sbTouchKF2);
2154         }
2155         sbTouchTimeline.playFromStart();
2156     }
2157 
2158     private void scrollBarOn() {
2159         tempVisibility = true;
2160         requestLayout();
2161     }
2162 
2163     void updateHbar() {
2164         // Bring the clipView.clipX back to 0 if control is vertical or
2165         // the hbar isn't visible (fix for RT-11666)
2166         if (! isVisible() || getScene() == null) return;
2167 
2168         if (isVertical()) {
2169             if (hbar.isVisible()) {
2170                 clipView.setClipX(hbar.getValue());
2171             } else {
2172                 // all cells are now less than the width of the flow,
2173                 // so we should shift the hbar/clip such that
2174                 // everything is visible in the viewport.
2175                 clipView.setClipX(0);
2176                 hbar.setValue(0);
2177             }
2178         }
2179     }
2180 
2181     /**
2182      * @return true if bar visibility changed
2183      */
2184     private boolean computeBarVisiblity() {
2185         if (cells.isEmpty()) {
2186             // In case no cells are set yet, we assume no bars are needed
2187             needLengthBar = false;
2188             needBreadthBar = false;
2189             return true;
2190         }
2191 
2192         final boolean isVertical = isVertical();
2193         boolean barVisibilityChanged = false;
2194 
2195         VirtualScrollBar breadthBar = isVertical ? hbar : vbar;
2196         VirtualScrollBar lengthBar = isVertical ? vbar : hbar;
2197 
2198         final double viewportBreadth = getViewportBreadth();
2199 
2200         final int cellsSize = cells.size();
2201         final int cellCount = getCellCount();
2202         for (int i = 0; i < 2; i++) {
2203             final boolean lengthBarVisible = getPosition() > 0
2204                     || cellCount > cellsSize
2205                     || (cellCount == cellsSize && (getCellPosition(cells.getLast()) + getCellLength(cells.getLast())) > getViewportLength())
2206                     || (cellCount == cellsSize - 1 && barVisibilityChanged && needBreadthBar);
2207 
2208             if (lengthBarVisible ^ needLengthBar) {
2209                 needLengthBar = lengthBarVisible;
2210                 barVisibilityChanged = true;
2211             }
2212 
2213             // second conditional removed for RT-36669.
2214             final boolean breadthBarVisible = (maxPrefBreadth > viewportBreadth);// || (needLengthBar && maxPrefBreadth > (viewportBreadth - lengthBarBreadth));
2215             if (breadthBarVisible ^ needBreadthBar) {
2216                 needBreadthBar = breadthBarVisible;
2217                 barVisibilityChanged = true;
2218             }
2219         }
2220 
2221         // Start by optimistically deciding whether the length bar and
2222         // breadth bar are needed and adjust the viewport dimensions
2223         // accordingly. If during layout we find that one or the other of the
2224         // bars actually is needed, then we will perform a cleanup pass
2225 
2226         if (!Properties.IS_TOUCH_SUPPORTED) {
2227             updateViewportDimensions();
2228             breadthBar.setVisible(needBreadthBar);
2229             lengthBar.setVisible(needLengthBar);
2230         } else {
2231             breadthBar.setVisible(needBreadthBar && tempVisibility);
2232             lengthBar.setVisible(needLengthBar && tempVisibility);
2233         }
2234 
2235         return barVisibilityChanged;
2236     }
2237 
2238     private void updateViewportDimensions() {
2239         final boolean isVertical = isVertical();
2240         final double breadthBarLength = snapSize(isVertical ? hbar.prefHeight(-1) : vbar.prefWidth(-1));
2241         final double lengthBarBreadth = snapSize(isVertical ? vbar.prefWidth(-1) : hbar.prefHeight(-1));
2242 
2243         setViewportBreadth((isVertical ? getWidth() : getHeight()) - (needLengthBar ? lengthBarBreadth : 0));
2244         setViewportLength((isVertical ? getHeight() : getWidth()) - (needBreadthBar ? breadthBarLength : 0));
2245     }
2246 
2247     private void initViewport() {
2248         // Initialize the viewportLength and viewportBreadth to match the
2249         // width/height of the flow
2250         final boolean isVertical = isVertical();
2251 
2252         updateViewportDimensions();
2253 
2254         VirtualScrollBar breadthBar = isVertical ? hbar : vbar;
2255         VirtualScrollBar lengthBar = isVertical ? vbar : hbar;
2256 
2257         // If there has been a switch between the virtualized bar, then we
2258         // will want to do some stuff TODO.
2259         breadthBar.setVirtual(false);
2260         lengthBar.setVirtual(true);
2261     }
2262 
2263     private void updateScrollBarsAndCells(boolean recreate) {
2264         // Assign the hbar and vbar to the breadthBar and lengthBar so as
2265         // to make some subsequent calculations easier.
2266         final boolean isVertical = isVertical();
2267         VirtualScrollBar breadthBar = isVertical ? hbar : vbar;
2268         VirtualScrollBar lengthBar = isVertical ? vbar : hbar;
2269 
2270         // We may have adjusted the viewport length and breadth after the
2271         // layout due to scroll bars becoming visible. So we need to perform
2272         // a follow up pass and resize and shift all the cells to fit the
2273         // viewport. Note that the prospective viewport size is always >= the
2274         // final viewport size, so we don't have to worry about adding
2275         // cells during this cleanup phase.
2276         fitCells();
2277 
2278         // Update cell positions.
2279         // When rebuilding the cells, we add the cells and along the way compute
2280         // the maxPrefBreadth. Based on the computed value, we may add
2281         // the breadth scrollbar which changes viewport length, so we need
2282         // to re-position the cells.
2283         if (!cells.isEmpty()) {
2284             final double currOffset = -computeViewportOffset(getPosition());
2285             final int currIndex = computeCurrentIndex() - cells.getFirst().getIndex();
2286             final int size = cells.size();
2287 
2288             // position leading cells
2289             double offset = currOffset;
2290 
2291             for (int i = currIndex - 1; i >= 0 && i < size; i--) {
2292                 final T cell = cells.get(i);
2293 
2294                 offset -= getCellLength(cell);
2295 
2296                 positionCell(cell, offset);
2297             }
2298 
2299             // position trailing cells
2300             offset = currOffset;
2301             for (int i = currIndex; i >= 0 && i < size; i++) {
2302                 final T cell = cells.get(i);
2303                 positionCell(cell, offset);
2304 
2305                 offset += getCellLength(cell);
2306             }
2307         }
2308 
2309         // Toggle visibility on the corner
2310         corner.setVisible(breadthBar.isVisible() && lengthBar.isVisible());
2311 
2312         double sumCellLength = 0;
2313         double flowLength = (isVertical ? getHeight() : getWidth()) -
2314                 (breadthBar.isVisible() ? breadthBar.prefHeight(-1) : 0);
2315 
2316         final double viewportBreadth = getViewportBreadth();
2317         final double viewportLength = getViewportLength();
2318 
2319         // Now position and update the scroll bars
2320         if (breadthBar.isVisible()) {
2321             /*
2322             ** Positioning the ScrollBar
2323             */
2324             if (!Properties.IS_TOUCH_SUPPORTED) {
2325                 if (isVertical) {
2326                     hbar.resizeRelocate(0, viewportLength,
2327                             viewportBreadth, hbar.prefHeight(viewportBreadth));
2328                 } else {
2329                     vbar.resizeRelocate(viewportLength, 0,
2330                             vbar.prefWidth(viewportBreadth), viewportBreadth);
2331                 }
2332             }
2333             else {
2334                 if (isVertical) {
2335                     hbar.resizeRelocate(0, (viewportLength-hbar.getHeight()),
2336                             viewportBreadth, hbar.prefHeight(viewportBreadth));
2337                 } else {
2338                     vbar.resizeRelocate((viewportLength-vbar.getWidth()), 0,
2339                             vbar.prefWidth(viewportBreadth), viewportBreadth);
2340                 }
2341             }
2342 
2343             if (getMaxPrefBreadth() != -1) {
2344                 double newMax = Math.max(1, getMaxPrefBreadth() - viewportBreadth);
2345                 if (newMax != breadthBar.getMax()) {
2346                     breadthBar.setMax(newMax);
2347 
2348                     double breadthBarValue = breadthBar.getValue();
2349                     boolean maxed = breadthBarValue != 0 && newMax == breadthBarValue;
2350                     if (maxed || breadthBarValue > newMax) {
2351                         breadthBar.setValue(newMax);
2352                     }
2353 
2354                     breadthBar.setVisibleAmount((viewportBreadth / getMaxPrefBreadth()) * newMax);
2355                 }
2356             }
2357         }
2358 
2359         // determine how many cells there are on screen so that the scrollbar
2360         // thumb can be appropriately sized
2361         if (recreate && (lengthBar.isVisible() || Properties.IS_TOUCH_SUPPORTED)) {
2362             final int cellCount = getCellCount();
2363             int numCellsVisibleOnScreen = 0;
2364             for (int i = 0, max = cells.size(); i < max; i++) {
2365                 T cell = cells.get(i);
2366                 if (cell != null && !cell.isEmpty()) {
2367                     sumCellLength += (isVertical ? cell.getHeight() : cell.getWidth());
2368                     if (sumCellLength > flowLength) {
2369                         break;
2370                     }
2371 
2372                     numCellsVisibleOnScreen++;
2373                 }
2374             }
2375 
2376             lengthBar.setMax(1);
2377             if (numCellsVisibleOnScreen == 0 && cellCount == 1) {
2378                 // special case to help resolve RT-17701 and the case where we have
2379                 // only a single row and it is bigger than the viewport
2380                 lengthBar.setVisibleAmount(flowLength / sumCellLength);
2381             } else {
2382                 lengthBar.setVisibleAmount(numCellsVisibleOnScreen / (float) cellCount);
2383             }
2384         }
2385 
2386         if (lengthBar.isVisible()) {
2387             // Fix for RT-11873. If this isn't here, we can have a situation where
2388             // the scrollbar scrolls endlessly. This is possible when the cell
2389             // count grows as the user hits the maximal position on the scrollbar
2390             // (i.e. the list size dynamically grows as the user needs more).
2391             //
2392             // This code was commented out to resolve RT-14477 after testing
2393             // whether RT-11873 can be recreated. It could not, and therefore
2394             // for now this code will remained uncommented until it is deleted
2395             // following further testing.
2396 //            if (lengthBar.getValue() == 1.0 && lastCellCount != cellCount) {
2397 //                lengthBar.setValue(0.99);
2398 //            }
2399 
2400             /*
2401             ** Positioning the ScrollBar
2402             */
2403             if (!Properties.IS_TOUCH_SUPPORTED) {
2404                 if (isVertical) {
2405                     vbar.resizeRelocate(viewportBreadth, 0, vbar.prefWidth(viewportLength), viewportLength);
2406                 } else {
2407                     hbar.resizeRelocate(0, viewportBreadth, viewportLength, hbar.prefHeight(-1));
2408                 }
2409             }
2410             else {
2411                 if (isVertical) {
2412                     vbar.resizeRelocate((viewportBreadth-vbar.getWidth()), 0, vbar.prefWidth(viewportLength), viewportLength);
2413                 } else {
2414                     hbar.resizeRelocate(0, (viewportBreadth-hbar.getHeight()), viewportLength, hbar.prefHeight(-1));
2415                 }
2416             }
2417         }
2418 
2419         if (corner.isVisible()) {
2420             if (!Properties.IS_TOUCH_SUPPORTED) {
2421                 corner.resize(vbar.getWidth(), hbar.getHeight());
2422                 corner.relocate(hbar.getLayoutX() + hbar.getWidth(), vbar.getLayoutY() + vbar.getHeight());
2423             }
2424             else {
2425                 corner.resize(vbar.getWidth(), hbar.getHeight());
2426                 corner.relocate(hbar.getLayoutX() + (hbar.getWidth()-vbar.getWidth()), vbar.getLayoutY() + (vbar.getHeight()-hbar.getHeight()));
2427                 hbar.resize(hbar.getWidth()-vbar.getWidth(), hbar.getHeight());
2428                 vbar.resize(vbar.getWidth(), vbar.getHeight()-hbar.getHeight());
2429             }
2430         }
2431 
2432         clipView.resize(snapSize(isVertical ? viewportBreadth : viewportLength),
2433                 snapSize(isVertical ? viewportLength : viewportBreadth));
2434 
2435         // If the viewportLength becomes large enough that all cells fit
2436         // within the viewport, then we want to update the value to match.
2437         if (getPosition() != lengthBar.getValue()) {
2438             lengthBar.setValue(getPosition());
2439         }
2440     }
2441 
2442     /**
2443      * Adjusts the cells location and size if necessary. The breadths of all
2444      * cells will be adjusted to fit the viewportWidth or maxPrefBreadth, and
2445      * the layout position will be updated if necessary based on index and
2446      * offset.
2447      */
2448     private void fitCells() {
2449         double size = Math.max(getMaxPrefBreadth(), getViewportBreadth());
2450         boolean isVertical = isVertical();
2451 
2452         // Note: Do not optimise this loop by pre-calculating the cells size and
2453         // storing that into a int value - this can lead to RT-32828
2454         for (int i = 0; i < cells.size(); i++) {
2455             Cell<?> cell = cells.get(i);
2456             if (isVertical) {
2457                 cell.resize(size, cell.prefHeight(size));
2458             } else {
2459                 cell.resize(cell.prefWidth(size), size);
2460             }
2461         }
2462     }
2463 
2464     private void cull() {
2465         final double viewportLength = getViewportLength();
2466         for (int i = cells.size() - 1; i >= 0; i--) {
2467             T cell = cells.get(i);
2468             double cellSize = getCellLength(cell);
2469             double cellStart = getCellPosition(cell);
2470             double cellEnd = cellStart + cellSize;
2471             if (cellStart >= viewportLength || cellEnd < 0) {
2472                 addToPile(cells.remove(i));
2473             }
2474         }
2475     }
2476 
2477     /**
2478      * After using the accum cell, it needs to be released!
2479      */
2480     private void releaseCell(T cell) {
2481         if (accumCell != null && cell == accumCell) {
2482             accumCell.updateIndex(-1);
2483         }
2484     }
2485 
2486     /**
2487      * This method is an experts-only method - if the requested index is not
2488      * already an existing visible cell, it will create a cell for the
2489      * given index and insert it into the sheet. From that point on it will be
2490      * unmanaged, and is up to the caller of this method to manage it.
2491      */
2492     T getPrivateCell(int index)  {
2493         T cell = null;
2494 
2495         // If there are cells, then we will attempt to get an existing cell
2496         if (! cells.isEmpty()) {
2497             // First check the cells that have already been created and are
2498             // in use. If this call returns a value, then we can use it
2499             cell = getVisibleCell(index);
2500             if (cell != null) {
2501                 // Force the underlying text inside the cell to be updated
2502                 // so that when the screen reader runs, it will match the
2503                 // text in the cell (force updateDisplayedText())
2504                 cell.layout();
2505                 return cell;
2506             }
2507         }
2508 
2509         // check the existing sheet children
2510         if (cell == null) {
2511             for (int i = 0; i < sheetChildren.size(); i++) {
2512                 T _cell = (T) sheetChildren.get(i);
2513                 if (getCellIndex(_cell) == index) {
2514                     return _cell;
2515                 }
2516             }
2517         }
2518 
2519         if (cell == null) {
2520             Callback<VirtualFlow<T>, T> cellFactory = getCellFactory();
2521             if (cellFactory != null) {
2522                 cell = cellFactory.call(this);
2523             }
2524         }
2525 
2526         if (cell != null) {
2527             setCellIndex(cell, index);
2528             resizeCellSize(cell);
2529             cell.setVisible(false);
2530             sheetChildren.add(cell);
2531             privateCells.add(cell);
2532         }
2533 
2534         return cell;
2535     }
2536 
2537     private final List<T> privateCells = new ArrayList<>();
2538 
2539     private void releaseAllPrivateCells() {
2540         sheetChildren.removeAll(privateCells);
2541     }
2542 
2543     /**
2544      * Puts the given cell onto the pile. This is called whenever a cell has
2545      * fallen off the flow's start.
2546      */
2547     private void addToPile(T cell) {
2548         assert cell != null;
2549         pile.addLast(cell);
2550     }
2551 
2552     private void cleanPile() {
2553         boolean wasFocusOwner = false;
2554 
2555         for (int i = 0, max = pile.size(); i < max; i++) {
2556             T cell = pile.get(i);
2557             wasFocusOwner = wasFocusOwner || doesCellContainFocus(cell);
2558             cell.setVisible(false);
2559         }
2560 
2561         // Fix for RT-35876: Rather than have the cells do weird things with
2562         // focus (in particular, have focus jump between cells), we return focus
2563         // to the VirtualFlow itself.
2564         if (wasFocusOwner) {
2565             requestFocus();
2566         }
2567     }
2568 
2569     private boolean doesCellContainFocus(Cell<?> c) {
2570         Scene scene = c.getScene();
2571         final Node focusOwner = scene == null ? null : scene.getFocusOwner();
2572 
2573         if (focusOwner != null) {
2574             if (c.equals(focusOwner)) {
2575                 return true;
2576             }
2577 
2578             Parent p = focusOwner.getParent();
2579             while (p != null && ! (p instanceof VirtualFlow)) {
2580                 if (c.equals(p)) {
2581                     return true;
2582                 }
2583                 p = p.getParent();
2584             }
2585         }
2586 
2587         return false;
2588     }
2589 
2590     private double getPrefBreadth(double oppDimension) {
2591         double max = getMaxCellWidth(10);
2592 
2593         // This primarily exists for the case where we do not want the breadth
2594         // to grow to ensure a golden ratio between width and height (for example,
2595         // when a ListView is used in a ComboBox - the width should not grow
2596         // just because items are being added to the ListView)
2597         if (oppDimension > -1) {
2598             double prefLength = getPrefLength();
2599             max = Math.max(max, prefLength * GOLDEN_RATIO_MULTIPLIER);
2600         }
2601 
2602         return max;
2603     }
2604 
2605     private double getPrefLength() {
2606         double sum = 0.0;
2607         int rows = Math.min(10, getCellCount());
2608         for (int i = 0; i < rows; i++) {
2609             sum += getCellLength(i);
2610         }
2611         return sum;
2612     }
2613 
2614     double getMaxCellWidth(int rowsToCount) {
2615         double max = 0.0;
2616 
2617         // we always measure at least one row
2618         int rows = Math.max(1, rowsToCount == -1 ? getCellCount() : rowsToCount);
2619         for (int i = 0; i < rows; i++) {
2620             max = Math.max(max, getCellBreadth(i));
2621         }
2622         return max;
2623     }
2624 
2625     // Old PositionMapper
2626     /**
2627      * Given a position value between 0 and 1, compute and return the viewport
2628      * offset from the "current" cell associated with that position value.
2629      * That is, if the return value of this function where used as a translation
2630      * factor for a sheet that contained all the items, then the current
2631      * item would end up positioned correctly.
2632      */
2633     private double computeViewportOffset(double position) {
2634         double p = com.sun.javafx.util.Utils.clamp(0, position, 1);
2635         double fractionalPosition = p * getCellCount();
2636         int cellIndex = (int) fractionalPosition;
2637         double fraction = fractionalPosition - cellIndex;
2638         double cellSize = getCellLength(cellIndex);
2639         double pixelOffset = cellSize * fraction;
2640         double viewportOffset = getViewportLength() * p;
2641         return pixelOffset - viewportOffset;
2642     }
2643 
2644     private void adjustPositionToIndex(int index) {
2645         int cellCount = getCellCount();
2646         if (cellCount <= 0) {
2647             setPosition(0.0f);
2648         } else {
2649             setPosition(((double)index) / cellCount);
2650         }
2651     }
2652 
2653     /**
2654      * Adjust the position based on a delta of pixels. If negative, then the
2655      * position will be adjusted negatively. If positive, then the position will
2656      * be adjusted positively. If the pixel amount is too great for the range of
2657      * the position, then it will be clamped such that position is always
2658      * strictly between 0 and 1
2659      */
2660     private void adjustByPixelAmount(double numPixels) {
2661         if (numPixels == 0) return;
2662         // Starting from the current cell, we move in the direction indicated
2663         // by numPixels one cell at a team. For each cell, we discover how many
2664         // pixels the "position" line would move within that cell, and adjust
2665         // our count of numPixels accordingly. When we come to the "final" cell,
2666         // then we can take the remaining number of pixels and multiply it by
2667         // the "travel rate" of "p" within that cell to get the delta. Add
2668         // the delta to "p" to get position.
2669 
2670         // get some basic info about the list and the current cell
2671         boolean forward = numPixels > 0;
2672         int cellCount = getCellCount();
2673         double fractionalPosition = getPosition() * cellCount;
2674         int cellIndex = (int) fractionalPosition;
2675         if (forward && cellIndex == cellCount) return;
2676         double cellSize = getCellLength(cellIndex);
2677         double fraction = fractionalPosition - cellIndex;
2678         double pixelOffset = cellSize * fraction;
2679 
2680         // compute the percentage of "position" that represents each cell
2681         double cellPercent = 1.0 / cellCount;
2682 
2683         // To help simplify the algorithm, we pretend as though the current
2684         // position is at the beginning of the current cell. This reduces some
2685         // of the corner cases and provides a simpler algorithm without adding
2686         // any overhead to performance.
2687         double start = computeOffsetForCell(cellIndex);
2688         double end = cellSize + computeOffsetForCell(cellIndex + 1);
2689 
2690         // We need to discover the distance that the fictional "position line"
2691         // would travel within this cell, from its current position to the end.
2692         double remaining = end - start;
2693 
2694         // Keep track of the number of pixels left to travel
2695         double n = forward ?
2696               numPixels + pixelOffset - (getViewportLength() * getPosition()) - start
2697             : -numPixels + end - (pixelOffset - (getViewportLength() * getPosition()));
2698 
2699         // "p" represents the most recent value for position. This is always
2700         // based on the edge between two cells, except at the very end of the
2701         // algorithm where it is added to the computed "p" offset for the final
2702         // value of Position.
2703         double p = cellPercent * cellIndex;
2704 
2705         // Loop over the cells one at a time until either we reach the end of
2706         // the cells, or we find that the "n" will fall within the cell we're on
2707         while (n > remaining && ((forward && cellIndex < cellCount - 1) || (! forward && cellIndex > 0))) {
2708             if (forward) cellIndex++; else cellIndex--;
2709             n -= remaining;
2710             cellSize = getCellLength(cellIndex);
2711             start = computeOffsetForCell(cellIndex);
2712             end = cellSize + computeOffsetForCell(cellIndex + 1);
2713             remaining = end - start;
2714             p = cellPercent * cellIndex;
2715         }
2716 
2717         // if remaining is < n, then we must have hit an end, so as a
2718         // fast path, we can just set position to 1.0 or 0.0 and return
2719         // because we know we hit the end
2720         if (n > remaining) {
2721             setPosition(forward ? 1.0f : 0.0f);
2722         } else if (forward) {
2723             double rate = cellPercent / Math.abs(end - start);
2724             setPosition(p + (rate * n));
2725         } else {
2726             double rate = cellPercent / Math.abs(end - start);
2727             setPosition((p + cellPercent) - (rate * n));
2728         }
2729     }
2730 
2731     private int computeCurrentIndex() {
2732         return (int) (getPosition() * getCellCount());
2733     }
2734 
2735     /**
2736      * Given an item index, this function will compute and return the viewport
2737      * offset from the beginning of the specified item. Notice that because each
2738      * item has the same percentage of the position dedicated to it, and since
2739      * we are measuring from the start of each item, this is a very simple
2740      * calculation.
2741      */
2742     private double computeOffsetForCell(int itemIndex) {
2743         double cellCount = getCellCount();
2744         double p = com.sun.javafx.util.Utils.clamp(0, itemIndex, cellCount) / cellCount;
2745         return -(getViewportLength() * p);
2746     }
2747 
2748 //    /**
2749 //     * Adjust the position based on a chunk of pixels. The position is based
2750 //     * on the start of the scrollbar position.
2751 //     */
2752 //    private void adjustByPixelChunk(double numPixels) {
2753 //        setPosition(0);
2754 //        adjustByPixelAmount(numPixels);
2755 //    }
2756     // end of old PositionMapper code
2757 
2758 
2759 
2760 
2761     /***************************************************************************
2762      *                                                                         *
2763      * Support classes                                                         *
2764      *                                                                         *
2765      **************************************************************************/
2766 
2767     /**
2768      * A simple extension to Region that ensures that anything wanting to flow
2769      * outside of the bounds of the Region is clipped.
2770      */
2771     static class ClippedContainer extends Region {
2772 
2773         /**
2774          * The Node which is embedded within this {@code ClipView}.
2775          */
2776         private Node node;
2777         public Node getNode() { return this.node; }
2778         public void setNode(Node n) {
2779             this.node = n;
2780 
2781             getChildren().clear();
2782             getChildren().add(node);
2783         }
2784 
2785         public void setClipX(double clipX) {
2786             setLayoutX(-clipX);
2787             clipRect.setLayoutX(clipX);
2788         }
2789 
2790         public void setClipY(double clipY) {
2791             setLayoutY(-clipY);
2792             clipRect.setLayoutY(clipY);
2793         }
2794 
2795         private final Rectangle clipRect;
2796 
2797         public ClippedContainer(final VirtualFlow<?> flow) {
2798             if (flow == null) {
2799                 throw new IllegalArgumentException("VirtualFlow can not be null");
2800             }
2801 
2802             getStyleClass().add("clipped-container");
2803 
2804             // clipping
2805             clipRect = new Rectangle();
2806             clipRect.setSmooth(false);
2807             setClip(clipRect);
2808             // --- clipping
2809 
2810             super.widthProperty().addListener(valueModel -> {
2811                 clipRect.setWidth(getWidth());
2812             });
2813             super.heightProperty().addListener(valueModel -> {
2814                 clipRect.setHeight(getHeight());
2815             });
2816         }
2817     }
2818 
2819     /**
2820      * A List-like implementation that is exceedingly efficient for the purposes
2821      * of the VirtualFlow. Typically there is not much variance in the number of
2822      * cells -- it is always some reasonably consistent number. Yet for efficiency
2823      * in code, we like to use a linked list implementation so as to append to
2824      * start or append to end. However, at times when we need to iterate, LinkedList
2825      * is expensive computationally as well as requiring the construction of
2826      * temporary iterators.
2827      * <p>
2828      * This linked list like implementation is done using an array. It begins by
2829      * putting the first item in the center of the allocated array, and then grows
2830      * outward (either towards the first or last of the array depending on whether
2831      * we are inserting at the head or tail). It maintains an index to the start
2832      * and end of the array, so that it can efficiently expose iteration.
2833      * <p>
2834      * This class is package private solely for the sake of testing.
2835      */
2836     static class ArrayLinkedList<T> extends AbstractList<T> {
2837         /**
2838          * The array list backing this class. We default the size of the array
2839          * list to be fairly large so as not to require resizing during normal
2840          * use, and since that many ArrayLinkedLists won't be created it isn't
2841          * very painful to do so.
2842          */
2843         private final ArrayList<T> array;
2844 
2845         private int firstIndex = -1;
2846         private int lastIndex = -1;
2847 
2848         public ArrayLinkedList() {
2849             array = new ArrayList<T>(50);
2850 
2851             for (int i = 0; i < 50; i++) {
2852                 array.add(null);
2853             }
2854         }
2855 
2856         public T getFirst() {
2857             return firstIndex == -1 ? null : array.get(firstIndex);
2858         }
2859 
2860         public T getLast() {
2861             return lastIndex == -1 ? null : array.get(lastIndex);
2862         }
2863 
2864         public void addFirst(T cell) {
2865             // if firstIndex == -1 then that means this is the first item in the
2866             // list and we need to initialize firstIndex and lastIndex
2867             if (firstIndex == -1) {
2868                 firstIndex = lastIndex = array.size() / 2;
2869                 array.set(firstIndex, cell);
2870             } else if (firstIndex == 0) {
2871                 // we're already at the head of the array, so insert at position
2872                 // 0 and then increment the lastIndex to compensate
2873                 array.add(0, cell);
2874                 lastIndex++;
2875             } else {
2876                 // we're not yet at the head of the array, so insert at the
2877                 // firstIndex - 1 position and decrement first position
2878                 array.set(--firstIndex, cell);
2879             }
2880         }
2881 
2882         public void addLast(T cell) {
2883             // if lastIndex == -1 then that means this is the first item in the
2884             // list and we need to initialize the firstIndex and lastIndex
2885             if (firstIndex == -1) {
2886                 firstIndex = lastIndex = array.size() / 2;
2887                 array.set(lastIndex, cell);
2888             } else if (lastIndex == array.size() - 1) {
2889                 // we're at the end of the array so need to "add" so as to force
2890                 // the array to be expanded in size
2891                 array.add(++lastIndex, cell);
2892             } else {
2893                 array.set(++lastIndex, cell);
2894             }
2895         }
2896 
2897         public int size() {
2898             return firstIndex == -1 ? 0 : lastIndex - firstIndex + 1;
2899         }
2900 
2901         public boolean isEmpty() {
2902             return firstIndex == -1;
2903         }
2904 
2905         public T get(int index) {
2906             if (index > (lastIndex - firstIndex) || index < 0) {
2907                 // Commented out exception due to RT-29111
2908                 // throw new java.lang.ArrayIndexOutOfBoundsException();
2909                 return null;
2910             }
2911 
2912             return array.get(firstIndex + index);
2913         }
2914 
2915         public void clear() {
2916             for (int i = 0; i < array.size(); i++) {
2917                 array.set(i, null);
2918             }
2919 
2920             firstIndex = lastIndex = -1;
2921         }
2922 
2923         public T removeFirst() {
2924             if (isEmpty()) return null;
2925             return remove(0);
2926         }
2927 
2928         public T removeLast() {
2929             if (isEmpty()) return null;
2930             return remove(lastIndex - firstIndex);
2931         }
2932 
2933         public T remove(int index) {
2934             if (index > lastIndex - firstIndex || index < 0) {
2935                 throw new ArrayIndexOutOfBoundsException();
2936             }
2937 
2938             // if the index == 0, then we're removing the first
2939             // item and can simply set it to null in the array and increment
2940             // the firstIndex unless there is only one item, in which case
2941             // we have to also set first & last index to -1.
2942             if (index == 0) {
2943                 T cell = array.get(firstIndex);
2944                 array.set(firstIndex, null);
2945                 if (firstIndex == lastIndex) {
2946                     firstIndex = lastIndex = -1;
2947                 } else {
2948                     firstIndex++;
2949                 }
2950                 return cell;
2951             } else if (index == lastIndex - firstIndex) {
2952                 // if the index == lastIndex - firstIndex, then we're removing the
2953                 // last item and can simply set it to null in the array and
2954                 // decrement the lastIndex
2955                 T cell = array.get(lastIndex);
2956                 array.set(lastIndex--, null);
2957                 return cell;
2958             } else {
2959                 // if the index is somewhere in between, then we have to remove the
2960                 // item and decrement the lastIndex
2961                 T cell = array.get(firstIndex + index);
2962                 array.set(firstIndex + index, null);
2963                 for (int i = (firstIndex + index + 1); i <= lastIndex; i++) {
2964                     array.set(i - 1, array.get(i));
2965                 }
2966                 array.set(lastIndex--, null);
2967                 return cell;
2968             }
2969         }
2970     }
2971 }