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