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      */
 904     public final void setCellFactory(Callback<VirtualFlow<T>, T> value) {
 905         cellFactoryProperty().set(value);
 906     }
 907 
 908     /**
 909      * Returns the current cell factory.
 910      */
 911     public final Callback<VirtualFlow<T>, T> getCellFactory() {
 912         return cellFactory == null ? null : cellFactory.get();
 913     }
 914 
 915     /**
 916      * <p>Setting a custom cell factory has the effect of deferring all cell
 917      * creation, allowing for total customization of the cell. Internally, the
 918      * VirtualFlow is responsible for reusing cells - all that is necessary
 919      * is for the custom cell factory to return from this function a cell
 920      * which might be usable for representing any item in the VirtualFlow.
 921      *
 922      * <p>Refer to the {@link Cell} class documentation for more detail.
 923      */
 924     public final ObjectProperty<Callback<VirtualFlow<T>, T>> cellFactoryProperty() {
 925         if (cellFactory == null) {
 926             cellFactory = new SimpleObjectProperty<Callback<VirtualFlow<T>, T>>(this, "cellFactory") {
 927                 @Override protected void invalidated() {
 928                     if (get() != null) {
 929                         accumCell = null;
 930                         setNeedsLayout(true);
 931                         recreateCells();
 932                         if (getParent() != null) getParent().requestLayout();
 933                     }
 934                 }
 935             };
 936         }
 937         return cellFactory;
 938     }
 939 
 940 
 941 
 942     /***************************************************************************
 943      *                                                                         *
 944      * Public API                                                              *
 945      *                                                                         *
 946      **************************************************************************/
 947 
 948     /**
 949      * Overridden to implement somewhat more efficient support for layout. The
 950      * VirtualFlow can generally be considered as being unmanaged, in that
 951      * whenever the position changes, or other such things change, we need
 952      * to perform a layout but there is no reason to notify the parent. However
 953      * when things change which may impact the preferred size (such as
 954      * vertical, createCell, and configCell) then we need to notify the
 955      * parent.
 956      */
 957     @Override public void requestLayout() {
 958 // Note: This block is commented as it was relaying on a bad assumption on how
 959 //       layout request was handled in parent class that is now fixed.
 960 //
 961 //        // isNeedsLayout() is commented out due to RT-21417. This does not
 962 //        // appear to impact performance (indeed, it may help), and resolves the
 963 //        // issue identified in RT-21417.
 964 //        setNeedsLayout(true);
 965 
 966         // The fix is to prograte this layout request to its parent class.
 967         // A better fix will be required if performance is negatively affected
 968         // by this fix.
 969         super.requestLayout();
 970     }
 971 
 972     /** {@inheritDoc} */
 973     @Override protected void layoutChildren() {
 974         if (needsRecreateCells) {
 975             lastWidth = -1;
 976             lastHeight = -1;
 977             releaseCell(accumCell);
 978 //            accumCell = null;
 979 //            accumCellParent.getChildren().clear();
 980             sheet.getChildren().clear();
 981             for (int i = 0, max = cells.size(); i < max; i++) {
 982                 cells.get(i).updateIndex(-1);
 983             }
 984             cells.clear();
 985             pile.clear();
 986             releaseAllPrivateCells();
 987         } else if (needsRebuildCells) {
 988             lastWidth = -1;
 989             lastHeight = -1;
 990             releaseCell(accumCell);
 991             for (int i = 0, max = cells.size(); i < max; i++) {
 992                 cells.get(i).updateIndex(-1);
 993             }
 994             addAllToPile();
 995             releaseAllPrivateCells();
 996         } else if (needsReconfigureCells) {
 997             setMaxPrefBreadth(-1);
 998             lastWidth = -1;
 999             lastHeight = -1;
1000         }
1001 
1002         if (! dirtyCells.isEmpty()) {
1003             int index;
1004             final int cellsSize = cells.size();
1005             while ((index = dirtyCells.nextSetBit(0)) != -1 && index < cellsSize) {
1006                 T cell = cells.get(index);
1007                 // updateIndex(-1) works for TableView, but breaks ListView.
1008                 // For now, the TableView just does not use the dirtyCells API
1009 //                cell.updateIndex(-1);
1010                 if (cell != null) {
1011                     cell.requestLayout();
1012                 }
1013                 dirtyCells.clear(index);
1014             }
1015 
1016             setMaxPrefBreadth(-1);
1017             lastWidth = -1;
1018             lastHeight = -1;
1019         }
1020 
1021         final boolean hasSizeChange = sizeChanged;
1022         boolean recreatedOrRebuilt = needsRebuildCells || needsRecreateCells || sizeChanged;
1023 
1024         needsRecreateCells = false;
1025         needsReconfigureCells = false;
1026         needsRebuildCells = false;
1027         sizeChanged = false;
1028 
1029         if (needsCellsLayout) {
1030             for (int i = 0, max = cells.size(); i < max; i++) {
1031                 Cell<?> cell = cells.get(i);
1032                 if (cell != null) {
1033                     cell.requestLayout();
1034                 }
1035             }
1036             needsCellsLayout = false;
1037 
1038             // yes, we return here - if needsCellsLayout was set to true, we
1039             // only did it to do the above - not rerun the entire layout.
1040             return;
1041         }
1042 
1043         final double width = getWidth();
1044         final double height = getHeight();
1045         final boolean isVertical = isVertical();
1046         final double position = getPosition();
1047 
1048         // if the width and/or height is 0, then there is no point doing
1049         // any of this work. In particular, this can happen during startup
1050         if (width <= 0 || height <= 0) {
1051             addAllToPile();
1052             lastWidth = width;
1053             lastHeight = height;
1054             hbar.setVisible(false);
1055             vbar.setVisible(false);
1056             corner.setVisible(false);
1057             return;
1058         }
1059 
1060         // we check if any of the cells in the cells list need layout. This is a
1061         // sign that they are perhaps animating their sizes. Without this check,
1062         // we may not perform a layout here, meaning that the cell will likely
1063         // 'jump' (in height normally) when the user drags the virtual thumb as
1064         // that is the first time the layout would occur otherwise.
1065         boolean cellNeedsLayout = false;
1066         boolean thumbNeedsLayout = false;
1067 
1068         if (Properties.IS_TOUCH_SUPPORTED) {
1069             if ((tempVisibility == true && (hbar.isVisible() == false || vbar.isVisible() == false)) ||
1070                 (tempVisibility == false && (hbar.isVisible() == true || vbar.isVisible() == true))) {
1071                 thumbNeedsLayout = true;
1072             }
1073         }
1074 
1075         if (!cellNeedsLayout) {
1076             for (int i = 0; i < cells.size(); i++) {
1077                 Cell<?> cell = cells.get(i);
1078                 cellNeedsLayout = cell.isNeedsLayout();
1079                 if (cellNeedsLayout) break;
1080             }
1081         }
1082 
1083         final int cellCount = getCellCount();
1084         final T firstCell = getFirstVisibleCell();
1085 
1086         // If no cells need layout, we check other criteria to see if this
1087         // layout call is even necessary. If it is found that no layout is
1088         // needed, we just punt.
1089         if (! cellNeedsLayout && !thumbNeedsLayout) {
1090             boolean cellSizeChanged = false;
1091             if (firstCell != null) {
1092                 double breadth = getCellBreadth(firstCell);
1093                 double length = getCellLength(firstCell);
1094                 cellSizeChanged = (breadth != lastCellBreadth) || (length != lastCellLength);
1095                 lastCellBreadth = breadth;
1096                 lastCellLength = length;
1097             }
1098 
1099             if (width == lastWidth &&
1100                 height == lastHeight &&
1101                 cellCount == lastCellCount &&
1102                 isVertical == lastVertical &&
1103                 position == lastPosition &&
1104                 ! cellSizeChanged)
1105             {
1106                 // TODO this happens to work around the problem tested by
1107                 // testCellLayout_LayoutWithoutChangingThingsUsesCellsInSameOrderAsBefore
1108                 // but isn't a proper solution. Really what we need to do is, when
1109                 // laying out cells, we need to make sure that if a cell is pressed
1110                 // AND we are doing a full rebuild then we need to make sure we
1111                 // use that cell in the same physical location as before so that
1112                 // it gets the mouse release event.
1113                 return;
1114             }
1115         }
1116 
1117         /*
1118          * This function may get called under a variety of circumstances.
1119          * It will determine what has changed from the last time it was laid
1120          * out, and will then take one of several execution paths based on
1121          * what has changed so as to perform minimal layout work and also to
1122          * give the expected behavior. One or more of the following may have
1123          * happened:
1124          *
1125          *  1) width/height has changed
1126          *      - If the width and/or height has been reduced (but neither of
1127          *        them has been expanded), then we simply have to reposition and
1128          *        resize the scroll bars
1129          *      - If the width (in the vertical case) has expanded, then we
1130          *        need to resize the existing cells and reposition and resize
1131          *        the scroll bars
1132          *      - If the height (in the vertical case) has expanded, then we
1133          *        need to resize and reposition the scroll bars and add
1134          *        any trailing cells
1135          *
1136          *  2) cell count has changed
1137          *      - If the number of cells is bigger, or it is smaller but not
1138          *        so small as to move the position then we can just update the
1139          *        cells in place without performing layout and update the
1140          *        scroll bars.
1141          *      - If the number of cells has been reduced and it affects the
1142          *        position, then move the position and rebuild all the cells
1143          *        and update the scroll bars
1144          *
1145          *  3) size of the cell has changed
1146          *      - If the size changed in the virtual direction (ie: height
1147          *        in the case of vertical) then layout the cells, adding
1148          *        trailing cells as necessary and updating the scroll bars
1149          *      - If the size changed in the non virtual direction (ie: width
1150          *        in the case of vertical) then simply adjust the widths of
1151          *        the cells as appropriate and adjust the scroll bars
1152          *
1153          *  4) vertical changed, cells is empty, maxPrefBreadth == -1, etc
1154          *      - Full rebuild.
1155          *
1156          * Each of the conditions really resolves to several of a handful of
1157          * possible outcomes:
1158          *  a) reposition & rebuild scroll bars
1159          *  b) resize cells in non-virtual direction
1160          *  c) add trailing cells
1161          *  d) update cells
1162          *  e) resize cells in the virtual direction
1163          *  f) all of the above
1164          *
1165          * So this function first determines what outcomes need to occur, and
1166          * then will execute all the ones that really need to happen. Every code
1167          * path ends up touching the "reposition & rebuild scroll bars" outcome,
1168          * so that one will be executed every time.
1169          */
1170         boolean needTrailingCells = false;
1171         boolean rebuild = cellNeedsLayout  ||
1172                 isVertical != lastVertical ||
1173                 cells.isEmpty()            ||
1174                 getMaxPrefBreadth() == -1  ||
1175                 position != lastPosition   ||
1176                 cellCount != lastCellCount ||
1177                 hasSizeChange ||
1178                 (isVertical && height < lastHeight) || (! isVertical && width < lastWidth);
1179 
1180         if (!rebuild) {
1181             // Check if maxPrefBreadth didn't change
1182             double maxPrefBreadth = getMaxPrefBreadth();
1183             boolean foundMax = false;
1184             for (int i = 0; i < cells.size(); ++i) {
1185                 double breadth = getCellBreadth(cells.get(i));
1186                 if (maxPrefBreadth == breadth) {
1187                     foundMax = true;
1188                 } else if (breadth > maxPrefBreadth) {
1189                     rebuild = true;
1190                     break;
1191                 }
1192             }
1193             if (!foundMax) { // All values were lower
1194                 rebuild = true;
1195             }
1196         }
1197 
1198         if (! rebuild) {
1199             if ((isVertical && height > lastHeight) || (! isVertical && width > lastWidth)) {
1200                 // resized in the virtual direction
1201                 needTrailingCells = true;
1202             }
1203         }
1204 
1205         initViewport();
1206 
1207         // Get the index of the "current" cell
1208         int currentIndex = computeCurrentIndex();
1209         if (lastCellCount != cellCount) {
1210             // The cell count has changed. We want to keep the viewport
1211             // stable if possible. If position was 0 or 1, we want to keep
1212             // the position in the same place. If the new cell count is >=
1213             // the currentIndex, then we will adjust the position to be 1.
1214             // Otherwise, our goal is to leave the index of the cell at the
1215             // top consistent, with the same translation etc.
1216             if (position == 0 || position == 1) {
1217                 // Update the item count
1218 //                setItemCount(cellCount);
1219             } else if (currentIndex >= cellCount) {
1220                 setPosition(1.0f);
1221 //                setItemCount(cellCount);
1222             } else if (firstCell != null) {
1223                 double firstCellOffset = getCellPosition(firstCell);
1224                 int firstCellIndex = getCellIndex(firstCell);
1225 //                setItemCount(cellCount);
1226                 adjustPositionToIndex(firstCellIndex);
1227                 double viewportTopToCellTop = -computeOffsetForCell(firstCellIndex);
1228                 adjustByPixelAmount(viewportTopToCellTop - firstCellOffset);
1229             }
1230 
1231             // Update the current index
1232             currentIndex = computeCurrentIndex();
1233         }
1234 
1235         if (rebuild) {
1236             setMaxPrefBreadth(-1);
1237             // Start by dumping all the cells into the pile
1238             addAllToPile();
1239 
1240             // The distance from the top of the viewport to the top of the
1241             // cell for the current index.
1242             double offset = -computeViewportOffset(getPosition());
1243 
1244             // Add all the leading and trailing cells (the call to add leading
1245             // cells will add the current cell as well -- that is, the one that
1246             // represents the current position on the mapper).
1247             addLeadingCells(currentIndex, offset);
1248 
1249             // Force filling of space with empty cells if necessary
1250             addTrailingCells(true);
1251         } else if (needTrailingCells) {
1252             addTrailingCells(true);
1253         }
1254 
1255         computeBarVisiblity();
1256 
1257         recreatedOrRebuilt = recreatedOrRebuilt || rebuild;
1258         updateScrollBarsAndCells(recreatedOrRebuilt);
1259 
1260         lastWidth = getWidth();
1261         lastHeight = getHeight();
1262         lastCellCount = getCellCount();
1263         lastVertical = isVertical();
1264         lastPosition = getPosition();
1265 
1266         cleanPile();
1267     }
1268 
1269     /** {@inheritDoc} */
1270     @Override protected void setWidth(double value) {
1271         if (value != lastWidth) {
1272             super.setWidth(value);
1273             sizeChanged = true;
1274             setNeedsLayout(true);
1275             requestLayout();
1276         }
1277     }
1278 
1279     /** {@inheritDoc} */
1280     @Override protected void setHeight(double value) {
1281         if (value != lastHeight) {
1282             super.setHeight(value);
1283             sizeChanged = true;
1284             setNeedsLayout(true);
1285             requestLayout();
1286         }
1287     }
1288 
1289     /**
1290      * Get a cell which can be used in the layout. This function will reuse
1291      * cells from the pile where possible, and will create new cells when
1292      * necessary.
1293      */
1294     protected T getAvailableCell(int prefIndex) {
1295         T cell = null;
1296 
1297         // Fix for RT-12822. We try to retrieve the cell from the pile rather
1298         // than just grab a random cell from the pile (or create another cell).
1299         for (int i = 0, max = pile.size(); i < max; i++) {
1300             T _cell = pile.get(i);
1301             assert _cell != null;
1302 
1303             if (getCellIndex(_cell) == prefIndex) {
1304                 cell = _cell;
1305                 pile.remove(i);
1306                 break;
1307             }
1308         }
1309 
1310         if (cell == null && !pile.isEmpty()) {
1311             cell = pile.removeLast();
1312         }
1313 
1314         if (cell == null) {
1315             cell = getCellFactory().call(this);
1316             cell.getProperties().put(NEW_CELL, null);
1317         }
1318 
1319         if (cell.getParent() == null) {
1320             sheetChildren.add(cell);
1321         }
1322 
1323         return cell;
1324     }
1325 
1326     /**
1327      * This method will remove all cells from the VirtualFlow and remove them,
1328      * adding them to the 'pile' (that is, a place from where cells can be used
1329      * at a later date). This method is protected to allow subclasses to clean up
1330      * appropriately.
1331      */
1332     protected void addAllToPile() {
1333         for (int i = 0, max = cells.size(); i < max; i++) {
1334             addToPile(cells.removeFirst());
1335         }
1336     }
1337 
1338     /**
1339      * Gets a cell for the given index if the cell has been created and laid out.
1340      * "Visible" is a bit of a misnomer, the cell might not be visible in the
1341      * viewport (it may be clipped), but does distinguish between cells that
1342      * have been created and are in use vs. those that are in the pile or
1343      * not created.
1344      */
1345     public T getVisibleCell(int index) {
1346         if (cells.isEmpty()) return null;
1347 
1348         // check the last index
1349         T lastCell = cells.getLast();
1350         int lastIndex = getCellIndex(lastCell);
1351         if (index == lastIndex) return lastCell;
1352 
1353         // check the first index
1354         T firstCell = cells.getFirst();
1355         int firstIndex = getCellIndex(firstCell);
1356         if (index == firstIndex) return firstCell;
1357 
1358         // if index is > firstIndex and < lastIndex then we can get the index
1359         if (index > firstIndex && index < lastIndex) {
1360             T cell = cells.get(index - firstIndex);
1361             if (getCellIndex(cell) == index) return cell;
1362         }
1363 
1364         // there is no visible cell for the specified index
1365         return null;
1366     }
1367 
1368     /**
1369      * Locates and returns the last non-empty IndexedCell that is currently
1370      * partially or completely visible. This function may return null if there
1371      * are no cells, or if the viewport length is 0.
1372      */
1373     public T getLastVisibleCell() {
1374         if (cells.isEmpty() || getViewportLength() <= 0) return null;
1375 
1376         T cell;
1377         for (int i = cells.size() - 1; i >= 0; i--) {
1378             cell = cells.get(i);
1379             if (! cell.isEmpty()) {
1380                 return cell;
1381             }
1382         }
1383 
1384         return null;
1385     }
1386 
1387     /**
1388      * Locates and returns the first non-empty IndexedCell that is partially or
1389      * completely visible. This really only ever returns null if there are no
1390      * cells or the viewport length is 0.
1391      */
1392     public T getFirstVisibleCell() {
1393         if (cells.isEmpty() || getViewportLength() <= 0) return null;
1394         T cell = cells.getFirst();
1395         return cell.isEmpty() ? null : cell;
1396     }
1397 
1398     /**
1399      * Adjust the position of cells so that the specified cell
1400      * will be positioned at the start of the viewport. The given cell must
1401      * already be "live".
1402      */
1403     public void scrollToTop(T firstCell) {
1404         if (firstCell != null) {
1405             scrollPixels(getCellPosition(firstCell));
1406         }
1407     }
1408 
1409     /**
1410      * Adjust the position of cells so that the specified cell
1411      * will be positioned at the end of the viewport. The given cell must
1412      * already be "live".
1413      */
1414     public void scrollToBottom(T lastCell) {
1415         if (lastCell != null) {
1416             scrollPixels(getCellPosition(lastCell) + getCellLength(lastCell) - getViewportLength());
1417         }
1418     }
1419 
1420     /**
1421      * Adjusts the cells such that the selected cell will be fully visible in
1422      * the viewport (but only just).
1423      */
1424     public void scrollTo(T cell) {
1425         if (cell != null) {
1426             final double start = getCellPosition(cell);
1427             final double length = getCellLength(cell);
1428             final double end = start + length;
1429             final double viewportLength = getViewportLength();
1430 
1431             if (start < 0) {
1432                 scrollPixels(start);
1433             } else if (end > viewportLength) {
1434                 scrollPixels(end - viewportLength);
1435             }
1436         }
1437     }
1438 
1439     /**
1440      * Adjusts the cells such that the cell in the given index will be fully visible in
1441      * the viewport.
1442      */
1443     public void scrollTo(int index) {
1444         T cell = getVisibleCell(index);
1445         if (cell != null) {
1446             scrollTo(cell);
1447         } else {
1448             adjustPositionToIndex(index);
1449             addAllToPile();
1450             requestLayout();
1451         }
1452     }
1453 
1454     /**
1455      * Adjusts the cells such that the cell in the given index will be fully visible in
1456      * the viewport, and positioned at the very top of the viewport.
1457      */
1458     public void scrollToTop(int index) {
1459         boolean posSet = false;
1460 
1461         if (index >= getCellCount() - 1) {
1462             setPosition(1);
1463             posSet = true;
1464         } else if (index < 0) {
1465             setPosition(0);
1466             posSet = true;
1467         }
1468 
1469         if (! posSet) {
1470             adjustPositionToIndex(index);
1471             double offset = - computeOffsetForCell(index);
1472             adjustByPixelAmount(offset);
1473         }
1474 
1475         requestLayout();
1476     }
1477 
1478 //    //TODO We assume all the cell have the same length.  We will need to support
1479 //    // cells of different lengths.
1480 //    public void scrollToOffset(int offset) {
1481 //        scrollPixels(offset * getCellLength(0));
1482 //    }
1483 
1484     /**
1485      * Given a delta value representing a number of pixels, this method attempts
1486      * to move the VirtualFlow in the given direction (positive is down/right,
1487      * negative is up/left) the given number of pixels. It returns the number of
1488      * pixels actually moved.
1489      */
1490     public double scrollPixels(final double delta) {
1491         // Short cut this method for cases where nothing should be done
1492         if (delta == 0) return 0;
1493 
1494         final boolean isVertical = isVertical();
1495         if (((isVertical && (tempVisibility ? !needLengthBar : !vbar.isVisible())) ||
1496                 (! isVertical && (tempVisibility ? !needLengthBar : !hbar.isVisible())))) return 0;
1497 
1498         double pos = getPosition();
1499         if (pos == 0.0f && delta < 0) return 0;
1500         if (pos == 1.0f && delta > 0) return 0;
1501 
1502         adjustByPixelAmount(delta);
1503         if (pos == getPosition()) {
1504             // The pos hasn't changed, there's nothing to do. This is likely
1505             // to occur when we hit either extremity
1506             return 0;
1507         }
1508 
1509         // Now move stuff around. Translating by pixels fundamentally means
1510         // moving the cells by the delta. However, after having
1511         // done that, we need to go through the cells and see which cells,
1512         // after adding in the translation factor, now fall off the viewport.
1513         // Also, we need to add cells as appropriate to the end (or beginning,
1514         // depending on the direction of travel).
1515         //
1516         // One simplifying assumption (that had better be true!) is that we
1517         // will only make it this far in the function if the virtual scroll
1518         // bar is visible. Otherwise, we never will pixel scroll. So as we go,
1519         // if we find that the maxPrefBreadth exceeds the viewportBreadth,
1520         // then we will be sure to show the breadthBar and update it
1521         // accordingly.
1522         if (cells.size() > 0) {
1523             for (int i = 0; i < cells.size(); i++) {
1524                 T cell = cells.get(i);
1525                 assert cell != null;
1526                 positionCell(cell, getCellPosition(cell) - delta);
1527             }
1528 
1529             // Fix for RT-32908
1530             T firstCell = cells.getFirst();
1531             double layoutY = firstCell == null ? 0 : getCellPosition(firstCell);
1532             for (int i = 0; i < cells.size(); i++) {
1533                 T cell = cells.get(i);
1534                 assert cell != null;
1535                 double actualLayoutY = getCellPosition(cell);
1536                 if (Math.abs(actualLayoutY - layoutY) > 0.001) {
1537                     // we need to shift the cell to layoutY
1538                     positionCell(cell, layoutY);
1539                 }
1540 
1541                 layoutY += getCellLength(cell);
1542             }
1543             // end of fix for RT-32908
1544             cull();
1545             firstCell = cells.getFirst();
1546 
1547             // Add any necessary leading cells
1548             if (firstCell != null) {
1549                 int firstIndex = getCellIndex(firstCell);
1550                 double prevIndexSize = getCellLength(firstIndex - 1);
1551                 addLeadingCells(firstIndex - 1, getCellPosition(firstCell) - prevIndexSize);
1552             } else {
1553                 int currentIndex = computeCurrentIndex();
1554 
1555                 // The distance from the top of the viewport to the top of the
1556                 // cell for the current index.
1557                 double offset = -computeViewportOffset(getPosition());
1558 
1559                 // Add all the leading and trailing cells (the call to add leading
1560                 // cells will add the current cell as well -- that is, the one that
1561                 // represents the current position on the mapper).
1562                 addLeadingCells(currentIndex, offset);
1563             }
1564 
1565             // Starting at the tail of the list, loop adding cells until
1566             // all the space on the table is filled up. We want to make
1567             // sure that we DO NOT add empty trailing cells (since we are
1568             // in the full virtual case and so there are no trailing empty
1569             // cells).
1570             if (! addTrailingCells(false)) {
1571                 // Reached the end, but not enough cells to fill up to
1572                 // the end. So, remove the trailing empty space, and translate
1573                 // the cells down
1574                 final T lastCell = getLastVisibleCell();
1575                 final double lastCellSize = getCellLength(lastCell);
1576                 final double cellEnd = getCellPosition(lastCell) + lastCellSize;
1577                 final double viewportLength = getViewportLength();
1578 
1579                 if (cellEnd < viewportLength) {
1580                     // Reposition the nodes
1581                     double emptySize = viewportLength - cellEnd;
1582                     for (int i = 0; i < cells.size(); i++) {
1583                         T cell = cells.get(i);
1584                         positionCell(cell, getCellPosition(cell) + emptySize);
1585                     }
1586                     setPosition(1.0f);
1587                     // fill the leading empty space
1588                     firstCell = cells.getFirst();
1589                     int firstIndex = getCellIndex(firstCell);
1590                     double prevIndexSize = getCellLength(firstIndex - 1);
1591                     addLeadingCells(firstIndex - 1, getCellPosition(firstCell) - prevIndexSize);
1592                 }
1593             }
1594         }
1595 
1596         // Now throw away any cells that don't fit
1597         cull();
1598 
1599         // Finally, update the scroll bars
1600         updateScrollBarsAndCells(false);
1601         lastPosition = getPosition();
1602 
1603         // notify
1604         return delta; // TODO fake
1605     }
1606 
1607     /** {@inheritDoc} */
1608     @Override protected double computePrefWidth(double height) {
1609         double w = isVertical() ? getPrefBreadth(height) : getPrefLength();
1610         return w + vbar.prefWidth(-1);
1611     }
1612 
1613     /** {@inheritDoc} */
1614     @Override protected double computePrefHeight(double width) {
1615         double h = isVertical() ? getPrefLength() : getPrefBreadth(width);
1616         return h + hbar.prefHeight(-1);
1617     }
1618 
1619     /**
1620      * Return a cell for the given index. This may be called for any cell,
1621      * including beyond the range defined by cellCount, in which case an
1622      * empty cell will be returned. The returned value should not be stored for
1623      * any reason.
1624      */
1625     public T getCell(int index) {
1626         // If there are cells, then we will attempt to get an existing cell
1627         if (! cells.isEmpty()) {
1628             // First check the cells that have already been created and are
1629             // in use. If this call returns a value, then we can use it
1630             T cell = getVisibleCell(index);
1631             if (cell != null) return cell;
1632         }
1633 
1634         // check the pile
1635         for (int i = 0; i < pile.size(); i++) {
1636             T cell = pile.get(i);
1637             if (getCellIndex(cell) == index) {
1638                 // Note that we don't remove from the pile: if we do it leads
1639                 // to a severe performance decrease. This seems to be OK, as
1640                 // getCell() is only used for cell measurement purposes.
1641                 // pile.remove(i);
1642                 return cell;
1643             }
1644         }
1645 
1646         if (pile.size() > 0) {
1647             return pile.get(0);
1648         }
1649 
1650         // We need to use the accumCell and return that
1651         if (accumCell == null) {
1652             Callback<VirtualFlow<T>,T> cellFactory = getCellFactory();
1653             if (cellFactory != null) {
1654                 accumCell = cellFactory.call(this);
1655                 accumCell.getProperties().put(NEW_CELL, null);
1656                 accumCellParent.getChildren().setAll(accumCell);
1657 
1658                 // Note the screen reader will attempt to find all
1659                 // the items inside the view to calculate the item count.
1660                 // Having items under different parents (sheet and accumCellParent)
1661                 // leads the screen reader to compute wrong values.
1662                 // The regular scheme to provide items to the screen reader
1663                 // uses getPrivateCell(), which places the item in the sheet.
1664                 // The accumCell, and its children, should be ignored by the
1665                 // screen reader.
1666                 accumCell.setAccessibleRole(AccessibleRole.NODE);
1667                 accumCell.getChildrenUnmodifiable().addListener((Observable c) -> {
1668                     for (Node n : accumCell.getChildrenUnmodifiable()) {
1669                         n.setAccessibleRole(AccessibleRole.NODE);
1670                     }
1671                 });
1672             }
1673         }
1674         setCellIndex(accumCell, index);
1675         resizeCellSize(accumCell);
1676         return accumCell;
1677     }
1678 
1679     /**
1680      * The VirtualFlow uses this method to set a cells index (rather than calling
1681      * {@link IndexedCell#updateIndex(int)} directly), so it is a perfect place
1682      * for subclasses to override if this if of interest.
1683      *
1684      * @param cell The cell whose index will be updated.
1685      * @param index The new index for the cell.
1686      */
1687     protected void setCellIndex(T cell, int index) {
1688         assert cell != null;
1689 
1690         cell.updateIndex(index);
1691 
1692         // make sure the cell is sized correctly. This is important for both
1693         // general layout of cells in a VirtualFlow, but also in cases such as
1694         // RT-34333, where the sizes were being reported incorrectly to the
1695         // ComboBox popup.
1696         if ((cell.isNeedsLayout() && cell.getScene() != null) || cell.getProperties().containsKey(NEW_CELL)) {
1697             cell.applyCss();
1698             cell.getProperties().remove(NEW_CELL);
1699         }
1700     }
1701 
1702     /**
1703      * Return the index for a given cell. This allows subclasses to customise
1704      * how cell indices are retrieved.
1705      */
1706     protected int getCellIndex(T cell){
1707         return cell.getIndex();
1708     }
1709 
1710 
1711 
1712     /***************************************************************************
1713      *                                                                         *
1714      * Private implementation                                                  *
1715      *                                                                         *
1716      **************************************************************************/
1717 
1718     final VirtualScrollBar getHbar() {
1719         return hbar;
1720     }
1721     final VirtualScrollBar getVbar() {
1722         return vbar;
1723     }
1724 
1725     /**
1726      * The maximum preferred size in the non-virtual direction. For example,
1727      * if vertical, then this is the max pref width of all cells encountered.
1728      * <p>
1729      * In general, this is the largest preferred size in the non-virtual
1730      * direction that we have ever encountered. We don't reduce this size
1731      * unless instructed to do so, so as to reduce the amount of scroll bar
1732      * jitter. The access on this variable is package ONLY FOR TESTING.
1733      */
1734     private double maxPrefBreadth;
1735     private final void setMaxPrefBreadth(double value) {
1736         this.maxPrefBreadth = value;
1737     }
1738     final double getMaxPrefBreadth() {
1739         return maxPrefBreadth;
1740     }
1741 
1742     /**
1743      * The breadth of the viewport portion of the VirtualFlow as computed during
1744      * the layout pass. In a vertical flow this would be the same as the clip
1745      * view width. In a horizontal flow this is the clip view height.
1746      * The access on this variable is package ONLY FOR TESTING.
1747      */
1748     private double viewportBreadth;
1749     private final void setViewportBreadth(double value) {
1750         this.viewportBreadth = value;
1751     }
1752     private final double getViewportBreadth() {
1753         return viewportBreadth;
1754     }
1755 
1756     /**
1757      * The length of the viewport portion of the VirtualFlow as computed
1758      * during the layout pass. In a vertical flow this would be the same as the
1759      * clip view height. In a horizontal flow this is the clip view width.
1760      * The access on this variable is package ONLY FOR TESTING.
1761      */
1762     private double viewportLength;
1763     void setViewportLength(double value) {
1764         this.viewportLength = value;
1765     }
1766     double getViewportLength() {
1767         return viewportLength;
1768     }
1769 
1770     /**
1771      * Compute and return the length of the cell for the given index. This is
1772      * called both internally when adjusting by pixels, and also at times
1773      * by PositionMapper (see the getItemSize callback). When called by
1774      * PositionMapper, it is possible that it will be called for some index
1775      * which is not associated with any cell, so we have to do a bit of work
1776      * to use a cell as a helper for computing cell size in some cases.
1777      */
1778     double getCellLength(int index) {
1779         if (fixedCellSizeEnabled) return getFixedCellSize();
1780 
1781         T cell = getCell(index);
1782         double length = getCellLength(cell);
1783         releaseCell(cell);
1784         return length;
1785     }
1786 
1787     /**
1788      */
1789     double getCellBreadth(int index) {
1790         T cell = getCell(index);
1791         double b = getCellBreadth(cell);
1792         releaseCell(cell);
1793         return b;
1794     }
1795 
1796     /**
1797      * Gets the length of a specific cell
1798      */
1799     double getCellLength(T cell) {
1800         if (cell == null) return 0;
1801         if (fixedCellSizeEnabled) return getFixedCellSize();
1802 
1803         return isVertical() ?
1804                 cell.getLayoutBounds().getHeight()
1805                 : cell.getLayoutBounds().getWidth();
1806     }
1807 
1808     /**
1809      * Gets the breadth of a specific cell
1810      */
1811     double getCellBreadth(Cell cell) {
1812         return isVertical() ?
1813                 cell.prefWidth(-1)
1814                 : cell.prefHeight(-1);
1815     }
1816 
1817     /**
1818      * Gets the layout position of the cell along the length axis
1819      */
1820     double getCellPosition(T cell) {
1821         if (cell == null) return 0;
1822 
1823         return isVertical() ?
1824                 cell.getLayoutY()
1825                 : cell.getLayoutX();
1826     }
1827 
1828     private void positionCell(T cell, double position) {
1829         if (isVertical()) {
1830             cell.setLayoutX(0);
1831             cell.setLayoutY(snapSizeY(position));
1832         } else {
1833             cell.setLayoutX(snapSizeX(position));
1834             cell.setLayoutY(0);
1835         }
1836     }
1837 
1838     private void resizeCellSize(T cell) {
1839         if (cell == null) return;
1840 
1841         if (isVertical()) {
1842             double width = Math.max(getMaxPrefBreadth(), getViewportBreadth());
1843             cell.resize(width, fixedCellSizeEnabled ? getFixedCellSize() : Utils.boundedSize(cell.prefHeight(width), cell.minHeight(width), cell.maxHeight(width)));
1844         } else {
1845             double height = Math.max(getMaxPrefBreadth(), getViewportBreadth());
1846             cell.resize(fixedCellSizeEnabled ? getFixedCellSize() : Utils.boundedSize(cell.prefWidth(height), cell.minWidth(height), cell.maxWidth(height)), height);
1847         }
1848     }
1849 
1850     private List<T> getCells() {
1851         return cells;
1852     }
1853 
1854     // Returns last visible cell whose bounds are entirely within the viewport
1855     T getLastVisibleCellWithinViewPort() {
1856         if (cells.isEmpty() || getViewportLength() <= 0) return null;
1857 
1858         T cell;
1859         final double max = getViewportLength();
1860         for (int i = cells.size() - 1; i >= 0; i--) {
1861             cell = cells.get(i);
1862             if (cell.isEmpty()) continue;
1863 
1864             final double cellStart = getCellPosition(cell);
1865             final double cellEnd = cellStart + getCellLength(cell);
1866 
1867             // we use the magic +2 to allow for a little bit of fuzziness,
1868             // this is to help in situations such as RT-34407
1869             if (cellEnd <= (max + 2)) {
1870                 return cell;
1871             }
1872         }
1873 
1874         return null;
1875     }
1876 
1877     // Returns first visible cell whose bounds are entirely within the viewport
1878     T getFirstVisibleCellWithinViewPort() {
1879         if (cells.isEmpty() || getViewportLength() <= 0) return null;
1880 
1881         T cell;
1882         for (int i = 0; i < cells.size(); i++) {
1883             cell = cells.get(i);
1884             if (cell.isEmpty()) continue;
1885 
1886             final double cellStart = getCellPosition(cell);
1887             if (cellStart >= 0) {
1888                 return cell;
1889             }
1890         }
1891 
1892         return null;
1893     }
1894 
1895     /**
1896      * Adds all the cells prior to and including the given currentIndex, until
1897      * no more can be added without falling off the flow. The startOffset
1898      * indicates the distance from the leading edge (top) of the viewport to
1899      * the leading edge (top) of the currentIndex.
1900      */
1901     void addLeadingCells(int currentIndex, double startOffset) {
1902         // The offset will keep track of the distance from the top of the
1903         // viewport to the top of the current index. We will increment it
1904         // as we lay out leading cells.
1905         double offset = startOffset;
1906         // The index is the absolute index of the cell being laid out
1907         int index = currentIndex;
1908 
1909         // Offset should really be the bottom of the current index
1910         boolean first = true; // first time in, we just fudge the offset and let
1911         // it be the top of the current index then redefine
1912         // it as the bottom of the current index thereafter
1913         // while we have not yet laid out so many cells that they would fall
1914         // off the flow, we will continue to create and add cells. The
1915         // offset is our indication of whether we can lay out additional
1916         // cells. If the offset is ever < 0, except in the case of the very
1917         // first cell, then we must quit.
1918         T cell = null;
1919 
1920         // special case for the position == 1.0, skip adding last invisible cell
1921         if (index == getCellCount() && offset == getViewportLength()) {
1922             index--;
1923             first = false;
1924         }
1925         while (index >= 0 && (offset > 0 || first)) {
1926             cell = getAvailableCell(index);
1927             setCellIndex(cell, index);
1928             resizeCellSize(cell); // resize must be after config
1929             cells.addFirst(cell);
1930 
1931             // A little gross but better than alternatives because it reduces
1932             // the number of times we have to update a cell or compute its
1933             // size. The first time into this loop "offset" is actually the
1934             // top of the current index. On all subsequent visits, it is the
1935             // bottom of the current index.
1936             if (first) {
1937                 first = false;
1938             } else {
1939                 offset -= getCellLength(cell);
1940             }
1941 
1942             // Position the cell, and update the maxPrefBreadth variable as we go.
1943             positionCell(cell, offset);
1944             setMaxPrefBreadth(Math.max(getMaxPrefBreadth(), getCellBreadth(cell)));
1945             cell.setVisible(true);
1946             --index;
1947         }
1948 
1949         // There are times when after laying out the cells we discover that
1950         // the top of the first cell which represents index 0 is below the top
1951         // of the viewport. In these cases, we have to adjust the cells up
1952         // and reset the mapper position. This might happen when items got
1953         // removed at the top or when the viewport size increased.
1954         if (cells.size() > 0) {
1955             cell = cells.getFirst();
1956             int firstIndex = getCellIndex(cell);
1957             double firstCellPos = getCellPosition(cell);
1958             if (firstIndex == 0 && firstCellPos > 0) {
1959                 setPosition(0.0f);
1960                 offset = 0;
1961                 for (int i = 0; i < cells.size(); i++) {
1962                     cell = cells.get(i);
1963                     positionCell(cell, offset);
1964                     offset += getCellLength(cell);
1965                 }
1966             }
1967         } else {
1968             // reset scrollbar to top, so if the flow sees cells again it starts at the top
1969             vbar.setValue(0);
1970             hbar.setValue(0);
1971         }
1972     }
1973 
1974     /**
1975      * Adds all the trailing cells that come <em>after</em> the last index in
1976      * the cells ObservableList.
1977      */
1978     boolean addTrailingCells(boolean fillEmptyCells) {
1979         // If cells is empty then addLeadingCells bailed for some reason and
1980         // we're hosed, so just punt
1981         if (cells.isEmpty()) return false;
1982 
1983         // While we have not yet laid out so many cells that they would fall
1984         // off the flow, so we will continue to create and add cells. When the
1985         // offset becomes greater than the width/height of the flow, then we
1986         // know we cannot add any more cells.
1987         T startCell = cells.getLast();
1988         double offset = getCellPosition(startCell) + getCellLength(startCell);
1989         int index = getCellIndex(startCell) + 1;
1990         final int cellCount = getCellCount();
1991         boolean filledWithNonEmpty = index <= cellCount;
1992 
1993         final double viewportLength = getViewportLength();
1994 
1995         // Fix for RT-37421, which was a regression caused by RT-36556
1996         if (offset < 0 && !fillEmptyCells) {
1997             return false;
1998         }
1999 
2000         //
2001         // RT-36507: viewportLength gives the maximum number of
2002         // additional cells that should ever be able to fit in the viewport if
2003         // every cell had a height of 1. If index ever exceeds this count,
2004         // then offset is not incrementing fast enough, or at all, which means
2005         // there is something wrong with the cell size calculation.
2006         //
2007         final double maxCellCount = viewportLength;
2008         while (offset < viewportLength) {
2009             if (index >= cellCount) {
2010                 if (offset < viewportLength) filledWithNonEmpty = false;
2011                 if (! fillEmptyCells) return filledWithNonEmpty;
2012                 // RT-36507 - return if we've exceeded the maximum
2013                 if (index > maxCellCount) {
2014                     final PlatformLogger logger = Logging.getControlsLogger();
2015                     if (logger.isLoggable(PlatformLogger.Level.INFO)) {
2016                         logger.info("index exceeds maxCellCount. Check size calculations for " + startCell.getClass());
2017                     }
2018                     return filledWithNonEmpty;
2019                 }
2020             }
2021             T cell = getAvailableCell(index);
2022             setCellIndex(cell, index);
2023             resizeCellSize(cell); // resize happens after config!
2024             cells.addLast(cell);
2025 
2026             // Position the cell and update the max pref
2027             positionCell(cell, offset);
2028             setMaxPrefBreadth(Math.max(getMaxPrefBreadth(), getCellBreadth(cell)));
2029 
2030             offset += getCellLength(cell);
2031             cell.setVisible(true);
2032             ++index;
2033         }
2034 
2035         // Discover whether the first cell coincides with index #0. If after
2036         // adding all the trailing cells we find that a) the first cell was
2037         // not index #0 and b) there are trailing cells, then we have a
2038         // problem. We need to shift all the cells down and add leading cells,
2039         // one at a time, until either the very last non-empty cells is aligned
2040         // with the bottom OR we have laid out cell index #0 at the first
2041         // position.
2042         T firstCell = cells.getFirst();
2043         index = getCellIndex(firstCell);
2044         T lastNonEmptyCell = getLastVisibleCell();
2045         double start = getCellPosition(firstCell);
2046         double end = getCellPosition(lastNonEmptyCell) + getCellLength(lastNonEmptyCell);
2047         if ((index != 0 || (index == 0 && start < 0)) && fillEmptyCells &&
2048                 lastNonEmptyCell != null && getCellIndex(lastNonEmptyCell) == cellCount - 1 && end < viewportLength) {
2049 
2050             double prospectiveEnd = end;
2051             double distance = viewportLength - end;
2052             while (prospectiveEnd < viewportLength && index != 0 && (-start) < distance) {
2053                 index--;
2054                 T cell = getAvailableCell(index);
2055                 setCellIndex(cell, index);
2056                 resizeCellSize(cell); // resize must be after config
2057                 cells.addFirst(cell);
2058                 double cellLength = getCellLength(cell);
2059                 start -= cellLength;
2060                 prospectiveEnd += cellLength;
2061                 positionCell(cell, start);
2062                 setMaxPrefBreadth(Math.max(getMaxPrefBreadth(), getCellBreadth(cell)));
2063                 cell.setVisible(true);
2064             }
2065 
2066             // The amount by which to translate the cells down
2067             firstCell = cells.getFirst();
2068             start = getCellPosition(firstCell);
2069             double delta = viewportLength - end;
2070             if (getCellIndex(firstCell) == 0 && delta > (-start)) {
2071                 delta = (-start);
2072             }
2073             // Move things
2074             for (int i = 0; i < cells.size(); i++) {
2075                 T cell = cells.get(i);
2076                 positionCell(cell, getCellPosition(cell) + delta);
2077             }
2078 
2079             // Check whether the first cell, subsequent to our adjustments, is
2080             // now index #0 and aligned with the top. If so, change the position
2081             // to be at 0 instead of 1.
2082             start = getCellPosition(firstCell);
2083             if (getCellIndex(firstCell) == 0 && start == 0) {
2084                 setPosition(0);
2085             } else if (getPosition() != 1) {
2086                 setPosition(1);
2087             }
2088         }
2089 
2090         return filledWithNonEmpty;
2091     }
2092 
2093     void reconfigureCells() {
2094         needsReconfigureCells = true;
2095         requestLayout();
2096     }
2097 
2098     void recreateCells() {
2099         needsRecreateCells = true;
2100         requestLayout();
2101     }
2102 
2103     void rebuildCells() {
2104         needsRebuildCells = true;
2105         requestLayout();
2106     }
2107 
2108     void requestCellLayout() {
2109         needsCellsLayout = true;
2110         requestLayout();
2111     }
2112 
2113     void setCellDirty(int index) {
2114         dirtyCells.set(index);
2115         requestLayout();
2116     }
2117 
2118     private void startSBReleasedAnimation() {
2119         if (sbTouchTimeline == null) {
2120             /*
2121             ** timeline to leave the scrollbars visible for a short
2122             ** while after a scroll/drag
2123             */
2124             sbTouchTimeline = new Timeline();
2125             sbTouchKF1 = new KeyFrame(Duration.millis(0), event -> {
2126                 tempVisibility = true;
2127                 requestLayout();
2128             });
2129 
2130             sbTouchKF2 = new KeyFrame(Duration.millis(1000), event -> {
2131                 if (touchDetected == false && mouseDown == false) {
2132                     tempVisibility = false;
2133                     requestLayout();
2134                 }
2135             });
2136             sbTouchTimeline.getKeyFrames().addAll(sbTouchKF1, sbTouchKF2);
2137         }
2138         sbTouchTimeline.playFromStart();
2139     }
2140 
2141     private void scrollBarOn() {
2142         tempVisibility = true;
2143         requestLayout();
2144     }
2145 
2146     void updateHbar() {
2147         if (! isVisible() || getScene() == null) return;
2148         // Bring the clipView.clipX back to 0 if control is vertical or
2149         // the hbar isn't visible (fix for RT-11666)
2150         if (isVertical()) {
2151             if (hbar.isVisible()) {
2152                 clipView.setClipX(hbar.getValue());
2153             } else {
2154                 // all cells are now less than the width of the flow,
2155                 // so we should shift the hbar/clip such that
2156                 // everything is visible in the viewport.
2157                 clipView.setClipX(0);
2158                 hbar.setValue(0);
2159             }
2160         }
2161     }
2162 
2163     /**
2164      * @return true if bar visibility changed
2165      */
2166     private boolean computeBarVisiblity() {
2167         if (cells.isEmpty()) {
2168             // In case no cells are set yet, we assume no bars are needed
2169             needLengthBar = false;
2170             needBreadthBar = false;
2171             return true;
2172         }
2173 
2174         final boolean isVertical = isVertical();
2175         boolean barVisibilityChanged = false;
2176 
2177         VirtualScrollBar breadthBar = isVertical ? hbar : vbar;
2178         VirtualScrollBar lengthBar = isVertical ? vbar : hbar;
2179 
2180         final double viewportBreadth = getViewportBreadth();
2181 
2182         final int cellsSize = cells.size();
2183         final int cellCount = getCellCount();
2184         for (int i = 0; i < 2; i++) {
2185             final boolean lengthBarVisible = getPosition() > 0
2186                     || cellCount > cellsSize
2187                     || (cellCount == cellsSize && (getCellPosition(cells.getLast()) + getCellLength(cells.getLast())) > getViewportLength())
2188                     || (cellCount == cellsSize - 1 && barVisibilityChanged && needBreadthBar);
2189 
2190             if (lengthBarVisible ^ needLengthBar) {
2191                 needLengthBar = lengthBarVisible;
2192                 barVisibilityChanged = true;
2193             }
2194 
2195             // second conditional removed for RT-36669.
2196             final boolean breadthBarVisible = (maxPrefBreadth > viewportBreadth);// || (needLengthBar && maxPrefBreadth > (viewportBreadth - lengthBarBreadth));
2197             if (breadthBarVisible ^ needBreadthBar) {
2198                 needBreadthBar = breadthBarVisible;
2199                 barVisibilityChanged = true;
2200             }
2201         }
2202 
2203         // Start by optimistically deciding whether the length bar and
2204         // breadth bar are needed and adjust the viewport dimensions
2205         // accordingly. If during layout we find that one or the other of the
2206         // bars actually is needed, then we will perform a cleanup pass
2207 
2208         if (!Properties.IS_TOUCH_SUPPORTED) {
2209             updateViewportDimensions();
2210             breadthBar.setVisible(needBreadthBar);
2211             lengthBar.setVisible(needLengthBar);
2212         } else {
2213             breadthBar.setVisible(needBreadthBar && tempVisibility);
2214             lengthBar.setVisible(needLengthBar && tempVisibility);
2215         }
2216 
2217         return barVisibilityChanged;
2218     }
2219 
2220     private void updateViewportDimensions() {
2221         final boolean isVertical = isVertical();
2222         final double breadthBarLength = isVertical ? snapSizeY(hbar.prefHeight(-1)) : snapSizeX(vbar.prefWidth(-1));
2223         final double lengthBarBreadth = isVertical ? snapSizeX(vbar.prefWidth(-1)) : snapSizeY(hbar.prefHeight(-1));
2224 
2225         setViewportBreadth((isVertical ? getWidth() : getHeight()) - (needLengthBar ? lengthBarBreadth : 0));
2226         setViewportLength((isVertical ? getHeight() : getWidth()) - (needBreadthBar ? breadthBarLength : 0));
2227     }
2228 
2229     private void initViewport() {
2230         // Initialize the viewportLength and viewportBreadth to match the
2231         // width/height of the flow
2232         final boolean isVertical = isVertical();
2233 
2234         updateViewportDimensions();
2235 
2236         VirtualScrollBar breadthBar = isVertical ? hbar : vbar;
2237         VirtualScrollBar lengthBar = isVertical ? vbar : hbar;
2238 
2239         // If there has been a switch between the virtualized bar, then we
2240         // will want to do some stuff TODO.
2241         breadthBar.setVirtual(false);
2242         lengthBar.setVirtual(true);
2243     }
2244 
2245     private void updateScrollBarsAndCells(boolean recreate) {
2246         // Assign the hbar and vbar to the breadthBar and lengthBar so as
2247         // to make some subsequent calculations easier.
2248         final boolean isVertical = isVertical();
2249         VirtualScrollBar breadthBar = isVertical ? hbar : vbar;
2250         VirtualScrollBar lengthBar = isVertical ? vbar : hbar;
2251 
2252         // We may have adjusted the viewport length and breadth after the
2253         // layout due to scroll bars becoming visible. So we need to perform
2254         // a follow up pass and resize and shift all the cells to fit the
2255         // viewport. Note that the prospective viewport size is always >= the
2256         // final viewport size, so we don't have to worry about adding
2257         // cells during this cleanup phase.
2258         fitCells();
2259 
2260         // Update cell positions.
2261         // When rebuilding the cells, we add the cells and along the way compute
2262         // the maxPrefBreadth. Based on the computed value, we may add
2263         // the breadth scrollbar which changes viewport length, so we need
2264         // to re-position the cells.
2265         if (!cells.isEmpty()) {
2266             final double currOffset = -computeViewportOffset(getPosition());
2267             final int currIndex = computeCurrentIndex() - cells.getFirst().getIndex();
2268             final int size = cells.size();
2269 
2270             // position leading cells
2271             double offset = currOffset;
2272 
2273             for (int i = currIndex - 1; i >= 0 && i < size; i--) {
2274                 final T cell = cells.get(i);
2275 
2276                 offset -= getCellLength(cell);
2277 
2278                 positionCell(cell, offset);
2279             }
2280 
2281             // position trailing cells
2282             offset = currOffset;
2283             for (int i = currIndex; i >= 0 && i < size; i++) {
2284                 final T cell = cells.get(i);
2285                 positionCell(cell, offset);
2286 
2287                 offset += getCellLength(cell);
2288             }
2289         }
2290 
2291         // Toggle visibility on the corner
2292         corner.setVisible(breadthBar.isVisible() && lengthBar.isVisible());
2293 
2294         double sumCellLength = 0;
2295         double flowLength = (isVertical ? getHeight() : getWidth()) -
2296                 (breadthBar.isVisible() ? breadthBar.prefHeight(-1) : 0);
2297 
2298         final double viewportBreadth = getViewportBreadth();
2299         final double viewportLength = getViewportLength();
2300 
2301         // Now position and update the scroll bars
2302         if (breadthBar.isVisible()) {
2303             /*
2304             ** Positioning the ScrollBar
2305             */
2306             if (!Properties.IS_TOUCH_SUPPORTED) {
2307                 if (isVertical) {
2308                     hbar.resizeRelocate(0, viewportLength,
2309                             viewportBreadth, hbar.prefHeight(viewportBreadth));
2310                 } else {
2311                     vbar.resizeRelocate(viewportLength, 0,
2312                             vbar.prefWidth(viewportBreadth), viewportBreadth);
2313                 }
2314             }
2315             else {
2316                 if (isVertical) {
2317                     hbar.resizeRelocate(0, (viewportLength-hbar.getHeight()),
2318                             viewportBreadth, hbar.prefHeight(viewportBreadth));
2319                 } else {
2320                     vbar.resizeRelocate((viewportLength-vbar.getWidth()), 0,
2321                             vbar.prefWidth(viewportBreadth), viewportBreadth);
2322                 }
2323             }
2324 
2325             if (getMaxPrefBreadth() != -1) {
2326                 double newMax = Math.max(1, getMaxPrefBreadth() - viewportBreadth);
2327                 if (newMax != breadthBar.getMax()) {
2328                     breadthBar.setMax(newMax);
2329 
2330                     double breadthBarValue = breadthBar.getValue();
2331                     boolean maxed = breadthBarValue != 0 && newMax == breadthBarValue;
2332                     if (maxed || breadthBarValue > newMax) {
2333                         breadthBar.setValue(newMax);
2334                     }
2335 
2336                     breadthBar.setVisibleAmount((viewportBreadth / getMaxPrefBreadth()) * newMax);
2337                 }
2338             }
2339         }
2340 
2341         // determine how many cells there are on screen so that the scrollbar
2342         // thumb can be appropriately sized
2343         if (recreate && (lengthBar.isVisible() || Properties.IS_TOUCH_SUPPORTED)) {
2344             final int cellCount = getCellCount();
2345             int numCellsVisibleOnScreen = 0;
2346             for (int i = 0, max = cells.size(); i < max; i++) {
2347                 T cell = cells.get(i);
2348                 if (cell != null && !cell.isEmpty()) {
2349                     sumCellLength += (isVertical ? cell.getHeight() : cell.getWidth());
2350                     if (sumCellLength > flowLength) {
2351                         break;
2352                     }
2353 
2354                     numCellsVisibleOnScreen++;
2355                 }
2356             }
2357 
2358             lengthBar.setMax(1);
2359             if (numCellsVisibleOnScreen == 0 && cellCount == 1) {
2360                 // special case to help resolve RT-17701 and the case where we have
2361                 // only a single row and it is bigger than the viewport
2362                 lengthBar.setVisibleAmount(flowLength / sumCellLength);
2363             } else {
2364                 lengthBar.setVisibleAmount(numCellsVisibleOnScreen / (float) cellCount);
2365             }
2366         }
2367 
2368         if (lengthBar.isVisible()) {
2369             // Fix for RT-11873. If this isn't here, we can have a situation where
2370             // the scrollbar scrolls endlessly. This is possible when the cell
2371             // count grows as the user hits the maximal position on the scrollbar
2372             // (i.e. the list size dynamically grows as the user needs more).
2373             //
2374             // This code was commented out to resolve RT-14477 after testing
2375             // whether RT-11873 can be recreated. It could not, and therefore
2376             // for now this code will remained uncommented until it is deleted
2377             // following further testing.
2378 //            if (lengthBar.getValue() == 1.0 && lastCellCount != cellCount) {
2379 //                lengthBar.setValue(0.99);
2380 //            }
2381 
2382             /*
2383             ** Positioning the ScrollBar
2384             */
2385             if (!Properties.IS_TOUCH_SUPPORTED) {
2386                 if (isVertical) {
2387                     vbar.resizeRelocate(viewportBreadth, 0, vbar.prefWidth(viewportLength), viewportLength);
2388                 } else {
2389                     hbar.resizeRelocate(0, viewportBreadth, viewportLength, hbar.prefHeight(-1));
2390                 }
2391             }
2392             else {
2393                 if (isVertical) {
2394                     vbar.resizeRelocate((viewportBreadth-vbar.getWidth()), 0, vbar.prefWidth(viewportLength), viewportLength);
2395                 } else {
2396                     hbar.resizeRelocate(0, (viewportBreadth-hbar.getHeight()), viewportLength, hbar.prefHeight(-1));
2397                 }
2398             }
2399         }
2400 
2401         if (corner.isVisible()) {
2402             if (!Properties.IS_TOUCH_SUPPORTED) {
2403                 corner.resize(vbar.getWidth(), hbar.getHeight());
2404                 corner.relocate(hbar.getLayoutX() + hbar.getWidth(), vbar.getLayoutY() + vbar.getHeight());
2405             }
2406             else {
2407                 corner.resize(vbar.getWidth(), hbar.getHeight());
2408                 corner.relocate(hbar.getLayoutX() + (hbar.getWidth()-vbar.getWidth()), vbar.getLayoutY() + (vbar.getHeight()-hbar.getHeight()));
2409                 hbar.resize(hbar.getWidth()-vbar.getWidth(), hbar.getHeight());
2410                 vbar.resize(vbar.getWidth(), vbar.getHeight()-hbar.getHeight());
2411             }
2412         }
2413 
2414         clipView.resize(snapSizeX(isVertical ? viewportBreadth : viewportLength),
2415                         snapSizeY(isVertical ? viewportLength : viewportBreadth));
2416 
2417         // If the viewportLength becomes large enough that all cells fit
2418         // within the viewport, then we want to update the value to match.
2419         if (getPosition() != lengthBar.getValue()) {
2420             lengthBar.setValue(getPosition());
2421         }
2422     }
2423 
2424     /**
2425      * Adjusts the cells location and size if necessary. The breadths of all
2426      * cells will be adjusted to fit the viewportWidth or maxPrefBreadth, and
2427      * the layout position will be updated if necessary based on index and
2428      * offset.
2429      */
2430     private void fitCells() {
2431         double size = Math.max(getMaxPrefBreadth(), getViewportBreadth());
2432         boolean isVertical = isVertical();
2433 
2434         // Note: Do not optimise this loop by pre-calculating the cells size and
2435         // storing that into a int value - this can lead to RT-32828
2436         for (int i = 0; i < cells.size(); i++) {
2437             Cell<?> cell = cells.get(i);
2438             if (isVertical) {
2439                 cell.resize(size, cell.prefHeight(size));
2440             } else {
2441                 cell.resize(cell.prefWidth(size), size);
2442             }
2443         }
2444     }
2445 
2446     private void cull() {
2447         final double viewportLength = getViewportLength();
2448         for (int i = cells.size() - 1; i >= 0; i--) {
2449             T cell = cells.get(i);
2450             double cellSize = getCellLength(cell);
2451             double cellStart = getCellPosition(cell);
2452             double cellEnd = cellStart + cellSize;
2453             if (cellStart >= viewportLength || cellEnd < 0) {
2454                 addToPile(cells.remove(i));
2455             }
2456         }
2457     }
2458 
2459     /**
2460      * After using the accum cell, it needs to be released!
2461      */
2462     private void releaseCell(T cell) {
2463         if (accumCell != null && cell == accumCell) {
2464             accumCell.updateIndex(-1);
2465         }
2466     }
2467 
2468     /**
2469      * This method is an experts-only method - if the requested index is not
2470      * already an existing visible cell, it will create a cell for the
2471      * given index and insert it into the sheet. From that point on it will be
2472      * unmanaged, and is up to the caller of this method to manage it.
2473      */
2474     T getPrivateCell(int index)  {
2475         T cell = null;
2476 
2477         // If there are cells, then we will attempt to get an existing cell
2478         if (! cells.isEmpty()) {
2479             // First check the cells that have already been created and are
2480             // in use. If this call returns a value, then we can use it
2481             cell = getVisibleCell(index);
2482             if (cell != null) {
2483                 // Force the underlying text inside the cell to be updated
2484                 // so that when the screen reader runs, it will match the
2485                 // text in the cell (force updateDisplayedText())
2486                 cell.layout();
2487                 return cell;
2488             }
2489         }
2490 
2491         // check the existing sheet children
2492         if (cell == null) {
2493             for (int i = 0; i < sheetChildren.size(); i++) {
2494                 T _cell = (T) sheetChildren.get(i);
2495                 if (getCellIndex(_cell) == index) {
2496                     return _cell;
2497                 }
2498             }
2499         }
2500 
2501         Callback<VirtualFlow<T>, T> cellFactory = getCellFactory();
2502         if (cellFactory != null) {
2503             cell = cellFactory.call(this);
2504         }
2505 
2506         if (cell != null) {
2507             setCellIndex(cell, index);
2508             resizeCellSize(cell);
2509             cell.setVisible(false);
2510             sheetChildren.add(cell);
2511             privateCells.add(cell);
2512         }
2513 
2514         return cell;
2515     }
2516 
2517     private final List<T> privateCells = new ArrayList<>();
2518 
2519     private void releaseAllPrivateCells() {
2520         sheetChildren.removeAll(privateCells);
2521     }
2522 
2523     /**
2524      * Puts the given cell onto the pile. This is called whenever a cell has
2525      * fallen off the flow's start.
2526      */
2527     private void addToPile(T cell) {
2528         assert cell != null;
2529         pile.addLast(cell);
2530     }
2531 
2532     private void cleanPile() {
2533         boolean wasFocusOwner = false;
2534 
2535         for (int i = 0, max = pile.size(); i < max; i++) {
2536             T cell = pile.get(i);
2537             wasFocusOwner = wasFocusOwner || doesCellContainFocus(cell);
2538             cell.setVisible(false);
2539         }
2540 
2541         // Fix for RT-35876: Rather than have the cells do weird things with
2542         // focus (in particular, have focus jump between cells), we return focus
2543         // to the VirtualFlow itself.
2544         if (wasFocusOwner) {
2545             requestFocus();
2546         }
2547     }
2548 
2549     private boolean doesCellContainFocus(Cell<?> c) {
2550         Scene scene = c.getScene();
2551         final Node focusOwner = scene == null ? null : scene.getFocusOwner();
2552 
2553         if (focusOwner != null) {
2554             if (c.equals(focusOwner)) {
2555                 return true;
2556             }
2557 
2558             Parent p = focusOwner.getParent();
2559             while (p != null && ! (p instanceof VirtualFlow)) {
2560                 if (c.equals(p)) {
2561                     return true;
2562                 }
2563                 p = p.getParent();
2564             }
2565         }
2566 
2567         return false;
2568     }
2569 
2570     private double getPrefBreadth(double oppDimension) {
2571         double max = getMaxCellWidth(10);
2572 
2573         // This primarily exists for the case where we do not want the breadth
2574         // to grow to ensure a golden ratio between width and height (for example,
2575         // when a ListView is used in a ComboBox - the width should not grow
2576         // just because items are being added to the ListView)
2577         if (oppDimension > -1) {
2578             double prefLength = getPrefLength();
2579             max = Math.max(max, prefLength * GOLDEN_RATIO_MULTIPLIER);
2580         }
2581 
2582         return max;
2583     }
2584 
2585     private double getPrefLength() {
2586         double sum = 0.0;
2587         int rows = Math.min(10, getCellCount());
2588         for (int i = 0; i < rows; i++) {
2589             sum += getCellLength(i);
2590         }
2591         return sum;
2592     }
2593 
2594     double getMaxCellWidth(int rowsToCount) {
2595         double max = 0.0;
2596 
2597         // we always measure at least one row
2598         int rows = Math.max(1, rowsToCount == -1 ? getCellCount() : rowsToCount);
2599         for (int i = 0; i < rows; i++) {
2600             max = Math.max(max, getCellBreadth(i));
2601         }
2602         return max;
2603     }
2604 
2605     // Old PositionMapper
2606     /**
2607      * Given a position value between 0 and 1, compute and return the viewport
2608      * offset from the "current" cell associated with that position value.
2609      * That is, if the return value of this function where used as a translation
2610      * factor for a sheet that contained all the items, then the current
2611      * item would end up positioned correctly.
2612      */
2613     private double computeViewportOffset(double position) {
2614         double p = com.sun.javafx.util.Utils.clamp(0, position, 1);
2615         double fractionalPosition = p * getCellCount();
2616         int cellIndex = (int) fractionalPosition;
2617         double fraction = fractionalPosition - cellIndex;
2618         double cellSize = getCellLength(cellIndex);
2619         double pixelOffset = cellSize * fraction;
2620         double viewportOffset = getViewportLength() * p;
2621         return pixelOffset - viewportOffset;
2622     }
2623 
2624     private void adjustPositionToIndex(int index) {
2625         int cellCount = getCellCount();
2626         if (cellCount <= 0) {
2627             setPosition(0.0f);
2628         } else {
2629             setPosition(((double)index) / cellCount);
2630         }
2631     }
2632 
2633     /**
2634      * Adjust the position based on a delta of pixels. If negative, then the
2635      * position will be adjusted negatively. If positive, then the position will
2636      * be adjusted positively. If the pixel amount is too great for the range of
2637      * the position, then it will be clamped such that position is always
2638      * strictly between 0 and 1
2639      */
2640     private void adjustByPixelAmount(double numPixels) {
2641         if (numPixels == 0) return;
2642         // Starting from the current cell, we move in the direction indicated
2643         // by numPixels one cell at a team. For each cell, we discover how many
2644         // pixels the "position" line would move within that cell, and adjust
2645         // our count of numPixels accordingly. When we come to the "final" cell,
2646         // then we can take the remaining number of pixels and multiply it by
2647         // the "travel rate" of "p" within that cell to get the delta. Add
2648         // the delta to "p" to get position.
2649 
2650         // get some basic info about the list and the current cell
2651         boolean forward = numPixels > 0;
2652         int cellCount = getCellCount();
2653         double fractionalPosition = getPosition() * cellCount;
2654         int cellIndex = (int) fractionalPosition;
2655         if (forward && cellIndex == cellCount) return;
2656         double cellSize = getCellLength(cellIndex);
2657         double fraction = fractionalPosition - cellIndex;
2658         double pixelOffset = cellSize * fraction;
2659 
2660         // compute the percentage of "position" that represents each cell
2661         double cellPercent = 1.0 / cellCount;
2662 
2663         // To help simplify the algorithm, we pretend as though the current
2664         // position is at the beginning of the current cell. This reduces some
2665         // of the corner cases and provides a simpler algorithm without adding
2666         // any overhead to performance.
2667         double start = computeOffsetForCell(cellIndex);
2668         double end = cellSize + computeOffsetForCell(cellIndex + 1);
2669 
2670         // We need to discover the distance that the fictional "position line"
2671         // would travel within this cell, from its current position to the end.
2672         double remaining = end - start;
2673 
2674         // Keep track of the number of pixels left to travel
2675         double n = forward ?
2676               numPixels + pixelOffset - (getViewportLength() * getPosition()) - start
2677             : -numPixels + end - (pixelOffset - (getViewportLength() * getPosition()));
2678 
2679         // "p" represents the most recent value for position. This is always
2680         // based on the edge between two cells, except at the very end of the
2681         // algorithm where it is added to the computed "p" offset for the final
2682         // value of Position.
2683         double p = cellPercent * cellIndex;
2684 
2685         // Loop over the cells one at a time until either we reach the end of
2686         // the cells, or we find that the "n" will fall within the cell we're on
2687         while (n > remaining && ((forward && cellIndex < cellCount - 1) || (! forward && cellIndex > 0))) {
2688             if (forward) cellIndex++; else cellIndex--;
2689             n -= remaining;
2690             cellSize = getCellLength(cellIndex);
2691             start = computeOffsetForCell(cellIndex);
2692             end = cellSize + computeOffsetForCell(cellIndex + 1);
2693             remaining = end - start;
2694             p = cellPercent * cellIndex;
2695         }
2696 
2697         // if remaining is < n, then we must have hit an end, so as a
2698         // fast path, we can just set position to 1.0 or 0.0 and return
2699         // because we know we hit the end
2700         if (n > remaining) {
2701             setPosition(forward ? 1.0f : 0.0f);
2702         } else if (forward) {
2703             double rate = cellPercent / Math.abs(end - start);
2704             setPosition(p + (rate * n));
2705         } else {
2706             double rate = cellPercent / Math.abs(end - start);
2707             setPosition((p + cellPercent) - (rate * n));
2708         }
2709     }
2710 
2711     private int computeCurrentIndex() {
2712         return (int) (getPosition() * getCellCount());
2713     }
2714 
2715     /**
2716      * Given an item index, this function will compute and return the viewport
2717      * offset from the beginning of the specified item. Notice that because each
2718      * item has the same percentage of the position dedicated to it, and since
2719      * we are measuring from the start of each item, this is a very simple
2720      * calculation.
2721      */
2722     private double computeOffsetForCell(int itemIndex) {
2723         double cellCount = getCellCount();
2724         double p = com.sun.javafx.util.Utils.clamp(0, itemIndex, cellCount) / cellCount;
2725         return -(getViewportLength() * p);
2726     }
2727 
2728 //    /**
2729 //     * Adjust the position based on a chunk of pixels. The position is based
2730 //     * on the start of the scrollbar position.
2731 //     */
2732 //    private void adjustByPixelChunk(double numPixels) {
2733 //        setPosition(0);
2734 //        adjustByPixelAmount(numPixels);
2735 //    }
2736     // end of old PositionMapper code
2737 
2738 
2739 
2740 
2741     /***************************************************************************
2742      *                                                                         *
2743      * Support classes                                                         *
2744      *                                                                         *
2745      **************************************************************************/
2746 
2747     /**
2748      * A simple extension to Region that ensures that anything wanting to flow
2749      * outside of the bounds of the Region is clipped.
2750      */
2751     static class ClippedContainer extends Region {
2752 
2753         /**
2754          * The Node which is embedded within this {@code ClipView}.
2755          */
2756         private Node node;
2757         public Node getNode() { return this.node; }
2758         public void setNode(Node n) {
2759             this.node = n;
2760 
2761             getChildren().clear();
2762             getChildren().add(node);
2763         }
2764 
2765         public void setClipX(double clipX) {
2766             setLayoutX(-clipX);
2767             clipRect.setLayoutX(clipX);
2768         }
2769 
2770         public void setClipY(double clipY) {
2771             setLayoutY(-clipY);
2772             clipRect.setLayoutY(clipY);
2773         }
2774 
2775         private final Rectangle clipRect;
2776 
2777         public ClippedContainer(final VirtualFlow<?> flow) {
2778             if (flow == null) {
2779                 throw new IllegalArgumentException("VirtualFlow can not be null");
2780             }
2781 
2782             getStyleClass().add("clipped-container");
2783 
2784             // clipping
2785             clipRect = new Rectangle();
2786             clipRect.setSmooth(false);
2787             setClip(clipRect);
2788             // --- clipping
2789 
2790             super.widthProperty().addListener(valueModel -> {
2791                 clipRect.setWidth(getWidth());
2792             });
2793             super.heightProperty().addListener(valueModel -> {
2794                 clipRect.setHeight(getHeight());
2795             });
2796         }
2797     }
2798 
2799     /**
2800      * A List-like implementation that is exceedingly efficient for the purposes
2801      * of the VirtualFlow. Typically there is not much variance in the number of
2802      * cells -- it is always some reasonably consistent number. Yet for efficiency
2803      * in code, we like to use a linked list implementation so as to append to
2804      * start or append to end. However, at times when we need to iterate, LinkedList
2805      * is expensive computationally as well as requiring the construction of
2806      * temporary iterators.
2807      * <p>
2808      * This linked list like implementation is done using an array. It begins by
2809      * putting the first item in the center of the allocated array, and then grows
2810      * outward (either towards the first or last of the array depending on whether
2811      * we are inserting at the head or tail). It maintains an index to the start
2812      * and end of the array, so that it can efficiently expose iteration.
2813      * <p>
2814      * This class is package private solely for the sake of testing.
2815      */
2816     static class ArrayLinkedList<T> extends AbstractList<T> {
2817         /**
2818          * The array list backing this class. We default the size of the array
2819          * list to be fairly large so as not to require resizing during normal
2820          * use, and since that many ArrayLinkedLists won't be created it isn't
2821          * very painful to do so.
2822          */
2823         private final ArrayList<T> array;
2824 
2825         private int firstIndex = -1;
2826         private int lastIndex = -1;
2827 
2828         public ArrayLinkedList() {
2829             array = new ArrayList<T>(50);
2830 
2831             for (int i = 0; i < 50; i++) {
2832                 array.add(null);
2833             }
2834         }
2835 
2836         public T getFirst() {
2837             return firstIndex == -1 ? null : array.get(firstIndex);
2838         }
2839 
2840         public T getLast() {
2841             return lastIndex == -1 ? null : array.get(lastIndex);
2842         }
2843 
2844         public void addFirst(T cell) {
2845             // if firstIndex == -1 then that means this is the first item in the
2846             // list and we need to initialize firstIndex and lastIndex
2847             if (firstIndex == -1) {
2848                 firstIndex = lastIndex = array.size() / 2;
2849                 array.set(firstIndex, cell);
2850             } else if (firstIndex == 0) {
2851                 // we're already at the head of the array, so insert at position
2852                 // 0 and then increment the lastIndex to compensate
2853                 array.add(0, cell);
2854                 lastIndex++;
2855             } else {
2856                 // we're not yet at the head of the array, so insert at the
2857                 // firstIndex - 1 position and decrement first position
2858                 array.set(--firstIndex, cell);
2859             }
2860         }
2861 
2862         public void addLast(T cell) {
2863             // if lastIndex == -1 then that means this is the first item in the
2864             // list and we need to initialize the firstIndex and lastIndex
2865             if (firstIndex == -1) {
2866                 firstIndex = lastIndex = array.size() / 2;
2867                 array.set(lastIndex, cell);
2868             } else if (lastIndex == array.size() - 1) {
2869                 // we're at the end of the array so need to "add" so as to force
2870                 // the array to be expanded in size
2871                 array.add(++lastIndex, cell);
2872             } else {
2873                 array.set(++lastIndex, cell);
2874             }
2875         }
2876 
2877         public int size() {
2878             return firstIndex == -1 ? 0 : lastIndex - firstIndex + 1;
2879         }
2880 
2881         public boolean isEmpty() {
2882             return firstIndex == -1;
2883         }
2884 
2885         public T get(int index) {
2886             if (index > (lastIndex - firstIndex) || index < 0) {
2887                 // Commented out exception due to RT-29111
2888                 // throw new java.lang.ArrayIndexOutOfBoundsException();
2889                 return null;
2890             }
2891 
2892             return array.get(firstIndex + index);
2893         }
2894 
2895         public void clear() {
2896             for (int i = 0; i < array.size(); i++) {
2897                 array.set(i, null);
2898             }
2899 
2900             firstIndex = lastIndex = -1;
2901         }
2902 
2903         public T removeFirst() {
2904             if (isEmpty()) return null;
2905             return remove(0);
2906         }
2907 
2908         public T removeLast() {
2909             if (isEmpty()) return null;
2910             return remove(lastIndex - firstIndex);
2911         }
2912 
2913         public T remove(int index) {
2914             if (index > lastIndex - firstIndex || index < 0) {
2915                 throw new ArrayIndexOutOfBoundsException();
2916             }
2917 
2918             // if the index == 0, then we're removing the first
2919             // item and can simply set it to null in the array and increment
2920             // the firstIndex unless there is only one item, in which case
2921             // we have to also set first & last index to -1.
2922             if (index == 0) {
2923                 T cell = array.get(firstIndex);
2924                 array.set(firstIndex, null);
2925                 if (firstIndex == lastIndex) {
2926                     firstIndex = lastIndex = -1;
2927                 } else {
2928                     firstIndex++;
2929                 }
2930                 return cell;
2931             } else if (index == lastIndex - firstIndex) {
2932                 // if the index == lastIndex - firstIndex, then we're removing the
2933                 // last item and can simply set it to null in the array and
2934                 // decrement the lastIndex
2935                 T cell = array.get(lastIndex);
2936                 array.set(lastIndex--, null);
2937                 return cell;
2938             } else {
2939                 // if the index is somewhere in between, then we have to remove the
2940                 // item and decrement the lastIndex
2941                 T cell = array.get(firstIndex + index);
2942                 array.set(firstIndex + index, null);
2943                 for (int i = (firstIndex + index + 1); i <= lastIndex; i++) {
2944                     array.set(i - 1, array.get(i));
2945                 }
2946                 array.set(lastIndex--, null);
2947                 return cell;
2948             }
2949         }
2950     }
2951 }