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