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