1 /*
   2  * Copyright (c) 2010, 2016, 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.ParentHelper;
  29 import com.sun.javafx.scene.control.Logging;
  30 import com.sun.javafx.scene.control.Properties;
  31 import com.sun.javafx.scene.control.VirtualScrollBar;
  32 import com.sun.javafx.scene.control.skin.Utils;
  33 import com.sun.javafx.scene.traversal.Algorithm;
  34 import com.sun.javafx.scene.traversal.Direction;
  35 import com.sun.javafx.scene.traversal.ParentTraversalEngine;
  36 import com.sun.javafx.scene.traversal.TraversalContext;
  37 import javafx.animation.KeyFrame;
  38 import javafx.animation.Timeline;
  39 import javafx.beans.InvalidationListener;
  40 import javafx.beans.Observable;
  41 import javafx.beans.property.BooleanProperty;
  42 import javafx.beans.property.BooleanPropertyBase;
  43 import javafx.beans.property.DoubleProperty;
  44 import javafx.beans.property.IntegerProperty;
  45 import javafx.beans.property.ObjectProperty;
  46 import javafx.beans.property.SimpleBooleanProperty;
  47 import javafx.beans.property.SimpleDoubleProperty;
  48 import javafx.beans.property.SimpleIntegerProperty;
  49 import javafx.beans.property.SimpleObjectProperty;
  50 import javafx.beans.value.ChangeListener;
  51 import javafx.collections.ObservableList;
  52 import javafx.event.EventDispatcher;
  53 import javafx.event.EventHandler;
  54 import javafx.geometry.Orientation;
  55 import javafx.scene.AccessibleRole;
  56 import javafx.scene.Group;
  57 import javafx.scene.Node;
  58 import javafx.scene.Parent;
  59 import javafx.scene.Scene;
  60 import javafx.scene.control.Cell;
  61 import javafx.scene.control.IndexedCell;
  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 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         ParentHelper.setTraversalEngine(this, 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      * @param value the new cell factory
 904      */
 905     public final void setCellFactory(Callback<VirtualFlow<T>, T> value) {
 906         cellFactoryProperty().set(value);
 907     }
 908 
 909     /**
 910      * Returns the current cell factory.
 911      * @return the current cell factory
 912      */
 913     public final Callback<VirtualFlow<T>, T> getCellFactory() {
 914         return cellFactory == null ? null : cellFactory.get();
 915     }
 916 
 917     /**
 918      * <p>Setting a custom cell factory has the effect of deferring all cell
 919      * creation, allowing for total customization of the cell. Internally, the
 920      * VirtualFlow is responsible for reusing cells - all that is necessary
 921      * is for the custom cell factory to return from this function a cell
 922      * which might be usable for representing any item in the VirtualFlow.
 923      *
 924      * <p>Refer to the {@link Cell} class documentation for more detail.
 925      * @return  the cell factory property
 926      */
 927     public final ObjectProperty<Callback<VirtualFlow<T>, T>> cellFactoryProperty() {
 928         if (cellFactory == null) {
 929             cellFactory = new SimpleObjectProperty<Callback<VirtualFlow<T>, T>>(this, "cellFactory") {
 930                 @Override protected void invalidated() {
 931                     if (get() != null) {
 932                         accumCell = null;
 933                         setNeedsLayout(true);
 934                         recreateCells();
 935                         if (getParent() != null) getParent().requestLayout();
 936                     }
 937                 }
 938             };
 939         }
 940         return cellFactory;
 941     }
 942 
 943 
 944 
 945     /***************************************************************************
 946      *                                                                         *
 947      * Public API                                                              *
 948      *                                                                         *
 949      **************************************************************************/
 950 
 951     /**
 952      * Overridden to implement somewhat more efficient support for layout. The
 953      * VirtualFlow can generally be considered as being unmanaged, in that
 954      * whenever the position changes, or other such things change, we need
 955      * to perform a layout but there is no reason to notify the parent. However
 956      * when things change which may impact the preferred size (such as
 957      * vertical, createCell, and configCell) then we need to notify the
 958      * parent.
 959      */
 960     @Override public void requestLayout() {
 961 // Note: This block is commented as it was relaying on a bad assumption on how
 962 //       layout request was handled in parent class that is now fixed.
 963 //
 964 //        // isNeedsLayout() is commented out due to RT-21417. This does not
 965 //        // appear to impact performance (indeed, it may help), and resolves the
 966 //        // issue identified in RT-21417.
 967 //        setNeedsLayout(true);
 968 
 969         // The fix is to prograte this layout request to its parent class.
 970         // A better fix will be required if performance is negatively affected
 971         // by this fix.
 972         super.requestLayout();
 973     }
 974 
 975     /** {@inheritDoc} */
 976     @Override protected void layoutChildren() {
 977         if (needsRecreateCells) {
 978             lastWidth = -1;
 979             lastHeight = -1;
 980             releaseCell(accumCell);
 981 //            accumCell = null;
 982 //            accumCellParent.getChildren().clear();
 983             sheet.getChildren().clear();
 984             for (int i = 0, max = cells.size(); i < max; i++) {
 985                 cells.get(i).updateIndex(-1);
 986             }
 987             cells.clear();
 988             pile.clear();
 989             releaseAllPrivateCells();
 990         } else if (needsRebuildCells) {
 991             lastWidth = -1;
 992             lastHeight = -1;
 993             releaseCell(accumCell);
 994             for (int i = 0, max = cells.size(); i < max; i++) {
 995                 cells.get(i).updateIndex(-1);
 996             }
 997             addAllToPile();
 998             releaseAllPrivateCells();
 999         } else if (needsReconfigureCells) {
1000             setMaxPrefBreadth(-1);
1001             lastWidth = -1;
1002             lastHeight = -1;
1003         }
1004 
1005         if (! dirtyCells.isEmpty()) {
1006             int index;
1007             final int cellsSize = cells.size();
1008             while ((index = dirtyCells.nextSetBit(0)) != -1 && index < cellsSize) {
1009                 T cell = cells.get(index);
1010                 // updateIndex(-1) works for TableView, but breaks ListView.
1011                 // For now, the TableView just does not use the dirtyCells API
1012 //                cell.updateIndex(-1);
1013                 if (cell != null) {
1014                     cell.requestLayout();
1015                 }
1016                 dirtyCells.clear(index);
1017             }
1018 
1019             setMaxPrefBreadth(-1);
1020             lastWidth = -1;
1021             lastHeight = -1;
1022         }
1023 
1024         final boolean hasSizeChange = sizeChanged;
1025         boolean recreatedOrRebuilt = needsRebuildCells || needsRecreateCells || sizeChanged;
1026 
1027         needsRecreateCells = false;
1028         needsReconfigureCells = false;
1029         needsRebuildCells = false;
1030         sizeChanged = false;
1031 
1032         if (needsCellsLayout) {
1033             for (int i = 0, max = cells.size(); i < max; i++) {
1034                 Cell<?> cell = cells.get(i);
1035                 if (cell != null) {
1036                     cell.requestLayout();
1037                 }
1038             }
1039             needsCellsLayout = false;
1040 
1041             // yes, we return here - if needsCellsLayout was set to true, we
1042             // only did it to do the above - not rerun the entire layout.
1043             return;
1044         }
1045 
1046         final double width = getWidth();
1047         final double height = getHeight();
1048         final boolean isVertical = isVertical();
1049         final double position = getPosition();
1050 
1051         // if the width and/or height is 0, then there is no point doing
1052         // any of this work. In particular, this can happen during startup
1053         if (width <= 0 || height <= 0) {
1054             addAllToPile();
1055             lastWidth = width;
1056             lastHeight = height;
1057             hbar.setVisible(false);
1058             vbar.setVisible(false);
1059             corner.setVisible(false);
1060             return;
1061         }
1062 
1063         // we check if any of the cells in the cells list need layout. This is a
1064         // sign that they are perhaps animating their sizes. Without this check,
1065         // we may not perform a layout here, meaning that the cell will likely
1066         // 'jump' (in height normally) when the user drags the virtual thumb as
1067         // that is the first time the layout would occur otherwise.
1068         boolean cellNeedsLayout = false;
1069         boolean thumbNeedsLayout = false;
1070 
1071         if (Properties.IS_TOUCH_SUPPORTED) {
1072             if ((tempVisibility == true && (hbar.isVisible() == false || vbar.isVisible() == false)) ||
1073                 (tempVisibility == false && (hbar.isVisible() == true || vbar.isVisible() == true))) {
1074                 thumbNeedsLayout = true;
1075             }
1076         }
1077 
1078         if (!cellNeedsLayout) {
1079             for (int i = 0; i < cells.size(); i++) {
1080                 Cell<?> cell = cells.get(i);
1081                 cellNeedsLayout = cell.isNeedsLayout();
1082                 if (cellNeedsLayout) break;
1083             }
1084         }
1085 
1086         final int cellCount = getCellCount();
1087         final T firstCell = getFirstVisibleCell();
1088 
1089         // If no cells need layout, we check other criteria to see if this
1090         // layout call is even necessary. If it is found that no layout is
1091         // needed, we just punt.
1092         if (! cellNeedsLayout && !thumbNeedsLayout) {
1093             boolean cellSizeChanged = false;
1094             if (firstCell != null) {
1095                 double breadth = getCellBreadth(firstCell);
1096                 double length = getCellLength(firstCell);
1097                 cellSizeChanged = (breadth != lastCellBreadth) || (length != lastCellLength);
1098                 lastCellBreadth = breadth;
1099                 lastCellLength = length;
1100             }
1101 
1102             if (width == lastWidth &&
1103                 height == lastHeight &&
1104                 cellCount == lastCellCount &&
1105                 isVertical == lastVertical &&
1106                 position == lastPosition &&
1107                 ! cellSizeChanged)
1108             {
1109                 // TODO this happens to work around the problem tested by
1110                 // testCellLayout_LayoutWithoutChangingThingsUsesCellsInSameOrderAsBefore
1111                 // but isn't a proper solution. Really what we need to do is, when
1112                 // laying out cells, we need to make sure that if a cell is pressed
1113                 // AND we are doing a full rebuild then we need to make sure we
1114                 // use that cell in the same physical location as before so that
1115                 // it gets the mouse release event.
1116                 return;
1117             }
1118         }
1119 
1120         /*
1121          * This function may get called under a variety of circumstances.
1122          * It will determine what has changed from the last time it was laid
1123          * out, and will then take one of several execution paths based on
1124          * what has changed so as to perform minimal layout work and also to
1125          * give the expected behavior. One or more of the following may have
1126          * happened:
1127          *
1128          *  1) width/height has changed
1129          *      - If the width and/or height has been reduced (but neither of
1130          *        them has been expanded), then we simply have to reposition and
1131          *        resize the scroll bars
1132          *      - If the width (in the vertical case) has expanded, then we
1133          *        need to resize the existing cells and reposition and resize
1134          *        the scroll bars
1135          *      - If the height (in the vertical case) has expanded, then we
1136          *        need to resize and reposition the scroll bars and add
1137          *        any trailing cells
1138          *
1139          *  2) cell count has changed
1140          *      - If the number of cells is bigger, or it is smaller but not
1141          *        so small as to move the position then we can just update the
1142          *        cells in place without performing layout and update the
1143          *        scroll bars.
1144          *      - If the number of cells has been reduced and it affects the
1145          *        position, then move the position and rebuild all the cells
1146          *        and update the scroll bars
1147          *
1148          *  3) size of the cell has changed
1149          *      - If the size changed in the virtual direction (ie: height
1150          *        in the case of vertical) then layout the cells, adding
1151          *        trailing cells as necessary and updating the scroll bars
1152          *      - If the size changed in the non virtual direction (ie: width
1153          *        in the case of vertical) then simply adjust the widths of
1154          *        the cells as appropriate and adjust the scroll bars
1155          *
1156          *  4) vertical changed, cells is empty, maxPrefBreadth == -1, etc
1157          *      - Full rebuild.
1158          *
1159          * Each of the conditions really resolves to several of a handful of
1160          * possible outcomes:
1161          *  a) reposition & rebuild scroll bars
1162          *  b) resize cells in non-virtual direction
1163          *  c) add trailing cells
1164          *  d) update cells
1165          *  e) resize cells in the virtual direction
1166          *  f) all of the above
1167          *
1168          * So this function first determines what outcomes need to occur, and
1169          * then will execute all the ones that really need to happen. Every code
1170          * path ends up touching the "reposition & rebuild scroll bars" outcome,
1171          * so that one will be executed every time.
1172          */
1173         boolean needTrailingCells = false;
1174         boolean rebuild = cellNeedsLayout  ||
1175                 isVertical != lastVertical ||
1176                 cells.isEmpty()            ||
1177                 getMaxPrefBreadth() == -1  ||
1178                 position != lastPosition   ||
1179                 cellCount != lastCellCount ||
1180                 hasSizeChange ||
1181                 (isVertical && height < lastHeight) || (! isVertical && width < lastWidth);
1182 
1183         if (!rebuild) {
1184             // Check if maxPrefBreadth didn't change
1185             double maxPrefBreadth = getMaxPrefBreadth();
1186             boolean foundMax = false;
1187             for (int i = 0; i < cells.size(); ++i) {
1188                 double breadth = getCellBreadth(cells.get(i));
1189                 if (maxPrefBreadth == breadth) {
1190                     foundMax = true;
1191                 } else if (breadth > maxPrefBreadth) {
1192                     rebuild = true;
1193                     break;
1194                 }
1195             }
1196             if (!foundMax) { // All values were lower
1197                 rebuild = true;
1198             }
1199         }
1200 
1201         if (! rebuild) {
1202             if ((isVertical && height > lastHeight) || (! isVertical && width > lastWidth)) {
1203                 // resized in the virtual direction
1204                 needTrailingCells = true;
1205             }
1206         }
1207 
1208         initViewport();
1209 
1210         // Get the index of the "current" cell
1211         int currentIndex = computeCurrentIndex();
1212         if (lastCellCount != cellCount) {
1213             // The cell count has changed. We want to keep the viewport
1214             // stable if possible. If position was 0 or 1, we want to keep
1215             // the position in the same place. If the new cell count is >=
1216             // the currentIndex, then we will adjust the position to be 1.
1217             // Otherwise, our goal is to leave the index of the cell at the
1218             // top consistent, with the same translation etc.
1219             if (position == 0 || position == 1) {
1220                 // Update the item count
1221 //                setItemCount(cellCount);
1222             } else if (currentIndex >= cellCount) {
1223                 setPosition(1.0f);
1224 //                setItemCount(cellCount);
1225             } else if (firstCell != null) {
1226                 double firstCellOffset = getCellPosition(firstCell);
1227                 int firstCellIndex = getCellIndex(firstCell);
1228 //                setItemCount(cellCount);
1229                 adjustPositionToIndex(firstCellIndex);
1230                 double viewportTopToCellTop = -computeOffsetForCell(firstCellIndex);
1231                 adjustByPixelAmount(viewportTopToCellTop - firstCellOffset);
1232             }
1233 
1234             // Update the current index
1235             currentIndex = computeCurrentIndex();
1236         }
1237 
1238         if (rebuild) {
1239             setMaxPrefBreadth(-1);
1240             // Start by dumping all the cells into the pile
1241             addAllToPile();
1242 
1243             // The distance from the top of the viewport to the top of the
1244             // cell for the current index.
1245             double offset = -computeViewportOffset(getPosition());
1246 
1247             // Add all the leading and trailing cells (the call to add leading
1248             // cells will add the current cell as well -- that is, the one that
1249             // represents the current position on the mapper).
1250             addLeadingCells(currentIndex, offset);
1251 
1252             // Force filling of space with empty cells if necessary
1253             addTrailingCells(true);
1254         } else if (needTrailingCells) {
1255             addTrailingCells(true);
1256         }
1257 
1258         computeBarVisiblity();
1259 
1260         recreatedOrRebuilt = recreatedOrRebuilt || rebuild;
1261         updateScrollBarsAndCells(recreatedOrRebuilt);
1262 
1263         lastWidth = getWidth();
1264         lastHeight = getHeight();
1265         lastCellCount = getCellCount();
1266         lastVertical = isVertical();
1267         lastPosition = getPosition();
1268 
1269         cleanPile();
1270     }
1271 
1272     /** {@inheritDoc} */
1273     @Override protected void setWidth(double value) {
1274         if (value != lastWidth) {
1275             super.setWidth(value);
1276             sizeChanged = true;
1277             setNeedsLayout(true);
1278             requestLayout();
1279         }
1280     }
1281 
1282     /** {@inheritDoc} */
1283     @Override protected void setHeight(double value) {
1284         if (value != lastHeight) {
1285             super.setHeight(value);
1286             sizeChanged = true;
1287             setNeedsLayout(true);
1288             requestLayout();
1289         }
1290     }
1291 
1292     /**
1293      * Get a cell which can be used in the layout. This function will reuse
1294      * cells from the pile where possible, and will create new cells when
1295      * necessary.
1296      * @param prefIndex the preferred index
1297      * @return the available cell
1298      */
1299     protected T getAvailableCell(int prefIndex) {
1300         T cell = null;
1301 
1302         // Fix for RT-12822. We try to retrieve the cell from the pile rather
1303         // than just grab a random cell from the pile (or create another cell).
1304         for (int i = 0, max = pile.size(); i < max; i++) {
1305             T _cell = pile.get(i);
1306             assert _cell != null;
1307 
1308             if (getCellIndex(_cell) == prefIndex) {
1309                 cell = _cell;
1310                 pile.remove(i);
1311                 break;
1312             }
1313         }
1314 
1315         if (cell == null && !pile.isEmpty()) {
1316             cell = pile.removeLast();
1317         }
1318 
1319         if (cell == null) {
1320             cell = getCellFactory().call(this);
1321             cell.getProperties().put(NEW_CELL, null);
1322         }
1323 
1324         if (cell.getParent() == null) {
1325             sheetChildren.add(cell);
1326         }
1327 
1328         return cell;
1329     }
1330 
1331     /**
1332      * This method will remove all cells from the VirtualFlow and remove them,
1333      * adding them to the 'pile' (that is, a place from where cells can be used
1334      * at a later date). This method is protected to allow subclasses to clean up
1335      * appropriately.
1336      */
1337     protected void addAllToPile() {
1338         for (int i = 0, max = cells.size(); i < max; i++) {
1339             addToPile(cells.removeFirst());
1340         }
1341     }
1342 
1343     /**
1344      * Gets a cell for the given index if the cell has been created and laid out.
1345      * "Visible" is a bit of a misnomer, the cell might not be visible in the
1346      * viewport (it may be clipped), but does distinguish between cells that
1347      * have been created and are in use vs. those that are in the pile or
1348      * not created.
1349      * @param index the index
1350      * @return the visible cell
1351      */
1352     public T getVisibleCell(int index) {
1353         if (cells.isEmpty()) return null;
1354 
1355         // check the last index
1356         T lastCell = cells.getLast();
1357         int lastIndex = getCellIndex(lastCell);
1358         if (index == lastIndex) return lastCell;
1359 
1360         // check the first index
1361         T firstCell = cells.getFirst();
1362         int firstIndex = getCellIndex(firstCell);
1363         if (index == firstIndex) return firstCell;
1364 
1365         // if index is > firstIndex and < lastIndex then we can get the index
1366         if (index > firstIndex && index < lastIndex) {
1367             T cell = cells.get(index - firstIndex);
1368             if (getCellIndex(cell) == index) return cell;
1369         }
1370 
1371         // there is no visible cell for the specified index
1372         return null;
1373     }
1374 
1375     /**
1376      * Locates and returns the last non-empty IndexedCell that is currently
1377      * partially or completely visible. This function may return null if there
1378      * are no cells, or if the viewport length is 0.
1379      * @return the last visible cell
1380      */
1381     public T getLastVisibleCell() {
1382         if (cells.isEmpty() || getViewportLength() <= 0) return null;
1383 
1384         T cell;
1385         for (int i = cells.size() - 1; i >= 0; i--) {
1386             cell = cells.get(i);
1387             if (! cell.isEmpty()) {
1388                 return cell;
1389             }
1390         }
1391 
1392         return null;
1393     }
1394 
1395     /**
1396      * Locates and returns the first non-empty IndexedCell that is partially or
1397      * completely visible. This really only ever returns null if there are no
1398      * cells or the viewport length is 0.
1399      * @return the first visible cell
1400      */
1401     public T getFirstVisibleCell() {
1402         if (cells.isEmpty() || getViewportLength() <= 0) return null;
1403         T cell = cells.getFirst();
1404         return cell.isEmpty() ? null : cell;
1405     }
1406 
1407     /**
1408      * Adjust the position of cells so that the specified cell
1409      * will be positioned at the start of the viewport. The given cell must
1410      * already be "live".
1411      * @param firstCell the first cell
1412      */
1413     public void scrollToTop(T firstCell) {
1414         if (firstCell != null) {
1415             scrollPixels(getCellPosition(firstCell));
1416         }
1417     }
1418 
1419     /**
1420      * Adjust the position of cells so that the specified cell
1421      * will be positioned at the end of the viewport. The given cell must
1422      * already be "live".
1423      * @param lastCell the last cell
1424      */
1425     public void scrollToBottom(T lastCell) {
1426         if (lastCell != null) {
1427             scrollPixels(getCellPosition(lastCell) + getCellLength(lastCell) - getViewportLength());
1428         }
1429     }
1430 
1431     /**
1432      * Adjusts the cells such that the selected cell will be fully visible in
1433      * the viewport (but only just).
1434      * @param cell the cell
1435      */
1436     public void scrollTo(T cell) {
1437         if (cell != null) {
1438             final double start = getCellPosition(cell);
1439             final double length = getCellLength(cell);
1440             final double end = start + length;
1441             final double viewportLength = getViewportLength();
1442 
1443             if (start < 0) {
1444                 scrollPixels(start);
1445             } else if (end > viewportLength) {
1446                 scrollPixels(end - viewportLength);
1447             }
1448         }
1449     }
1450 
1451     /**
1452      * Adjusts the cells such that the cell in the given index will be fully visible in
1453      * the viewport.
1454      * @param index the index
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      * @param index the index
1471      */
1472     public void scrollToTop(int index) {
1473         boolean posSet = false;
1474 
1475         if (index >= getCellCount() - 1) {
1476             setPosition(1);
1477             posSet = true;
1478         } else if (index < 0) {
1479             setPosition(0);
1480             posSet = true;
1481         }
1482 
1483         if (! posSet) {
1484             adjustPositionToIndex(index);
1485             double offset = - computeOffsetForCell(index);
1486             adjustByPixelAmount(offset);
1487         }
1488 
1489         requestLayout();
1490     }
1491 
1492 //    //TODO We assume all the cell have the same length.  We will need to support
1493 //    // cells of different lengths.
1494 //    public void scrollToOffset(int offset) {
1495 //        scrollPixels(offset * getCellLength(0));
1496 //    }
1497 
1498     /**
1499      * Given a delta value representing a number of pixels, this method attempts
1500      * to move the VirtualFlow in the given direction (positive is down/right,
1501      * negative is up/left) the given number of pixels. It returns the number of
1502      * pixels actually moved.
1503      * @param delta the delta value
1504      * @return the number of pixels actually moved
1505      */
1506     public double scrollPixels(final double delta) {
1507         // Short cut this method for cases where nothing should be done
1508         if (delta == 0) return 0;
1509 
1510         final boolean isVertical = isVertical();
1511         if (((isVertical && (tempVisibility ? !needLengthBar : !vbar.isVisible())) ||
1512                 (! isVertical && (tempVisibility ? !needLengthBar : !hbar.isVisible())))) return 0;
1513 
1514         double pos = getPosition();
1515         if (pos == 0.0f && delta < 0) return 0;
1516         if (pos == 1.0f && delta > 0) return 0;
1517 
1518         adjustByPixelAmount(delta);
1519         if (pos == getPosition()) {
1520             // The pos hasn't changed, there's nothing to do. This is likely
1521             // to occur when we hit either extremity
1522             return 0;
1523         }
1524 
1525         // Now move stuff around. Translating by pixels fundamentally means
1526         // moving the cells by the delta. However, after having
1527         // done that, we need to go through the cells and see which cells,
1528         // after adding in the translation factor, now fall off the viewport.
1529         // Also, we need to add cells as appropriate to the end (or beginning,
1530         // depending on the direction of travel).
1531         //
1532         // One simplifying assumption (that had better be true!) is that we
1533         // will only make it this far in the function if the virtual scroll
1534         // bar is visible. Otherwise, we never will pixel scroll. So as we go,
1535         // if we find that the maxPrefBreadth exceeds the viewportBreadth,
1536         // then we will be sure to show the breadthBar and update it
1537         // accordingly.
1538         if (cells.size() > 0) {
1539             for (int i = 0; i < cells.size(); i++) {
1540                 T cell = cells.get(i);
1541                 assert cell != null;
1542                 positionCell(cell, getCellPosition(cell) - delta);
1543             }
1544 
1545             // Fix for RT-32908
1546             T firstCell = cells.getFirst();
1547             double layoutY = firstCell == null ? 0 : getCellPosition(firstCell);
1548             for (int i = 0; i < cells.size(); i++) {
1549                 T cell = cells.get(i);
1550                 assert cell != null;
1551                 double actualLayoutY = getCellPosition(cell);
1552                 if (Math.abs(actualLayoutY - layoutY) > 0.001) {
1553                     // we need to shift the cell to layoutY
1554                     positionCell(cell, layoutY);
1555                 }
1556 
1557                 layoutY += getCellLength(cell);
1558             }
1559             // end of fix for RT-32908
1560             cull();
1561             firstCell = cells.getFirst();
1562 
1563             // Add any necessary leading cells
1564             if (firstCell != null) {
1565                 int firstIndex = getCellIndex(firstCell);
1566                 double prevIndexSize = getCellLength(firstIndex - 1);
1567                 addLeadingCells(firstIndex - 1, getCellPosition(firstCell) - prevIndexSize);
1568             } else {
1569                 int currentIndex = computeCurrentIndex();
1570 
1571                 // The distance from the top of the viewport to the top of the
1572                 // cell for the current index.
1573                 double offset = -computeViewportOffset(getPosition());
1574 
1575                 // Add all the leading and trailing cells (the call to add leading
1576                 // cells will add the current cell as well -- that is, the one that
1577                 // represents the current position on the mapper).
1578                 addLeadingCells(currentIndex, offset);
1579             }
1580 
1581             // Starting at the tail of the list, loop adding cells until
1582             // all the space on the table is filled up. We want to make
1583             // sure that we DO NOT add empty trailing cells (since we are
1584             // in the full virtual case and so there are no trailing empty
1585             // cells).
1586             if (! addTrailingCells(false)) {
1587                 // Reached the end, but not enough cells to fill up to
1588                 // the end. So, remove the trailing empty space, and translate
1589                 // the cells down
1590                 final T lastCell = getLastVisibleCell();
1591                 final double lastCellSize = getCellLength(lastCell);
1592                 final double cellEnd = getCellPosition(lastCell) + lastCellSize;
1593                 final double viewportLength = getViewportLength();
1594 
1595                 if (cellEnd < viewportLength) {
1596                     // Reposition the nodes
1597                     double emptySize = viewportLength - cellEnd;
1598                     for (int i = 0; i < cells.size(); i++) {
1599                         T cell = cells.get(i);
1600                         positionCell(cell, getCellPosition(cell) + emptySize);
1601                     }
1602                     setPosition(1.0f);
1603                     // fill the leading empty space
1604                     firstCell = cells.getFirst();
1605                     int firstIndex = getCellIndex(firstCell);
1606                     double prevIndexSize = getCellLength(firstIndex - 1);
1607                     addLeadingCells(firstIndex - 1, getCellPosition(firstCell) - prevIndexSize);
1608                 }
1609             }
1610         }
1611 
1612         // Now throw away any cells that don't fit
1613         cull();
1614 
1615         // Finally, update the scroll bars
1616         updateScrollBarsAndCells(false);
1617         lastPosition = getPosition();
1618 
1619         // notify
1620         return delta; // TODO fake
1621     }
1622 
1623     /** {@inheritDoc} */
1624     @Override protected double computePrefWidth(double height) {
1625         double w = isVertical() ? getPrefBreadth(height) : getPrefLength();
1626         return w + vbar.prefWidth(-1);
1627     }
1628 
1629     /** {@inheritDoc} */
1630     @Override protected double computePrefHeight(double width) {
1631         double h = isVertical() ? getPrefLength() : getPrefBreadth(width);
1632         return h + hbar.prefHeight(-1);
1633     }
1634 
1635     /**
1636      * Return a cell for the given index. This may be called for any cell,
1637      * including beyond the range defined by cellCount, in which case an
1638      * empty cell will be returned. The returned value should not be stored for
1639      * any reason.
1640      * @param index the index
1641      * @return the cell
1642      */
1643     public T getCell(int index) {
1644         // If there are cells, then we will attempt to get an existing cell
1645         if (! cells.isEmpty()) {
1646             // First check the cells that have already been created and are
1647             // in use. If this call returns a value, then we can use it
1648             T cell = getVisibleCell(index);
1649             if (cell != null) return cell;
1650         }
1651 
1652         // check the pile
1653         for (int i = 0; i < pile.size(); i++) {
1654             T cell = pile.get(i);
1655             if (getCellIndex(cell) == index) {
1656                 // Note that we don't remove from the pile: if we do it leads
1657                 // to a severe performance decrease. This seems to be OK, as
1658                 // getCell() is only used for cell measurement purposes.
1659                 // pile.remove(i);
1660                 return cell;
1661             }
1662         }
1663 
1664         if (pile.size() > 0) {
1665             return pile.get(0);
1666         }
1667 
1668         // We need to use the accumCell and return that
1669         if (accumCell == null) {
1670             Callback<VirtualFlow<T>,T> cellFactory = getCellFactory();
1671             if (cellFactory != null) {
1672                 accumCell = cellFactory.call(this);
1673                 accumCell.getProperties().put(NEW_CELL, null);
1674                 accumCellParent.getChildren().setAll(accumCell);
1675 
1676                 // Note the screen reader will attempt to find all
1677                 // the items inside the view to calculate the item count.
1678                 // Having items under different parents (sheet and accumCellParent)
1679                 // leads the screen reader to compute wrong values.
1680                 // The regular scheme to provide items to the screen reader
1681                 // uses getPrivateCell(), which places the item in the sheet.
1682                 // The accumCell, and its children, should be ignored by the
1683                 // screen reader.
1684                 accumCell.setAccessibleRole(AccessibleRole.NODE);
1685                 accumCell.getChildrenUnmodifiable().addListener((Observable c) -> {
1686                     for (Node n : accumCell.getChildrenUnmodifiable()) {
1687                         n.setAccessibleRole(AccessibleRole.NODE);
1688                     }
1689                 });
1690             }
1691         }
1692         setCellIndex(accumCell, index);
1693         resizeCellSize(accumCell);
1694         return accumCell;
1695     }
1696 
1697     /**
1698      * The VirtualFlow uses this method to set a cells index (rather than calling
1699      * {@link IndexedCell#updateIndex(int)} directly), so it is a perfect place
1700      * for subclasses to override if this if of interest.
1701      *
1702      * @param cell The cell whose index will be updated.
1703      * @param index The new index for the cell.
1704      */
1705     protected void setCellIndex(T cell, int index) {
1706         assert cell != null;
1707 
1708         cell.updateIndex(index);
1709 
1710         // make sure the cell is sized correctly. This is important for both
1711         // general layout of cells in a VirtualFlow, but also in cases such as
1712         // RT-34333, where the sizes were being reported incorrectly to the
1713         // ComboBox popup.
1714         if ((cell.isNeedsLayout() && cell.getScene() != null) || cell.getProperties().containsKey(NEW_CELL)) {
1715             cell.applyCss();
1716             cell.getProperties().remove(NEW_CELL);
1717         }
1718     }
1719 
1720     /**
1721      * Return the index for a given cell. This allows subclasses to customise
1722      * how cell indices are retrieved.
1723      * @param cell the cell
1724      * @return the index
1725      */
1726     protected int getCellIndex(T cell){
1727         return cell.getIndex();
1728     }
1729 
1730 
1731 
1732     /***************************************************************************
1733      *                                                                         *
1734      * Private implementation                                                  *
1735      *                                                                         *
1736      **************************************************************************/
1737 
1738     final VirtualScrollBar getHbar() {
1739         return hbar;
1740     }
1741     final VirtualScrollBar getVbar() {
1742         return vbar;
1743     }
1744 
1745     /**
1746      * The maximum preferred size in the non-virtual direction. For example,
1747      * if vertical, then this is the max pref width of all cells encountered.
1748      * <p>
1749      * In general, this is the largest preferred size in the non-virtual
1750      * direction that we have ever encountered. We don't reduce this size
1751      * unless instructed to do so, so as to reduce the amount of scroll bar
1752      * jitter. The access on this variable is package ONLY FOR TESTING.
1753      */
1754     private double maxPrefBreadth;
1755     private final void setMaxPrefBreadth(double value) {
1756         this.maxPrefBreadth = value;
1757     }
1758     final double getMaxPrefBreadth() {
1759         return maxPrefBreadth;
1760     }
1761 
1762     /**
1763      * The breadth of the viewport portion of the VirtualFlow as computed during
1764      * the layout pass. In a vertical flow this would be the same as the clip
1765      * view width. In a horizontal flow this is the clip view height.
1766      * The access on this variable is package ONLY FOR TESTING.
1767      */
1768     private double viewportBreadth;
1769     private final void setViewportBreadth(double value) {
1770         this.viewportBreadth = value;
1771     }
1772     private final double getViewportBreadth() {
1773         return viewportBreadth;
1774     }
1775 
1776     /**
1777      * The length of the viewport portion of the VirtualFlow as computed
1778      * during the layout pass. In a vertical flow this would be the same as the
1779      * clip view height. In a horizontal flow this is the clip view width.
1780      * The access on this variable is package ONLY FOR TESTING.
1781      */
1782     private double viewportLength;
1783     void setViewportLength(double value) {
1784         this.viewportLength = value;
1785     }
1786     double getViewportLength() {
1787         return viewportLength;
1788     }
1789 
1790     /**
1791      * Compute and return the length of the cell for the given index. This is
1792      * called both internally when adjusting by pixels, and also at times
1793      * by PositionMapper (see the getItemSize callback). When called by
1794      * PositionMapper, it is possible that it will be called for some index
1795      * which is not associated with any cell, so we have to do a bit of work
1796      * to use a cell as a helper for computing cell size in some cases.
1797      */
1798     double getCellLength(int index) {
1799         if (fixedCellSizeEnabled) return getFixedCellSize();
1800 
1801         T cell = getCell(index);
1802         double length = getCellLength(cell);
1803         releaseCell(cell);
1804         return length;
1805     }
1806 
1807     /**
1808      */
1809     double getCellBreadth(int index) {
1810         T cell = getCell(index);
1811         double b = getCellBreadth(cell);
1812         releaseCell(cell);
1813         return b;
1814     }
1815 
1816     /**
1817      * Gets the length of a specific cell
1818      */
1819     double getCellLength(T cell) {
1820         if (cell == null) return 0;
1821         if (fixedCellSizeEnabled) return getFixedCellSize();
1822 
1823         return isVertical() ?
1824                 cell.getLayoutBounds().getHeight()
1825                 : cell.getLayoutBounds().getWidth();
1826     }
1827 
1828     /**
1829      * Gets the breadth of a specific cell
1830      */
1831     double getCellBreadth(Cell cell) {
1832         return isVertical() ?
1833                 cell.prefWidth(-1)
1834                 : cell.prefHeight(-1);
1835     }
1836 
1837     /**
1838      * Gets the layout position of the cell along the length axis
1839      */
1840     double getCellPosition(T cell) {
1841         if (cell == null) return 0;
1842 
1843         return isVertical() ?
1844                 cell.getLayoutY()
1845                 : cell.getLayoutX();
1846     }
1847 
1848     private void positionCell(T cell, double position) {
1849         if (isVertical()) {
1850             cell.setLayoutX(0);
1851             cell.setLayoutY(snapSizeY(position));
1852         } else {
1853             cell.setLayoutX(snapSizeX(position));
1854             cell.setLayoutY(0);
1855         }
1856     }
1857 
1858     private void resizeCellSize(T cell) {
1859         if (cell == null) return;
1860 
1861         if (isVertical()) {
1862             double width = Math.max(getMaxPrefBreadth(), getViewportBreadth());
1863             cell.resize(width, fixedCellSizeEnabled ? getFixedCellSize() : Utils.boundedSize(cell.prefHeight(width), cell.minHeight(width), cell.maxHeight(width)));
1864         } else {
1865             double height = Math.max(getMaxPrefBreadth(), getViewportBreadth());
1866             cell.resize(fixedCellSizeEnabled ? getFixedCellSize() : Utils.boundedSize(cell.prefWidth(height), cell.minWidth(height), cell.maxWidth(height)), height);
1867         }
1868     }
1869 
1870     private List<T> getCells() {
1871         return cells;
1872     }
1873 
1874     // Returns last visible cell whose bounds are entirely within the viewport
1875     T getLastVisibleCellWithinViewPort() {
1876         if (cells.isEmpty() || getViewportLength() <= 0) return null;
1877 
1878         T cell;
1879         final double max = getViewportLength();
1880         for (int i = cells.size() - 1; i >= 0; i--) {
1881             cell = cells.get(i);
1882             if (cell.isEmpty()) continue;
1883 
1884             final double cellStart = getCellPosition(cell);
1885             final double cellEnd = cellStart + getCellLength(cell);
1886 
1887             // we use the magic +2 to allow for a little bit of fuzziness,
1888             // this is to help in situations such as RT-34407
1889             if (cellEnd <= (max + 2)) {
1890                 return cell;
1891             }
1892         }
1893 
1894         return null;
1895     }
1896 
1897     // Returns first visible cell whose bounds are entirely within the viewport
1898     T getFirstVisibleCellWithinViewPort() {
1899         if (cells.isEmpty() || getViewportLength() <= 0) return null;
1900 
1901         T cell;
1902         for (int i = 0; i < cells.size(); i++) {
1903             cell = cells.get(i);
1904             if (cell.isEmpty()) continue;
1905 
1906             final double cellStart = getCellPosition(cell);
1907             if (cellStart >= 0) {
1908                 return cell;
1909             }
1910         }
1911 
1912         return null;
1913     }
1914 
1915     /**
1916      * Adds all the cells prior to and including the given currentIndex, until
1917      * no more can be added without falling off the flow. The startOffset
1918      * indicates the distance from the leading edge (top) of the viewport to
1919      * the leading edge (top) of the currentIndex.
1920      */
1921     void addLeadingCells(int currentIndex, double startOffset) {
1922         // The offset will keep track of the distance from the top of the
1923         // viewport to the top of the current index. We will increment it
1924         // as we lay out leading cells.
1925         double offset = startOffset;
1926         // The index is the absolute index of the cell being laid out
1927         int index = currentIndex;
1928 
1929         // Offset should really be the bottom of the current index
1930         boolean first = true; // first time in, we just fudge the offset and let
1931         // it be the top of the current index then redefine
1932         // it as the bottom of the current index thereafter
1933         // while we have not yet laid out so many cells that they would fall
1934         // off the flow, we will continue to create and add cells. The
1935         // offset is our indication of whether we can lay out additional
1936         // cells. If the offset is ever < 0, except in the case of the very
1937         // first cell, then we must quit.
1938         T cell = null;
1939 
1940         // special case for the position == 1.0, skip adding last invisible cell
1941         if (index == getCellCount() && offset == getViewportLength()) {
1942             index--;
1943             first = false;
1944         }
1945         while (index >= 0 && (offset > 0 || first)) {
1946             cell = getAvailableCell(index);
1947             setCellIndex(cell, index);
1948             resizeCellSize(cell); // resize must be after config
1949             cells.addFirst(cell);
1950 
1951             // A little gross but better than alternatives because it reduces
1952             // the number of times we have to update a cell or compute its
1953             // size. The first time into this loop "offset" is actually the
1954             // top of the current index. On all subsequent visits, it is the
1955             // bottom of the current index.
1956             if (first) {
1957                 first = false;
1958             } else {
1959                 offset -= getCellLength(cell);
1960             }
1961 
1962             // Position the cell, and update the maxPrefBreadth variable as we go.
1963             positionCell(cell, offset);
1964             setMaxPrefBreadth(Math.max(getMaxPrefBreadth(), getCellBreadth(cell)));
1965             cell.setVisible(true);
1966             --index;
1967         }
1968 
1969         // There are times when after laying out the cells we discover that
1970         // the top of the first cell which represents index 0 is below the top
1971         // of the viewport. In these cases, we have to adjust the cells up
1972         // and reset the mapper position. This might happen when items got
1973         // removed at the top or when the viewport size increased.
1974         if (cells.size() > 0) {
1975             cell = cells.getFirst();
1976             int firstIndex = getCellIndex(cell);
1977             double firstCellPos = getCellPosition(cell);
1978             if (firstIndex == 0 && firstCellPos > 0) {
1979                 setPosition(0.0f);
1980                 offset = 0;
1981                 for (int i = 0; i < cells.size(); i++) {
1982                     cell = cells.get(i);
1983                     positionCell(cell, offset);
1984                     offset += getCellLength(cell);
1985                 }
1986             }
1987         } else {
1988             // reset scrollbar to top, so if the flow sees cells again it starts at the top
1989             vbar.setValue(0);
1990             hbar.setValue(0);
1991         }
1992     }
1993 
1994     /**
1995      * Adds all the trailing cells that come <em>after</em> the last index in
1996      * the cells ObservableList.
1997      */
1998     boolean addTrailingCells(boolean fillEmptyCells) {
1999         // If cells is empty then addLeadingCells bailed for some reason and
2000         // we're hosed, so just punt
2001         if (cells.isEmpty()) return false;
2002 
2003         // While we have not yet laid out so many cells that they would fall
2004         // off the flow, so we will continue to create and add cells. When the
2005         // offset becomes greater than the width/height of the flow, then we
2006         // know we cannot add any more cells.
2007         T startCell = cells.getLast();
2008         double offset = getCellPosition(startCell) + getCellLength(startCell);
2009         int index = getCellIndex(startCell) + 1;
2010         final int cellCount = getCellCount();
2011         boolean filledWithNonEmpty = index <= cellCount;
2012 
2013         final double viewportLength = getViewportLength();
2014 
2015         // Fix for RT-37421, which was a regression caused by RT-36556
2016         if (offset < 0 && !fillEmptyCells) {
2017             return false;
2018         }
2019 
2020         //
2021         // RT-36507: viewportLength gives the maximum number of
2022         // additional cells that should ever be able to fit in the viewport if
2023         // every cell had a height of 1. If index ever exceeds this count,
2024         // then offset is not incrementing fast enough, or at all, which means
2025         // there is something wrong with the cell size calculation.
2026         //
2027         final double maxCellCount = viewportLength;
2028         while (offset < viewportLength) {
2029             if (index >= cellCount) {
2030                 if (offset < viewportLength) filledWithNonEmpty = false;
2031                 if (! fillEmptyCells) return filledWithNonEmpty;
2032                 // RT-36507 - return if we've exceeded the maximum
2033                 if (index > maxCellCount) {
2034                     final PlatformLogger logger = Logging.getControlsLogger();
2035                     if (logger.isLoggable(PlatformLogger.Level.INFO)) {
2036                         logger.info("index exceeds maxCellCount. Check size calculations for " + startCell.getClass());
2037                     }
2038                     return filledWithNonEmpty;
2039                 }
2040             }
2041             T cell = getAvailableCell(index);
2042             setCellIndex(cell, index);
2043             resizeCellSize(cell); // resize happens after config!
2044             cells.addLast(cell);
2045 
2046             // Position the cell and update the max pref
2047             positionCell(cell, offset);
2048             setMaxPrefBreadth(Math.max(getMaxPrefBreadth(), getCellBreadth(cell)));
2049 
2050             offset += getCellLength(cell);
2051             cell.setVisible(true);
2052             ++index;
2053         }
2054 
2055         // Discover whether the first cell coincides with index #0. If after
2056         // adding all the trailing cells we find that a) the first cell was
2057         // not index #0 and b) there are trailing cells, then we have a
2058         // problem. We need to shift all the cells down and add leading cells,
2059         // one at a time, until either the very last non-empty cells is aligned
2060         // with the bottom OR we have laid out cell index #0 at the first
2061         // position.
2062         T firstCell = cells.getFirst();
2063         index = getCellIndex(firstCell);
2064         T lastNonEmptyCell = getLastVisibleCell();
2065         double start = getCellPosition(firstCell);
2066         double end = getCellPosition(lastNonEmptyCell) + getCellLength(lastNonEmptyCell);
2067         if ((index != 0 || (index == 0 && start < 0)) && fillEmptyCells &&
2068                 lastNonEmptyCell != null && getCellIndex(lastNonEmptyCell) == cellCount - 1 && end < viewportLength) {
2069 
2070             double prospectiveEnd = end;
2071             double distance = viewportLength - end;
2072             while (prospectiveEnd < viewportLength && index != 0 && (-start) < distance) {
2073                 index--;
2074                 T cell = getAvailableCell(index);
2075                 setCellIndex(cell, index);
2076                 resizeCellSize(cell); // resize must be after config
2077                 cells.addFirst(cell);
2078                 double cellLength = getCellLength(cell);
2079                 start -= cellLength;
2080                 prospectiveEnd += cellLength;
2081                 positionCell(cell, start);
2082                 setMaxPrefBreadth(Math.max(getMaxPrefBreadth(), getCellBreadth(cell)));
2083                 cell.setVisible(true);
2084             }
2085 
2086             // The amount by which to translate the cells down
2087             firstCell = cells.getFirst();
2088             start = getCellPosition(firstCell);
2089             double delta = viewportLength - end;
2090             if (getCellIndex(firstCell) == 0 && delta > (-start)) {
2091                 delta = (-start);
2092             }
2093             // Move things
2094             for (int i = 0; i < cells.size(); i++) {
2095                 T cell = cells.get(i);
2096                 positionCell(cell, getCellPosition(cell) + delta);
2097             }
2098 
2099             // Check whether the first cell, subsequent to our adjustments, is
2100             // now index #0 and aligned with the top. If so, change the position
2101             // to be at 0 instead of 1.
2102             start = getCellPosition(firstCell);
2103             if (getCellIndex(firstCell) == 0 && start == 0) {
2104                 setPosition(0);
2105             } else if (getPosition() != 1) {
2106                 setPosition(1);
2107             }
2108         }
2109 
2110         return filledWithNonEmpty;
2111     }
2112 
2113     void reconfigureCells() {
2114         needsReconfigureCells = true;
2115         requestLayout();
2116     }
2117 
2118     void recreateCells() {
2119         needsRecreateCells = true;
2120         requestLayout();
2121     }
2122 
2123     void rebuildCells() {
2124         needsRebuildCells = true;
2125         requestLayout();
2126     }
2127 
2128     void requestCellLayout() {
2129         needsCellsLayout = true;
2130         requestLayout();
2131     }
2132 
2133     void setCellDirty(int index) {
2134         dirtyCells.set(index);
2135         requestLayout();
2136     }
2137 
2138     private void startSBReleasedAnimation() {
2139         if (sbTouchTimeline == null) {
2140             /*
2141             ** timeline to leave the scrollbars visible for a short
2142             ** while after a scroll/drag
2143             */
2144             sbTouchTimeline = new Timeline();
2145             sbTouchKF1 = new KeyFrame(Duration.millis(0), event -> {
2146                 tempVisibility = true;
2147                 requestLayout();
2148             });
2149 
2150             sbTouchKF2 = new KeyFrame(Duration.millis(1000), event -> {
2151                 if (touchDetected == false && mouseDown == false) {
2152                     tempVisibility = false;
2153                     requestLayout();
2154                 }
2155             });
2156             sbTouchTimeline.getKeyFrames().addAll(sbTouchKF1, sbTouchKF2);
2157         }
2158         sbTouchTimeline.playFromStart();
2159     }
2160 
2161     private void scrollBarOn() {
2162         tempVisibility = true;
2163         requestLayout();
2164     }
2165 
2166     void updateHbar() {
2167         if (! isVisible() || getScene() == null) return;
2168         // Bring the clipView.clipX back to 0 if control is vertical or
2169         // the hbar isn't visible (fix for RT-11666)
2170         if (isVertical()) {
2171             if (hbar.isVisible()) {
2172                 clipView.setClipX(hbar.getValue());
2173             } else {
2174                 // all cells are now less than the width of the flow,
2175                 // so we should shift the hbar/clip such that
2176                 // everything is visible in the viewport.
2177                 clipView.setClipX(0);
2178                 hbar.setValue(0);
2179             }
2180         }
2181     }
2182 
2183     /**
2184      * @return true if bar visibility changed
2185      */
2186     private boolean computeBarVisiblity() {
2187         if (cells.isEmpty()) {
2188             // In case no cells are set yet, we assume no bars are needed
2189             needLengthBar = false;
2190             needBreadthBar = false;
2191             return true;
2192         }
2193 
2194         final boolean isVertical = isVertical();
2195         boolean barVisibilityChanged = false;
2196 
2197         VirtualScrollBar breadthBar = isVertical ? hbar : vbar;
2198         VirtualScrollBar lengthBar = isVertical ? vbar : hbar;
2199 
2200         final double viewportBreadth = getViewportBreadth();
2201 
2202         final int cellsSize = cells.size();
2203         final int cellCount = getCellCount();
2204         for (int i = 0; i < 2; i++) {
2205             final boolean lengthBarVisible = getPosition() > 0
2206                     || cellCount > cellsSize
2207                     || (cellCount == cellsSize && (getCellPosition(cells.getLast()) + getCellLength(cells.getLast())) > getViewportLength())
2208                     || (cellCount == cellsSize - 1 && barVisibilityChanged && needBreadthBar);
2209 
2210             if (lengthBarVisible ^ needLengthBar) {
2211                 needLengthBar = lengthBarVisible;
2212                 barVisibilityChanged = true;
2213             }
2214 
2215             // second conditional removed for RT-36669.
2216             final boolean breadthBarVisible = (maxPrefBreadth > viewportBreadth);// || (needLengthBar && maxPrefBreadth > (viewportBreadth - lengthBarBreadth));
2217             if (breadthBarVisible ^ needBreadthBar) {
2218                 needBreadthBar = breadthBarVisible;
2219                 barVisibilityChanged = true;
2220             }
2221         }
2222 
2223         // Start by optimistically deciding whether the length bar and
2224         // breadth bar are needed and adjust the viewport dimensions
2225         // accordingly. If during layout we find that one or the other of the
2226         // bars actually is needed, then we will perform a cleanup pass
2227 
2228         if (!Properties.IS_TOUCH_SUPPORTED) {
2229             updateViewportDimensions();
2230             breadthBar.setVisible(needBreadthBar);
2231             lengthBar.setVisible(needLengthBar);
2232         } else {
2233             breadthBar.setVisible(needBreadthBar && tempVisibility);
2234             lengthBar.setVisible(needLengthBar && tempVisibility);
2235         }
2236 
2237         return barVisibilityChanged;
2238     }
2239 
2240     private void updateViewportDimensions() {
2241         final boolean isVertical = isVertical();
2242         final double breadthBarLength = isVertical ? snapSizeY(hbar.prefHeight(-1)) : snapSizeX(vbar.prefWidth(-1));
2243         final double lengthBarBreadth = isVertical ? snapSizeX(vbar.prefWidth(-1)) : snapSizeY(hbar.prefHeight(-1));
2244 
2245         setViewportBreadth((isVertical ? getWidth() : getHeight()) - (needLengthBar ? lengthBarBreadth : 0));
2246         setViewportLength((isVertical ? getHeight() : getWidth()) - (needBreadthBar ? breadthBarLength : 0));
2247     }
2248 
2249     private void initViewport() {
2250         // Initialize the viewportLength and viewportBreadth to match the
2251         // width/height of the flow
2252         final boolean isVertical = isVertical();
2253 
2254         updateViewportDimensions();
2255 
2256         VirtualScrollBar breadthBar = isVertical ? hbar : vbar;
2257         VirtualScrollBar lengthBar = isVertical ? vbar : hbar;
2258 
2259         // If there has been a switch between the virtualized bar, then we
2260         // will want to do some stuff TODO.
2261         breadthBar.setVirtual(false);
2262         lengthBar.setVirtual(true);
2263     }
2264 
2265     private void updateScrollBarsAndCells(boolean recreate) {
2266         // Assign the hbar and vbar to the breadthBar and lengthBar so as
2267         // to make some subsequent calculations easier.
2268         final boolean isVertical = isVertical();
2269         VirtualScrollBar breadthBar = isVertical ? hbar : vbar;
2270         VirtualScrollBar lengthBar = isVertical ? vbar : hbar;
2271 
2272         // We may have adjusted the viewport length and breadth after the
2273         // layout due to scroll bars becoming visible. So we need to perform
2274         // a follow up pass and resize and shift all the cells to fit the
2275         // viewport. Note that the prospective viewport size is always >= the
2276         // final viewport size, so we don't have to worry about adding
2277         // cells during this cleanup phase.
2278         fitCells();
2279 
2280         // Update cell positions.
2281         // When rebuilding the cells, we add the cells and along the way compute
2282         // the maxPrefBreadth. Based on the computed value, we may add
2283         // the breadth scrollbar which changes viewport length, so we need
2284         // to re-position the cells.
2285         if (!cells.isEmpty()) {
2286             final double currOffset = -computeViewportOffset(getPosition());
2287             final int currIndex = computeCurrentIndex() - cells.getFirst().getIndex();
2288             final int size = cells.size();
2289 
2290             // position leading cells
2291             double offset = currOffset;
2292 
2293             for (int i = currIndex - 1; i >= 0 && i < size; i--) {
2294                 final T cell = cells.get(i);
2295 
2296                 offset -= getCellLength(cell);
2297 
2298                 positionCell(cell, offset);
2299             }
2300 
2301             // position trailing cells
2302             offset = currOffset;
2303             for (int i = currIndex; i >= 0 && i < size; i++) {
2304                 final T cell = cells.get(i);
2305                 positionCell(cell, offset);
2306 
2307                 offset += getCellLength(cell);
2308             }
2309         }
2310 
2311         // Toggle visibility on the corner
2312         corner.setVisible(breadthBar.isVisible() && lengthBar.isVisible());
2313 
2314         double sumCellLength = 0;
2315         double flowLength = (isVertical ? getHeight() : getWidth()) -
2316                 (breadthBar.isVisible() ? breadthBar.prefHeight(-1) : 0);
2317 
2318         final double viewportBreadth = getViewportBreadth();
2319         final double viewportLength = getViewportLength();
2320 
2321         // Now position and update the scroll bars
2322         if (breadthBar.isVisible()) {
2323             /*
2324             ** Positioning the ScrollBar
2325             */
2326             if (!Properties.IS_TOUCH_SUPPORTED) {
2327                 if (isVertical) {
2328                     hbar.resizeRelocate(0, viewportLength,
2329                             viewportBreadth, hbar.prefHeight(viewportBreadth));
2330                 } else {
2331                     vbar.resizeRelocate(viewportLength, 0,
2332                             vbar.prefWidth(viewportBreadth), viewportBreadth);
2333                 }
2334             }
2335             else {
2336                 if (isVertical) {
2337                     hbar.resizeRelocate(0, (viewportLength-hbar.getHeight()),
2338                             viewportBreadth, hbar.prefHeight(viewportBreadth));
2339                 } else {
2340                     vbar.resizeRelocate((viewportLength-vbar.getWidth()), 0,
2341                             vbar.prefWidth(viewportBreadth), viewportBreadth);
2342                 }
2343             }
2344 
2345             if (getMaxPrefBreadth() != -1) {
2346                 double newMax = Math.max(1, getMaxPrefBreadth() - viewportBreadth);
2347                 if (newMax != breadthBar.getMax()) {
2348                     breadthBar.setMax(newMax);
2349 
2350                     double breadthBarValue = breadthBar.getValue();
2351                     boolean maxed = breadthBarValue != 0 && newMax == breadthBarValue;
2352                     if (maxed || breadthBarValue > newMax) {
2353                         breadthBar.setValue(newMax);
2354                     }
2355 
2356                     breadthBar.setVisibleAmount((viewportBreadth / getMaxPrefBreadth()) * newMax);
2357                 }
2358             }
2359         }
2360 
2361         // determine how many cells there are on screen so that the scrollbar
2362         // thumb can be appropriately sized
2363         if (recreate && (lengthBar.isVisible() || Properties.IS_TOUCH_SUPPORTED)) {
2364             final int cellCount = getCellCount();
2365             int numCellsVisibleOnScreen = 0;
2366             for (int i = 0, max = cells.size(); i < max; i++) {
2367                 T cell = cells.get(i);
2368                 if (cell != null && !cell.isEmpty()) {
2369                     sumCellLength += (isVertical ? cell.getHeight() : cell.getWidth());
2370                     if (sumCellLength > flowLength) {
2371                         break;
2372                     }
2373 
2374                     numCellsVisibleOnScreen++;
2375                 }
2376             }
2377 
2378             lengthBar.setMax(1);
2379             if (numCellsVisibleOnScreen == 0 && cellCount == 1) {
2380                 // special case to help resolve RT-17701 and the case where we have
2381                 // only a single row and it is bigger than the viewport
2382                 lengthBar.setVisibleAmount(flowLength / sumCellLength);
2383             } else {
2384                 lengthBar.setVisibleAmount(numCellsVisibleOnScreen / (float) cellCount);
2385             }
2386         }
2387 
2388         if (lengthBar.isVisible()) {
2389             // Fix for RT-11873. If this isn't here, we can have a situation where
2390             // the scrollbar scrolls endlessly. This is possible when the cell
2391             // count grows as the user hits the maximal position on the scrollbar
2392             // (i.e. the list size dynamically grows as the user needs more).
2393             //
2394             // This code was commented out to resolve RT-14477 after testing
2395             // whether RT-11873 can be recreated. It could not, and therefore
2396             // for now this code will remained uncommented until it is deleted
2397             // following further testing.
2398 //            if (lengthBar.getValue() == 1.0 && lastCellCount != cellCount) {
2399 //                lengthBar.setValue(0.99);
2400 //            }
2401 
2402             /*
2403             ** Positioning the ScrollBar
2404             */
2405             if (!Properties.IS_TOUCH_SUPPORTED) {
2406                 if (isVertical) {
2407                     vbar.resizeRelocate(viewportBreadth, 0, vbar.prefWidth(viewportLength), viewportLength);
2408                 } else {
2409                     hbar.resizeRelocate(0, viewportBreadth, viewportLength, hbar.prefHeight(-1));
2410                 }
2411             }
2412             else {
2413                 if (isVertical) {
2414                     vbar.resizeRelocate((viewportBreadth-vbar.getWidth()), 0, vbar.prefWidth(viewportLength), viewportLength);
2415                 } else {
2416                     hbar.resizeRelocate(0, (viewportBreadth-hbar.getHeight()), viewportLength, hbar.prefHeight(-1));
2417                 }
2418             }
2419         }
2420 
2421         if (corner.isVisible()) {
2422             if (!Properties.IS_TOUCH_SUPPORTED) {
2423                 corner.resize(vbar.getWidth(), hbar.getHeight());
2424                 corner.relocate(hbar.getLayoutX() + hbar.getWidth(), vbar.getLayoutY() + vbar.getHeight());
2425             }
2426             else {
2427                 corner.resize(vbar.getWidth(), hbar.getHeight());
2428                 corner.relocate(hbar.getLayoutX() + (hbar.getWidth()-vbar.getWidth()), vbar.getLayoutY() + (vbar.getHeight()-hbar.getHeight()));
2429                 hbar.resize(hbar.getWidth()-vbar.getWidth(), hbar.getHeight());
2430                 vbar.resize(vbar.getWidth(), vbar.getHeight()-hbar.getHeight());
2431             }
2432         }
2433 
2434         clipView.resize(snapSizeX(isVertical ? viewportBreadth : viewportLength),
2435                         snapSizeY(isVertical ? viewportLength : viewportBreadth));
2436 
2437         // If the viewportLength becomes large enough that all cells fit
2438         // within the viewport, then we want to update the value to match.
2439         if (getPosition() != lengthBar.getValue()) {
2440             lengthBar.setValue(getPosition());
2441         }
2442     }
2443 
2444     /**
2445      * Adjusts the cells location and size if necessary. The breadths of all
2446      * cells will be adjusted to fit the viewportWidth or maxPrefBreadth, and
2447      * the layout position will be updated if necessary based on index and
2448      * offset.
2449      */
2450     private void fitCells() {
2451         double size = Math.max(getMaxPrefBreadth(), getViewportBreadth());
2452         boolean isVertical = isVertical();
2453 
2454         // Note: Do not optimise this loop by pre-calculating the cells size and
2455         // storing that into a int value - this can lead to RT-32828
2456         for (int i = 0; i < cells.size(); i++) {
2457             Cell<?> cell = cells.get(i);
2458             if (isVertical) {
2459                 cell.resize(size, cell.prefHeight(size));
2460             } else {
2461                 cell.resize(cell.prefWidth(size), size);
2462             }
2463         }
2464     }
2465 
2466     private void cull() {
2467         final double viewportLength = getViewportLength();
2468         for (int i = cells.size() - 1; i >= 0; i--) {
2469             T cell = cells.get(i);
2470             double cellSize = getCellLength(cell);
2471             double cellStart = getCellPosition(cell);
2472             double cellEnd = cellStart + cellSize;
2473             if (cellStart >= viewportLength || cellEnd < 0) {
2474                 addToPile(cells.remove(i));
2475             }
2476         }
2477     }
2478 
2479     /**
2480      * After using the accum cell, it needs to be released!
2481      */
2482     private void releaseCell(T cell) {
2483         if (accumCell != null && cell == accumCell) {
2484             accumCell.updateIndex(-1);
2485         }
2486     }
2487 
2488     /**
2489      * This method is an experts-only method - if the requested index is not
2490      * already an existing visible cell, it will create a cell for the
2491      * given index and insert it into the sheet. From that point on it will be
2492      * unmanaged, and is up to the caller of this method to manage it.
2493      */
2494     T getPrivateCell(int index)  {
2495         T cell = null;
2496 
2497         // If there are cells, then we will attempt to get an existing cell
2498         if (! cells.isEmpty()) {
2499             // First check the cells that have already been created and are
2500             // in use. If this call returns a value, then we can use it
2501             cell = getVisibleCell(index);
2502             if (cell != null) {
2503                 // Force the underlying text inside the cell to be updated
2504                 // so that when the screen reader runs, it will match the
2505                 // text in the cell (force updateDisplayedText())
2506                 cell.layout();
2507                 return cell;
2508             }
2509         }
2510 
2511         // check the existing sheet children
2512         if (cell == null) {
2513             for (int i = 0; i < sheetChildren.size(); i++) {
2514                 T _cell = (T) sheetChildren.get(i);
2515                 if (getCellIndex(_cell) == index) {
2516                     return _cell;
2517                 }
2518             }
2519         }
2520 
2521         Callback<VirtualFlow<T>, T> cellFactory = getCellFactory();
2522         if (cellFactory != null) {
2523             cell = cellFactory.call(this);
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 }