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.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         cell = cells.getFirst();
1274         int firstIndex = getCellIndex(cell);
1275         double firstCellPos = getCellPosition(cell);
1276         if (firstIndex == 0 && firstCellPos > 0) {
1277             setPosition(0.0f);
1278             offset = 0;
1279             for (int i = 0; i < cells.size(); i++) {
1280                 cell = cells.get(i);
1281                 positionCell(cell, offset);
1282                 offset += getCellLength(cell);
1283             }
1284         }
1285     }
1286 
1287     /**
1288      * Adds all the trailing cells that come <em>after</em> the last index in
1289      * the cells ObservableList.
1290      */
1291     protected boolean addTrailingCells(boolean fillEmptyCells) {
1292         // If cells is empty then addLeadingCells bailed for some reason and
1293         // we're hosed, so just punt
1294         if (cells.isEmpty()) return false;
1295         
1296         // While we have not yet laid out so many cells that they would fall
1297         // off the flow, so we will continue to create and add cells. When the
1298         // offset becomes greater than the width/height of the flow, then we
1299         // know we cannot add any more cells.
1300         T startCell = cells.getLast();
1301         double offset = getCellPosition(startCell) + getCellLength(startCell);
1302         int index = getCellIndex(startCell) + 1;
1303         boolean filledWithNonEmpty = index <= cellCount;
1304 
1305         final double viewportLength = getViewportLength();
1306 
1307         // Fix for RT-37421, which was a regression caused by RT-36556
1308         if (offset < 0 && !fillEmptyCells) {
1309             return false;
1310         }
1311 
1312         //
1313         // RT-36507: viewportLength - offset gives the maximum number of
1314         // additional cells that should ever be able to fit in the viewport if
1315         // every cell had a height of 1. If index ever exceeds this count,
1316         // then offset is not incrementing fast enough, or at all, which means
1317         // there is something wrong with the cell size calculation.
1318         //
1319         final double maxCellCount = viewportLength - offset;
1320         while (offset < viewportLength) {
1321             if (index >= cellCount) {
1322                 if (offset < viewportLength) filledWithNonEmpty = false;
1323                 if (! fillEmptyCells) return filledWithNonEmpty;
1324                 // RT-36507 - return if we've exceeded the maximum
1325                 if (index > maxCellCount) {
1326                     final PlatformLogger logger = Logging.getControlsLogger();
1327                     if (logger.isLoggable(PlatformLogger.Level.INFO)) {
1328                         if (startCell != null) {
1329                             logger.info("index exceeds maxCellCount. Check size calculations for " + startCell.getClass());
1330                         } else {
1331                             logger.info("index exceeds maxCellCount");
1332                         }
1333                     }
1334                     return filledWithNonEmpty;
1335                 }
1336             }
1337             T cell = getAvailableCell(index);
1338             setCellIndex(cell, index);
1339             resizeCellSize(cell); // resize happens after config!
1340             cells.addLast(cell);
1341 
1342             // Position the cell and update the max pref
1343             positionCell(cell, offset);
1344             setMaxPrefBreadth(Math.max(getMaxPrefBreadth(), getCellBreadth(cell)));
1345 
1346             offset += getCellLength(cell);
1347             cell.setVisible(true);
1348             ++index;
1349         }
1350 
1351         // Discover whether the first cell coincides with index #0. If after
1352         // adding all the trailing cells we find that a) the first cell was
1353         // not index #0 and b) there are trailing cells, then we have a
1354         // problem. We need to shift all the cells down and add leading cells,
1355         // one at a time, until either the very last non-empty cells is aligned
1356         // with the bottom OR we have laid out cell index #0 at the first
1357         // position.
1358         T firstCell = cells.getFirst();
1359         index = getCellIndex(firstCell);
1360         T lastNonEmptyCell = getLastVisibleCell();
1361         double start = getCellPosition(firstCell);
1362         double end = getCellPosition(lastNonEmptyCell) + getCellLength(lastNonEmptyCell);
1363         if ((index != 0 || (index == 0 && start < 0)) && fillEmptyCells &&
1364                 lastNonEmptyCell != null && getCellIndex(lastNonEmptyCell) == cellCount - 1 && end < viewportLength) {
1365 
1366             double prospectiveEnd = end;
1367             double distance = viewportLength - end;
1368             while (prospectiveEnd < viewportLength && index != 0 && (-start) < distance) {
1369                 index--;
1370                 T cell = getAvailableCell(index);
1371                 setCellIndex(cell, index);
1372                 resizeCellSize(cell); // resize must be after config
1373                 cells.addFirst(cell);
1374                 double cellLength = getCellLength(cell);
1375                 start -= cellLength;
1376                 prospectiveEnd += cellLength;
1377                 positionCell(cell, start);
1378                 setMaxPrefBreadth(Math.max(getMaxPrefBreadth(), getCellBreadth(cell)));
1379                 cell.setVisible(true);
1380             }
1381 
1382             // The amount by which to translate the cells down
1383             firstCell = cells.getFirst();
1384             start = getCellPosition(firstCell);
1385             double delta = viewportLength - end;
1386             if (getCellIndex(firstCell) == 0 && delta > (-start)) {
1387                 delta = (-start);
1388             }
1389             // Move things
1390             for (int i = 0; i < cells.size(); i++) {
1391                 T cell = cells.get(i);
1392                 positionCell(cell, getCellPosition(cell) + delta);
1393             }
1394 
1395             // Check whether the first cell, subsequent to our adjustments, is
1396             // now index #0 and aligned with the top. If so, change the position
1397             // to be at 0 instead of 1.
1398             start = getCellPosition(firstCell);
1399             if (getCellIndex(firstCell) == 0 && start == 0) {
1400                 setPosition(0);
1401             } else if (getPosition() != 1) {
1402                 setPosition(1);
1403             }
1404         }
1405 
1406         return filledWithNonEmpty;
1407     }
1408 
1409     /**
1410      * @return true if bar visibility changed
1411      */
1412     private boolean computeBarVisiblity() {
1413         if (cells.isEmpty()) {
1414             // In case no cells are set yet, we assume no bars are needed
1415             needLengthBar = false;
1416             needBreadthBar = false;
1417             return true;
1418         }
1419 
1420         final boolean isVertical = isVertical();
1421         boolean barVisibilityChanged = false;
1422 
1423         VirtualScrollBar breadthBar = isVertical ? hbar : vbar;
1424         VirtualScrollBar lengthBar = isVertical ? vbar : hbar;
1425         double breadthBarLength = snapSize(isVertical ? hbar.prefHeight(-1) : vbar.prefWidth(-1));
1426         double lengthBarBreadth = snapSize(isVertical ? vbar.prefWidth(-1) : hbar.prefHeight(-1));
1427 
1428         final double viewportBreadth = getViewportBreadth();
1429 
1430         final int cellsSize = cells.size();
1431         for (int i = 0; i < 2; i++) {
1432             final boolean lengthBarVisible = getPosition() > 0 || cellCount > cellsSize ||
1433                     (cellCount == cellsSize && (getCellPosition(cells.getLast()) + getCellLength(cells.getLast())) > getViewportLength());
1434             if (lengthBarVisible ^ needLengthBar) {
1435                 needLengthBar = lengthBarVisible;
1436                 barVisibilityChanged = true;
1437             }
1438 
1439             // second conditional removed for RT-36669.
1440             final boolean breadthBarVisible = (maxPrefBreadth > viewportBreadth);// || (needLengthBar && maxPrefBreadth > (viewportBreadth - lengthBarBreadth));
1441             if (breadthBarVisible ^ needBreadthBar) {
1442                 needBreadthBar = breadthBarVisible;
1443                 barVisibilityChanged = true;
1444             }
1445         }
1446 
1447         // Start by optimistically deciding whether the length bar and
1448         // breadth bar are needed and adjust the viewport dimensions
1449         // accordingly. If during layout we find that one or the other of the
1450         // bars actually is needed, then we will perform a cleanup pass
1451 
1452         if (!BehaviorSkinBase.IS_TOUCH_SUPPORTED) {
1453             setViewportLength((isVertical ? getHeight() : getWidth()) - (needBreadthBar ? breadthBarLength : 0));
1454             setViewportBreadth((isVertical ? getWidth() : getHeight()) - (needLengthBar ? lengthBarBreadth : 0));
1455 
1456             breadthBar.setVisible(needBreadthBar);
1457             lengthBar.setVisible(needLengthBar);
1458         } else {
1459             breadthBar.setVisible(needBreadthBar && tempVisibility);
1460             lengthBar.setVisible(needLengthBar && tempVisibility);
1461         }
1462 
1463         return barVisibilityChanged;
1464     }
1465 
1466     private void initViewport() {
1467         // Initialize the viewportLength and viewportBreadth to match the
1468         // width/height of the flow
1469         final boolean isVertical = isVertical();
1470         double width = getWidth();
1471         double height = getHeight();
1472         setViewportLength(snapSize(isVertical ? height : width));
1473         setViewportBreadth(snapSize(isVertical ? width : height));
1474 
1475         VirtualScrollBar breadthBar = isVertical ? hbar : vbar;
1476         VirtualScrollBar lengthBar = isVertical ? vbar : hbar;
1477 
1478         // If there has been a switch between the virtualized bar, then we
1479         // will want to do some stuff TODO.
1480         breadthBar.setVirtual(false);
1481         lengthBar.setVirtual(true);
1482     }
1483 
1484     @Override protected void setWidth(double value) {
1485         if (value != lastWidth) {
1486             super.setWidth(value);
1487             sizeChanged = true;
1488             setNeedsLayout(true);
1489             requestLayout();
1490         }
1491     }
1492 
1493     @Override protected void setHeight(double value) {
1494         if (value != lastHeight) {
1495             super.setHeight(value);
1496             sizeChanged = true;
1497             setNeedsLayout(true);
1498             requestLayout();
1499         }
1500     }
1501 
1502     private void updateScrollBarsAndCells(boolean recreate) {
1503         // Assign the hbar and vbar to the breadthBar and lengthBar so as
1504         // to make some subsequent calculations easier.
1505         final boolean isVertical = isVertical();
1506         VirtualScrollBar breadthBar = isVertical ? hbar : vbar;
1507         VirtualScrollBar lengthBar = isVertical ? vbar : hbar;
1508         
1509         // Update cell positions.
1510         // When rebuilding the cells, we add the cells and along the way compute
1511         // the maxPrefBreadth. Based on the computed value, we may add
1512         // the breadth scrollbar which changes viewport length, so we need
1513         // to re-position the cells.
1514         if (!cells.isEmpty()) {
1515             final double currOffset = -computeViewportOffset(getPosition());
1516             final int currIndex = computeCurrentIndex() - cells.getFirst().getIndex();
1517             final int size = cells.size();
1518 
1519             // position leading cells
1520             double offset = currOffset;
1521             boolean first = true;
1522             for (int i = currIndex; i >= 0 && i < size; i--) {
1523                 final T cell = cells.get(i);
1524 
1525                 offset -= first ? 0.0 : getCellLength(cell);
1526                 first = false;
1527 
1528                 positionCell(cell, offset);
1529             }
1530 
1531             // position trailing cells
1532             offset = currOffset;
1533             for (int i = currIndex; i >= 0 && i < size; i++) {
1534                 final T cell = cells.get(i);
1535                 positionCell(cell, offset);
1536 
1537                 offset += getCellLength(cell);
1538             }
1539         }
1540 
1541         // Toggle visibility on the corner
1542         corner.setVisible(breadthBar.isVisible() && lengthBar.isVisible());
1543 
1544         double sumCellLength = 0;
1545         double flowLength = (isVertical ? getHeight() : getWidth()) -
1546             (breadthBar.isVisible() ? breadthBar.prefHeight(-1) : 0);
1547 
1548         final double viewportBreadth = getViewportBreadth();
1549         final double viewportLength = getViewportLength();
1550         
1551         // Now position and update the scroll bars
1552         if (breadthBar.isVisible()) {
1553             /*
1554             ** Positioning the ScrollBar
1555             */
1556             if (!BehaviorSkinBase.IS_TOUCH_SUPPORTED) {
1557                 if (isVertical) {
1558                     hbar.resizeRelocate(0, viewportLength,
1559                         viewportBreadth, hbar.prefHeight(viewportBreadth));
1560                 } else {
1561                     vbar.resizeRelocate(viewportLength, 0,
1562                         vbar.prefWidth(viewportBreadth), viewportBreadth);
1563                 }
1564             }
1565             else {
1566                 if (isVertical) {
1567                     hbar.resizeRelocate(0, (viewportLength-hbar.getHeight()),
1568                         viewportBreadth, hbar.prefHeight(viewportBreadth));
1569                 } else {
1570                     vbar.resizeRelocate((viewportLength-vbar.getWidth()), 0,
1571                         vbar.prefWidth(viewportBreadth), viewportBreadth);
1572                 }
1573             }
1574 
1575             if (getMaxPrefBreadth() != -1) {
1576                 double newMax = Math.max(1, getMaxPrefBreadth() - viewportBreadth);
1577                 if (newMax != breadthBar.getMax()) {
1578                     breadthBar.setMax(newMax);
1579 
1580                     double breadthBarValue = breadthBar.getValue();
1581                     boolean maxed = breadthBarValue != 0 && newMax == breadthBarValue;
1582                     if (maxed || breadthBarValue > newMax) {
1583                         breadthBar.setValue(newMax);
1584                     }
1585 
1586                     breadthBar.setVisibleAmount((viewportBreadth / getMaxPrefBreadth()) * newMax);
1587                 }
1588             }
1589         }
1590         
1591         // determine how many cells there are on screen so that the scrollbar
1592         // thumb can be appropriately sized
1593         if (recreate && (lengthBar.isVisible() || BehaviorSkinBase.IS_TOUCH_SUPPORTED)) {
1594             int numCellsVisibleOnScreen = 0;
1595             for (int i = 0, max = cells.size(); i < max; i++) {
1596                 T cell = cells.get(i);
1597                 if (cell != null && !cell.isEmpty()) {
1598                     sumCellLength += (isVertical ? cell.getHeight() : cell.getWidth());
1599                     if (sumCellLength > flowLength) {
1600                         break;
1601                     }
1602 
1603                     numCellsVisibleOnScreen++;
1604                 }
1605             }
1606 
1607             lengthBar.setMax(1);
1608             if (numCellsVisibleOnScreen == 0 && cellCount == 1) {
1609                     // special case to help resolve RT-17701 and the case where we have
1610                 // only a single row and it is bigger than the viewport
1611                 lengthBar.setVisibleAmount(flowLength / sumCellLength);
1612             } else {
1613                 lengthBar.setVisibleAmount(numCellsVisibleOnScreen / (float) cellCount);
1614             }
1615         }
1616 
1617         if (lengthBar.isVisible()) {
1618             // Fix for RT-11873. If this isn't here, we can have a situation where
1619             // the scrollbar scrolls endlessly. This is possible when the cell
1620             // count grows as the user hits the maximal position on the scrollbar
1621             // (i.e. the list size dynamically grows as the user needs more).
1622             //
1623             // This code was commented out to resolve RT-14477 after testing
1624             // whether RT-11873 can be recreated. It could not, and therefore
1625             // for now this code will remained uncommented until it is deleted
1626             // following further testing.
1627 //            if (lengthBar.getValue() == 1.0 && lastCellCount != cellCount) {
1628 //                lengthBar.setValue(0.99);
1629 //            }
1630 
1631             /*
1632             ** Positioning the ScrollBar
1633             */
1634             if (!BehaviorSkinBase.IS_TOUCH_SUPPORTED) {
1635                 if (isVertical) {
1636                     vbar.resizeRelocate(viewportBreadth, 0, vbar.prefWidth(viewportLength), viewportLength);
1637                 } else {
1638                     hbar.resizeRelocate(0, viewportBreadth, viewportLength, hbar.prefHeight(-1));
1639                 }
1640             }
1641             else {
1642                 if (isVertical) {
1643                     vbar.resizeRelocate((viewportBreadth-vbar.getWidth()), 0, vbar.prefWidth(viewportLength), viewportLength);
1644                 } else {
1645                     hbar.resizeRelocate(0, (viewportBreadth-hbar.getHeight()), viewportLength, hbar.prefHeight(-1));
1646                 }
1647             }
1648         }
1649 
1650         if (corner.isVisible()) {
1651             if (!BehaviorSkinBase.IS_TOUCH_SUPPORTED) {
1652                 corner.resize(vbar.getWidth(), hbar.getHeight());
1653                 corner.relocate(hbar.getLayoutX() + hbar.getWidth(), vbar.getLayoutY() + vbar.getHeight());
1654             }
1655             else {
1656                 corner.resize(vbar.getWidth(), hbar.getHeight());
1657                 corner.relocate(hbar.getLayoutX() + (hbar.getWidth()-vbar.getWidth()), vbar.getLayoutY() + (vbar.getHeight()-hbar.getHeight()));
1658                 hbar.resize(hbar.getWidth()-vbar.getWidth(), hbar.getHeight());
1659                 vbar.resize(vbar.getWidth(), vbar.getHeight()-hbar.getHeight());
1660             }
1661         }
1662 
1663         clipView.resize(snapSize(isVertical ? viewportBreadth : viewportLength),
1664                         snapSize(isVertical ? viewportLength : viewportBreadth));
1665 
1666         // We may have adjusted the viewport length and breadth after the
1667         // layout due to scroll bars becoming visible. So we need to perform
1668         // a follow up pass and resize and shift all the cells to fit the
1669         // viewport. Note that the prospective viewport size is always >= the
1670         // final viewport size, so we don't have to worry about adding
1671         // cells during this cleanup phase.
1672         fitCells();
1673 
1674         // If the viewportLength becomes large enough that all cells fit
1675         // within the viewport, then we want to update the value to match.
1676         if (getPosition() != lengthBar.getValue()) {
1677             lengthBar.setValue(getPosition());
1678         }
1679     }
1680 
1681     /**
1682      * Adjusts the cells location and size if necessary. The breadths of all
1683      * cells will be adjusted to fit the viewportWidth or maxPrefBreadth, and
1684      * the layout position will be updated if necessary based on index and
1685      * offset.
1686      */
1687     private void fitCells() {
1688         double size = Math.max(getMaxPrefBreadth(), getViewportBreadth());
1689         boolean isVertical = isVertical();
1690 
1691         // Note: Do not optimise this loop by pre-calculating the cells size and
1692         // storing that into a int value - this can lead to RT-32828
1693         for (int i = 0; i < cells.size(); i++) {
1694             Cell<?> cell = cells.get(i);
1695             if (isVertical) {
1696                 cell.resize(size, cell.getHeight());
1697             } else {
1698                 cell.resize(cell.getWidth(), size);
1699             }
1700         }
1701     }
1702 
1703     private void cull() {
1704         final double viewportLength = getViewportLength();
1705         for (int i = cells.size() - 1; i >= 0; i--) {
1706             T cell = cells.get(i);
1707             double cellSize = getCellLength(cell);
1708             double cellStart = getCellPosition(cell);
1709             double cellEnd = cellStart + cellSize;
1710             if (cellStart >= viewportLength || cellEnd < 0) {
1711                 addToPile(cells.remove(i));
1712             }
1713         }
1714     }
1715 
1716     /***************************************************************************
1717      *                                                                         *
1718      *                Helper functions for working with cells                  *
1719      *                                                                         *
1720      **************************************************************************/
1721 
1722     /**
1723      * Return the index for a given cell. This allows subclasses to customise
1724      * how cell indices are retrieved.
1725      */
1726     protected int getCellIndex(T cell){
1727         return cell.getIndex();
1728     }
1729 
1730 
1731     /**
1732      * Return a cell for the given index. This may be called for any cell,
1733      * including beyond the range defined by cellCount, in which case an
1734      * empty cell will be returned. The returned value should not be stored for
1735      * any reason.
1736      */
1737     public T getCell(int index) {
1738         // If there are cells, then we will attempt to get an existing cell
1739         if (! cells.isEmpty()) {
1740             // First check the cells that have already been created and are
1741             // in use. If this call returns a value, then we can use it
1742             T cell = getVisibleCell(index);
1743             if (cell != null) return cell;
1744         }
1745 
1746         // check the pile
1747         for (int i = 0; i < pile.size(); i++) {
1748             T cell = pile.get(i);
1749             if (getCellIndex(cell) == index) {
1750                 // Note that we don't remove from the pile: if we do it leads
1751                 // to a severe performance decrease. This seems to be OK, as
1752                 // getCell() is only used for cell measurement purposes.
1753                 // pile.remove(i);
1754                 return cell;
1755             }
1756         }
1757 
1758         if (pile.size() > 0) {
1759             return pile.get(0);
1760         }
1761 
1762         // We need to use the accumCell and return that
1763         if (accumCell == null) {
1764             Callback<VirtualFlow,T> createCell = getCreateCell();
1765             if (createCell != null) {
1766                 accumCell = createCell.call(this);
1767                 accumCellParent.getChildren().setAll(accumCell);
1768 
1769                 // Note the screen reader will attempt to find all
1770                 // the items inside the view to calculate the item count.
1771                 // Having items under different parents (sheet and accumCellParent)
1772                 // leads the screen reader to compute wrong values.
1773                 // The regular scheme to provide items to the screen reader
1774                 // uses getPrivateCell(), which places the item in the sheet.
1775                 // The accumCell, and its children, should be ignored by the
1776                 // screen reader. 
1777                 accumCell.setAccessibleRole(AccessibleRole.NODE);
1778                 accumCell.getChildrenUnmodifiable().addListener((Observable c) -> {
1779                     accumCell.getChildrenUnmodifiable().forEach(n -> n.setAccessibleRole(AccessibleRole.NODE));
1780                 });
1781             }
1782         }
1783         setCellIndex(accumCell, index);
1784         resizeCellSize(accumCell);
1785         return accumCell;
1786     }
1787 
1788     /**
1789      * After using the accum cell, it needs to be released!
1790      */
1791     private void releaseCell(T cell) {
1792         if (accumCell != null && cell == accumCell) {
1793             accumCell.updateIndex(-1);
1794         }
1795     }
1796 
1797     /**
1798      * This method is an experts-only method - if the requested index is not
1799      * already an existing visible cell, it will create a cell for the
1800      * given index and insert it into the sheet. From that point on it will be
1801      * unmanaged, and is up to the caller of this method to manage it.
1802      */
1803     T getPrivateCell(int index)  {
1804         T cell = null;
1805 
1806         // If there are cells, then we will attempt to get an existing cell
1807         if (! cells.isEmpty()) {
1808             // First check the cells that have already been created and are
1809             // in use. If this call returns a value, then we can use it
1810             cell = getVisibleCell(index);
1811             if (cell != null) {
1812                 // Force the underlying text inside the cell to be updated
1813                 // so that when the screen reader runs, it will match the
1814                 // text in the cell (force updateDisplayedText())
1815                 cell.layout();
1816                 return cell;
1817             }
1818         }
1819 
1820         // check the existing sheet children
1821         if (cell == null) {
1822             for (int i = 0; i < sheetChildren.size(); i++) {
1823                 T _cell = (T) sheetChildren.get(i);
1824                 if (getCellIndex(_cell) == index) {
1825                     return _cell;
1826                 }
1827             }
1828         }
1829 
1830         if (cell == null) {
1831             Callback<VirtualFlow, T> createCell = getCreateCell();
1832             if (createCell != null) {
1833                 cell = createCell.call(this);
1834             }
1835         }
1836 
1837         if (cell != null) {
1838             setCellIndex(cell, index);
1839             resizeCellSize(cell);
1840             cell.setVisible(false);
1841             sheetChildren.add(cell);
1842             privateCells.add(cell);
1843         }
1844 
1845         return cell;
1846     }
1847 
1848     private final List<T> privateCells = new ArrayList<>();
1849 
1850     private void releaseAllPrivateCells() {
1851         sheetChildren.removeAll(privateCells);
1852     }
1853 
1854     /**
1855      * Compute and return the length of the cell for the given index. This is
1856      * called both internally when adjusting by pixels, and also at times
1857      * by PositionMapper (see the getItemSize callback). When called by
1858      * PositionMapper, it is possible that it will be called for some index
1859      * which is not associated with any cell, so we have to do a bit of work
1860      * to use a cell as a helper for computing cell size in some cases.
1861      */
1862     protected double getCellLength(int index) {
1863         if (fixedCellSizeEnabled) return fixedCellSize;
1864         
1865         T cell = getCell(index);
1866         double length = getCellLength(cell);
1867         releaseCell(cell);
1868         return length;
1869     }
1870 
1871     /**
1872      */
1873     protected double getCellBreadth(int index) {
1874         T cell = getCell(index);
1875         double b = getCellBreadth(cell);
1876         releaseCell(cell);
1877         return b;
1878     }
1879 
1880     /**
1881      * Gets the length of a specific cell
1882      */
1883     protected double getCellLength(T cell) {
1884         if (cell == null) return 0;
1885         if (fixedCellSizeEnabled) return fixedCellSize;
1886 
1887         return isVertical() ?
1888             cell.getLayoutBounds().getHeight()
1889             : cell.getLayoutBounds().getWidth();
1890     }
1891 
1892 //    private double getCellPrefLength(T cell) {
1893 //        return isVertical() ?
1894 //            cell.prefHeight(-1)
1895 //            : cell.prefWidth(-1);
1896 //    }
1897 
1898     /**
1899      * Gets the breadth of a specific cell
1900      */
1901     protected double getCellBreadth(Cell cell) {
1902         return isVertical() ?
1903             cell.prefWidth(-1)
1904             : cell.prefHeight(-1);
1905     }
1906 
1907     /**
1908      * Gets the layout position of the cell along the length axis
1909      */
1910     protected double getCellPosition(T cell) {
1911         if (cell == null) return 0;
1912 
1913         return isVertical() ?
1914             cell.getLayoutY()
1915             : cell.getLayoutX();
1916     }
1917 
1918     protected void positionCell(T cell, double position) {
1919         if (isVertical()) {
1920             cell.setLayoutX(0);
1921             cell.setLayoutY(snapSize(position));
1922         } else {
1923             cell.setLayoutX(snapSize(position));
1924             cell.setLayoutY(0);
1925         }
1926     }
1927 
1928     protected void resizeCellSize(T cell) {
1929         if (cell == null) return;
1930         
1931         if (isVertical()) {
1932             double width = Math.max(getMaxPrefBreadth(), getViewportBreadth());
1933             cell.resize(width, fixedCellSizeEnabled ? fixedCellSize : Utils.boundedSize(cell.prefHeight(width), cell.minHeight(width), cell.maxHeight(width)));
1934         } else {
1935             double height = Math.max(getMaxPrefBreadth(), getViewportBreadth());
1936             cell.resize(fixedCellSizeEnabled ? fixedCellSize : Utils.boundedSize(cell.prefWidth(height), cell.minWidth(height), cell.maxWidth(height)), height);
1937         }
1938     }
1939 
1940     protected void setCellIndex(T cell, int index) {
1941         assert cell != null;
1942 
1943         cell.updateIndex(index);
1944 
1945         // make sure the cell is sized correctly. This is important for both
1946         // general layout of cells in a VirtualFlow, but also in cases such as
1947         // RT-34333, where the sizes were being reported incorrectly to the
1948         // ComboBox popup.
1949         if (cell.isNeedsLayout() && cell.getScene() != null) {
1950             cell.applyCss();
1951         }
1952     }
1953 
1954     /***************************************************************************
1955      *                                                                         *
1956      *                 Helper functions for cell management                    *
1957      *                                                                         *
1958      **************************************************************************/
1959 
1960     /**
1961      * Get a cell which can be used in the layout. This function will reuse
1962      * cells from the pile where possible, and will create new cells when
1963      * necessary.
1964      */
1965     protected T getAvailableCell(int prefIndex) {
1966         T cell = null;
1967         
1968         // Fix for RT-12822. We try to retrieve the cell from the pile rather
1969         // than just grab a random cell from the pile (or create another cell).
1970         for (int i = 0, max = pile.size(); i < max; i++) {
1971             T _cell = pile.get(i);
1972             assert _cell != null;
1973             
1974             if (getCellIndex(_cell) == prefIndex) {
1975                 cell = _cell;
1976                 pile.remove(i);
1977                 break;
1978             }
1979             cell = null;
1980         }
1981 
1982         if (cell == null) {
1983             if (pile.size() > 0) {
1984                 // we try to get a cell with an index that is the same even/odd
1985                 // as the prefIndex. This saves us from having to run so much
1986                 // css on the cell as it will not change from even to odd, or
1987                 // vice versa
1988                 final boolean prefIndexIsEven = (prefIndex & 1) == 0;
1989                 for (int i = 0, max = pile.size(); i < max; i++) {
1990                     final T c = pile.get(i);
1991                     final int cellIndex = getCellIndex(c);
1992 
1993                     if ((cellIndex & 1) == 0 && prefIndexIsEven) {
1994                         cell = c;
1995                         pile.remove(i);
1996                         break;
1997                     } else if ((cellIndex & 1) == 1 && ! prefIndexIsEven) {
1998                         cell = c;
1999                         pile.remove(i);
2000                         break;
2001                     }
2002                 }
2003 
2004                 if (cell == null) {
2005                     cell = pile.removeFirst();
2006                 }
2007             } else {
2008                 cell = getCreateCell().call(this);
2009             }
2010         }
2011 
2012         if (cell.getParent() == null) {
2013             sheetChildren.add(cell);
2014         }
2015         
2016         return cell;
2017     }
2018 
2019     // protected to allow subclasses to clean up
2020     protected void addAllToPile() {
2021         for (int i = 0, max = cells.size(); i < max; i++) {
2022             addToPile(cells.removeFirst());
2023         }
2024     }
2025 
2026     /**
2027      * Puts the given cell onto the pile. This is called whenever a cell has
2028      * fallen off the flow's start.
2029      */
2030     private void addToPile(T cell) {
2031         assert cell != null;
2032         pile.addLast(cell);
2033     }
2034 
2035     private void cleanPile() {
2036         boolean wasFocusOwner = false;
2037 
2038         for (int i = 0, max = pile.size(); i < max; i++) {
2039             T cell = pile.get(i);
2040             wasFocusOwner = wasFocusOwner || doesCellContainFocus(cell);
2041             cell.setVisible(false);
2042         }
2043 
2044         // Fix for RT-35876: Rather than have the cells do weird things with
2045         // focus (in particular, have focus jump between cells), we return focus
2046         // to the VirtualFlow itself.
2047         if (wasFocusOwner) {
2048             requestFocus();
2049         }
2050     }
2051 
2052     private boolean doesCellContainFocus(Cell<?> c) {
2053         Scene scene = c.getScene();
2054         final Node focusOwner = scene == null ? null : scene.getFocusOwner();
2055 
2056         if (focusOwner != null) {
2057             if (c.equals(focusOwner)) {
2058                 return true;
2059             }
2060 
2061             Parent p = focusOwner.getParent();
2062             while (p != null && ! (p instanceof VirtualFlow)) {
2063                 if (c.equals(p)) {
2064                     return true;
2065                 }
2066                 p = p.getParent();
2067             }
2068         }
2069 
2070         return false;
2071     }
2072 
2073     /**
2074      * Gets a cell for the given index if the cell has been created and laid out.
2075      * "Visible" is a bit of a misnomer, the cell might not be visible in the
2076      * viewport (it may be clipped), but does distinguish between cells that
2077      * have been created and are in use vs. those that are in the pile or
2078      * not created.
2079      */
2080     public T getVisibleCell(int index) {
2081         if (cells.isEmpty()) return null;
2082 
2083         // check the last index
2084         T lastCell = cells.getLast();
2085         int lastIndex = getCellIndex(lastCell);
2086         if (index == lastIndex) return lastCell;
2087 
2088         // check the first index
2089         T firstCell = cells.getFirst();
2090         int firstIndex = getCellIndex(firstCell);
2091         if (index == firstIndex) return firstCell;
2092 
2093         // if index is > firstIndex and < lastIndex then we can get the index
2094         if (index > firstIndex && index < lastIndex) {
2095             T cell = cells.get(index - firstIndex);
2096             if (getCellIndex(cell) == index) return cell;
2097         }
2098 
2099         // there is no visible cell for the specified index
2100         return null;
2101     }
2102 
2103     /**
2104      * Locates and returns the last non-empty IndexedCell that is currently
2105      * partially or completely visible. This function may return null if there
2106      * are no cells, or if the viewport length is 0.
2107      */
2108     public T getLastVisibleCell() {
2109         if (cells.isEmpty() || getViewportLength() <= 0) return null;
2110 
2111         T cell;
2112         for (int i = cells.size() - 1; i >= 0; i--) {
2113             cell = cells.get(i);
2114             if (! cell.isEmpty()) {
2115                 return cell;
2116             }
2117         }
2118 
2119         return null;
2120     }
2121 
2122     /**
2123      * Locates and returns the first non-empty IndexedCell that is partially or
2124      * completely visible. This really only ever returns null if there are no
2125      * cells or the viewport length is 0.
2126      */
2127     public T getFirstVisibleCell() {
2128         if (cells.isEmpty() || getViewportLength() <= 0) return null;
2129         T cell = cells.getFirst();
2130         return cell.isEmpty() ? null : cell;
2131     }
2132 
2133     // Returns last visible cell whose bounds are entirely within the viewport
2134     public T getLastVisibleCellWithinViewPort() {
2135         if (cells.isEmpty() || getViewportLength() <= 0) return null;
2136 
2137         T cell;
2138         final double max = getViewportLength();
2139         for (int i = cells.size() - 1; i >= 0; i--) {
2140             cell = cells.get(i);
2141             if (cell.isEmpty()) continue;
2142 
2143             final double cellStart = getCellPosition(cell);
2144             final double cellEnd = cellStart + getCellLength(cell);
2145 
2146             // we use the magic +2 to allow for a little bit of fuzziness,
2147             // this is to help in situations such as RT-34407
2148             if (cellEnd <= (max + 2)) {
2149                 return cell;
2150             }
2151         }
2152 
2153         return null;
2154     }
2155 
2156     // Returns first visible cell whose bounds are entirely within the viewport
2157     public T getFirstVisibleCellWithinViewPort() {
2158         if (cells.isEmpty() || getViewportLength() <= 0) return null;
2159 
2160         T cell;
2161         for (int i = 0; i < cells.size(); i++) {
2162             cell = cells.get(i);
2163             if (cell.isEmpty()) continue;
2164 
2165             final double cellStart = getCellPosition(cell);
2166             if (cellStart >= 0) {
2167                 return cell;
2168             }
2169         }
2170 
2171         return null;
2172     }
2173 
2174     /**
2175      * Adjust the position of cells so that the specified cell
2176      * will be positioned at the start of the viewport. The given cell must
2177      * already be "live". This is bad public API!
2178      */
2179     public void showAsFirst(T firstCell) {
2180         if (firstCell != null) {
2181             adjustPixels(getCellPosition(firstCell));
2182         }
2183     }
2184 
2185     /**
2186      * Adjust the position of cells so that the specified cell
2187      * will be positioned at the end of the viewport. The given cell must
2188      * already be "live". This is bad public API!
2189      */
2190     public void showAsLast(T lastCell) {
2191         if (lastCell != null) {
2192             adjustPixels(getCellPosition(lastCell) + getCellLength(lastCell) - getViewportLength());
2193         }
2194     }
2195 
2196     /**
2197      * Adjusts the cells such that the selected cell will be fully visible in
2198      * the viewport (but only just).
2199      */
2200     public void show(T cell) {
2201         if (cell != null) {
2202             final double start = getCellPosition(cell);
2203             final double length = getCellLength(cell);
2204             final double end = start + length;
2205             final double viewportLength = getViewportLength();
2206 
2207             if (start < 0) {
2208                 adjustPixels(start);
2209             } else if (end > viewportLength) {
2210                 adjustPixels(end - viewportLength);
2211             }
2212         }
2213     }
2214 
2215     public void show(int index) {
2216         T cell = getVisibleCell(index);
2217         if (cell != null) {
2218             show(cell);
2219         } else {
2220             // See if the previous index is a visible cell
2221             T prev = getVisibleCell(index - 1);
2222             if (prev != null) {
2223                 // Need to add a new cell and then we can show it
2224 //                layingOut = true;
2225                 cell = getAvailableCell(index);
2226                 setCellIndex(cell, index);
2227                 resizeCellSize(cell); // resize must be after config
2228                 cells.addLast(cell);
2229                 positionCell(cell, getCellPosition(prev) + getCellLength(prev));
2230                 setMaxPrefBreadth(Math.max(getMaxPrefBreadth(), getCellBreadth(cell)));
2231                 cell.setVisible(true);
2232                 show(cell);
2233 //                layingOut = false;
2234                 return;
2235             }
2236             // See if the next index is a visible cell
2237             T next = getVisibleCell(index + 1);
2238             if (next != null) {
2239 //                layingOut = true;
2240                 cell = getAvailableCell(index);
2241                 setCellIndex(cell, index);
2242                 resizeCellSize(cell); // resize must be after config
2243                 cells.addFirst(cell);
2244                 positionCell(cell, getCellPosition(next) - getCellLength(cell));
2245                 setMaxPrefBreadth(Math.max(getMaxPrefBreadth(), getCellBreadth(cell)));
2246                 cell.setVisible(true);
2247                 show(cell);
2248 //                layingOut = false;
2249                 return;
2250             }
2251 
2252             // In this case, we're asked to show a random cell
2253 //            layingOut = true;
2254             adjustPositionToIndex(index);
2255             addAllToPile();
2256             requestLayout();
2257 //            layingOut = false;            
2258         }
2259     }
2260 
2261     public void scrollTo(int index) {
2262         boolean posSet = false;
2263         
2264         if (index >= cellCount - 1) {
2265             setPosition(1);
2266             posSet = true;
2267         } else if (index < 0) {
2268             setPosition(0);
2269             posSet = true;
2270         }
2271         
2272         if (! posSet) {
2273             adjustPositionToIndex(index);
2274             double offset = - computeOffsetForCell(index);
2275             adjustByPixelAmount(offset);
2276         }
2277         
2278         requestLayout();        
2279     }
2280     
2281     //TODO We assume all the cell have the same length.  We will need to support
2282     // cells of different lengths.
2283     public void scrollToOffset(int offset) {
2284         adjustPixels(offset * getCellLength(0));
2285     }    
2286     
2287     /**
2288      * Given a delta value representing a number of pixels, this method attempts
2289      * to move the VirtualFlow in the given direction (positive is down/right,
2290      * negative is up/left) the given number of pixels. It returns the number of
2291      * pixels actually moved.
2292      */
2293     public double adjustPixels(final double delta) {
2294         // Short cut this method for cases where nothing should be done
2295         if (delta == 0) return 0;
2296 
2297         final boolean isVertical = isVertical();
2298         if (((isVertical && (tempVisibility ? !needLengthBar : !vbar.isVisible())) ||
2299                 (! isVertical && (tempVisibility ? !needLengthBar : !hbar.isVisible())))) return 0;
2300         
2301         double pos = getPosition();
2302         if (pos == 0.0f && delta < 0) return 0;
2303         if (pos == 1.0f && delta > 0) return 0;
2304 
2305         adjustByPixelAmount(delta);
2306         if (pos == getPosition()) {
2307             // The pos hasn't changed, there's nothing to do. This is likely
2308             // to occur when we hit either extremity
2309             return 0;
2310         }
2311 
2312         // Now move stuff around. Translating by pixels fundamentally means
2313         // moving the cells by the delta. However, after having
2314         // done that, we need to go through the cells and see which cells,
2315         // after adding in the translation factor, now fall off the viewport.
2316         // Also, we need to add cells as appropriate to the end (or beginning,
2317         // depending on the direction of travel).
2318         //
2319         // One simplifying assumption (that had better be true!) is that we
2320         // will only make it this far in the function if the virtual scroll
2321         // bar is visible. Otherwise, we never will pixel scroll. So as we go,
2322         // if we find that the maxPrefBreadth exceeds the viewportBreadth,
2323         // then we will be sure to show the breadthBar and update it
2324         // accordingly.
2325         if (cells.size() > 0) {
2326             for (int i = 0; i < cells.size(); i++) {
2327                 T cell = cells.get(i);
2328                 assert cell != null;
2329                 positionCell(cell, getCellPosition(cell) - delta);
2330             }
2331 
2332             // Fix for RT-32908
2333             T firstCell = cells.getFirst();
2334             double layoutY = firstCell == null ? 0 : getCellPosition(firstCell);
2335             for (int i = 0; i < cells.size(); i++) {
2336                 T cell = cells.get(i);
2337                 assert cell != null;
2338                 double actualLayoutY = getCellPosition(cell);
2339                 if (actualLayoutY != layoutY) {
2340                     // we need to shift the cell to layoutY
2341                     positionCell(cell, layoutY);
2342                 }
2343 
2344                 layoutY += getCellLength(cell);
2345             }
2346             // end of fix for RT-32908
2347             cull();
2348             firstCell = cells.getFirst();
2349 
2350             // Add any necessary leading cells
2351             if (firstCell != null) {
2352                 int firstIndex = getCellIndex(firstCell);
2353                 double prevIndexSize = getCellLength(firstIndex - 1);
2354                 addLeadingCells(firstIndex - 1, getCellPosition(firstCell) - prevIndexSize);
2355             } else {
2356                 int currentIndex = computeCurrentIndex();
2357 
2358                 // The distance from the top of the viewport to the top of the
2359                 // cell for the current index.
2360                 double offset = -computeViewportOffset(getPosition());
2361 
2362                 // Add all the leading and trailing cells (the call to add leading
2363                 // cells will add the current cell as well -- that is, the one that
2364                 // represents the current position on the mapper).
2365                 addLeadingCells(currentIndex, offset);
2366             }
2367 
2368             // Starting at the tail of the list, loop adding cells until
2369             // all the space on the table is filled up. We want to make
2370             // sure that we DO NOT add empty trailing cells (since we are
2371             // in the full virtual case and so there are no trailing empty
2372             // cells).
2373             if (! addTrailingCells(false)) {
2374                 // Reached the end, but not enough cells to fill up to
2375                 // the end. So, remove the trailing empty space, and translate
2376                 // the cells down
2377                 final T lastCell = getLastVisibleCell();
2378                 final double lastCellSize = getCellLength(lastCell);
2379                 final double cellEnd = getCellPosition(lastCell) + lastCellSize;
2380                 final double viewportLength = getViewportLength();
2381 
2382                 if (cellEnd < viewportLength) {
2383                     // Reposition the nodes
2384                     double emptySize = viewportLength - cellEnd;
2385                     for (int i = 0; i < cells.size(); i++) {
2386                         T cell = cells.get(i);
2387                         positionCell(cell, getCellPosition(cell) + emptySize);
2388                     }
2389                     setPosition(1.0f);
2390                     // fill the leading empty space
2391                     firstCell = cells.getFirst();
2392                     int firstIndex = getCellIndex(firstCell);
2393                     double prevIndexSize = getCellLength(firstIndex - 1);
2394                     addLeadingCells(firstIndex - 1, getCellPosition(firstCell) - prevIndexSize);
2395                 }
2396             }
2397         }
2398 
2399         // Now throw away any cells that don't fit
2400         cull();
2401 
2402         // Finally, update the scroll bars
2403         updateScrollBarsAndCells(false);
2404         lastPosition = getPosition();
2405 
2406         // notify
2407         return delta; // TODO fake
2408     }
2409 
2410     private boolean needsReconfigureCells = false; // when cell contents are the same
2411     private boolean needsRecreateCells = false; // when cell factory changed
2412     private boolean needsRebuildCells = false; // when cell contents have changed
2413     private boolean needsCellsLayout = false;
2414     private boolean sizeChanged = false;
2415     private final BitSet dirtyCells = new BitSet();
2416     
2417     public void reconfigureCells() {
2418         needsReconfigureCells = true;
2419         requestLayout();
2420     }
2421 
2422     public void recreateCells() {
2423         needsRecreateCells = true;
2424         requestLayout();
2425     }
2426     
2427     public void rebuildCells() {
2428         needsRebuildCells = true;
2429         requestLayout();
2430     }
2431 
2432     public void requestCellLayout() {
2433         needsCellsLayout = true;
2434         requestLayout();
2435     }
2436 
2437     public void setCellDirty(int index) {
2438         dirtyCells.set(index);
2439         requestLayout();
2440     }
2441 
2442     private static final double GOLDEN_RATIO_MULTIPLIER = 0.618033987;
2443 
2444     private double getPrefBreadth(double oppDimension) {
2445         double max = getMaxCellWidth(10);
2446 
2447         // This primarily exists for the case where we do not want the breadth
2448         // to grow to ensure a golden ratio between width and height (for example,
2449         // when a ListView is used in a ComboBox - the width should not grow
2450         // just because items are being added to the ListView)
2451         if (oppDimension > -1) {
2452             double prefLength = getPrefLength();
2453             max = Math.max(max, prefLength * GOLDEN_RATIO_MULTIPLIER);
2454         }
2455         
2456         return max;
2457     }
2458 
2459     private double getPrefLength() {
2460         double sum = 0.0;
2461         int rows = Math.min(10, cellCount);
2462         for (int i = 0; i < rows; i++) {
2463             sum += getCellLength(i);
2464         }
2465         return sum;
2466     }
2467 
2468     @Override protected double computePrefWidth(double height) {
2469         double w = isVertical() ? getPrefBreadth(height) : getPrefLength();
2470         return w + vbar.prefWidth(-1);
2471     }
2472 
2473     @Override protected double computePrefHeight(double width) {
2474         double h = isVertical() ? getPrefLength() : getPrefBreadth(width);
2475         return h + hbar.prefHeight(-1);
2476     }
2477     
2478     double getMaxCellWidth(int rowsToCount) {
2479         double max = 0.0;
2480         
2481         // we always measure at least one row
2482         int rows = Math.max(1, rowsToCount == -1 ? cellCount : rowsToCount); 
2483         for (int i = 0; i < rows; i++) {
2484             max = Math.max(max, getCellBreadth(i));
2485         }
2486         return max;
2487     }
2488     
2489     
2490     
2491     // Old PositionMapper
2492     /**
2493      * Given a position value between 0 and 1, compute and return the viewport
2494      * offset from the "current" cell associated with that position value.
2495      * That is, if the return value of this function where used as a translation
2496      * factor for a sheet that contained all the items, then the current
2497      * item would end up positioned correctly.
2498      */
2499     private double computeViewportOffset(double position) {
2500         double p = com.sun.javafx.Utils.clamp(0, position, 1);
2501         double fractionalPosition = p * getCellCount();
2502         int cellIndex = (int) fractionalPosition;
2503         double fraction = fractionalPosition - cellIndex;
2504         double cellSize = getCellLength(cellIndex);
2505         double pixelOffset = cellSize * fraction;
2506         double viewportOffset = getViewportLength() * p;
2507         return pixelOffset - viewportOffset;
2508     }
2509 
2510     private void adjustPositionToIndex(int index) {
2511         int cellCount = getCellCount();
2512         if (cellCount <= 0) {
2513             setPosition(0.0f);
2514         } else {            
2515             setPosition(((double)index) / cellCount);
2516         }
2517     }
2518 
2519     /**
2520      * Adjust the position based on a delta of pixels. If negative, then the
2521      * position will be adjusted negatively. If positive, then the position will
2522      * be adjusted positively. If the pixel amount is too great for the range of
2523      * the position, then it will be clamped such that position is always
2524      * strictly between 0 and 1
2525      */
2526     private void adjustByPixelAmount(double numPixels) {
2527         if (numPixels == 0) return;
2528         // Starting from the current cell, we move in the direction indicated
2529         // by numPixels one cell at a team. For each cell, we discover how many
2530         // pixels the "position" line would move within that cell, and adjust
2531         // our count of numPixels accordingly. When we come to the "final" cell,
2532         // then we can take the remaining number of pixels and multiply it by
2533         // the "travel rate" of "p" within that cell to get the delta. Add
2534         // the delta to "p" to get position.
2535 
2536         // get some basic info about the list and the current cell
2537         boolean forward = numPixels > 0;
2538         int cellCount = getCellCount();
2539         double fractionalPosition = getPosition() * cellCount;
2540         int cellIndex = (int) fractionalPosition;
2541         if (forward && cellIndex == cellCount) return;
2542         double cellSize = getCellLength(cellIndex);
2543         double fraction = fractionalPosition - cellIndex;
2544         double pixelOffset = cellSize * fraction;
2545 
2546         // compute the percentage of "position" that represents each cell
2547         double cellPercent = 1.0 / cellCount;
2548 
2549         // To help simplify the algorithm, we pretend as though the current
2550         // position is at the beginning of the current cell. This reduces some
2551         // of the corner cases and provides a simpler algorithm without adding
2552         // any overhead to performance.
2553         double start = computeOffsetForCell(cellIndex);
2554         double end = cellSize + computeOffsetForCell(cellIndex + 1);
2555 
2556         // We need to discover the distance that the fictional "position line"
2557         // would travel within this cell, from its current position to the end.
2558         double remaining = end - start;
2559 
2560         // Keep track of the number of pixels left to travel
2561         double n = forward ?
2562               numPixels + pixelOffset - (getViewportLength() * getPosition()) - start
2563             : -numPixels + end - (pixelOffset - (getViewportLength() * getPosition()));
2564 
2565         // "p" represents the most recent value for position. This is always
2566         // based on the edge between two cells, except at the very end of the
2567         // algorithm where it is added to the computed "p" offset for the final
2568         // value of Position.
2569         double p = cellPercent * cellIndex;
2570 
2571         // Loop over the cells one at a time until either we reach the end of
2572         // the cells, or we find that the "n" will fall within the cell we're on
2573         while (n > remaining && ((forward && cellIndex < cellCount - 1) || (! forward && cellIndex > 0))) {
2574             if (forward) cellIndex++; else cellIndex--;
2575             n -= remaining;
2576             cellSize = getCellLength(cellIndex);
2577             start = computeOffsetForCell(cellIndex);
2578             end = cellSize + computeOffsetForCell(cellIndex + 1);
2579             remaining = end - start;
2580             p = cellPercent * cellIndex;
2581         }
2582 
2583         // if remaining is < n, then we must have hit an end, so as a
2584         // fast path, we can just set position to 1.0 or 0.0 and return
2585         // because we know we hit the end
2586         if (n > remaining) {
2587             setPosition(forward ? 1.0f : 0.0f);
2588         } else if (forward) {
2589             double rate = cellPercent / Math.abs(end - start);
2590             setPosition(p + (rate * n));
2591         } else {
2592             double rate = cellPercent / Math.abs(end - start);
2593             setPosition((p + cellPercent) - (rate * n));
2594         }
2595     }
2596 
2597     private int computeCurrentIndex() {
2598         return (int) (getPosition() * getCellCount());
2599     }
2600 
2601     /**
2602      * Given an item index, this function will compute and return the viewport
2603      * offset from the beginning of the specified item. Notice that because each
2604      * item has the same percentage of the position dedicated to it, and since
2605      * we are measuring from the start of each item, this is a very simple
2606      * calculation.
2607      */
2608     private double computeOffsetForCell(int itemIndex) {
2609         double cellCount = getCellCount();
2610         double p = com.sun.javafx.Utils.clamp(0, itemIndex, cellCount) / cellCount;
2611         return -(getViewportLength() * p);
2612     }
2613     
2614 //    /**
2615 //     * Adjust the position based on a chunk of pixels. The position is based
2616 //     * on the start of the scrollbar position.
2617 //     */
2618 //    private void adjustByPixelChunk(double numPixels) {
2619 //        setPosition(0);
2620 //        adjustByPixelAmount(numPixels);
2621 //    }
2622     // end of old PositionMapper code
2623     
2624     
2625     /**
2626      * A simple extension to Region that ensures that anything wanting to flow
2627      * outside of the bounds of the Region is clipped.
2628      */
2629     static class ClippedContainer extends Region {
2630 
2631         /**
2632          * The Node which is embedded within this {@code ClipView}.
2633          */
2634         private Node node;
2635         public Node getNode() { return this.node; }
2636         public void setNode(Node n) {
2637             this.node = n;
2638 
2639             getChildren().clear();
2640             getChildren().add(node);
2641         }
2642 
2643         public void setClipX(double clipX) {
2644             setLayoutX(-clipX);
2645             clipRect.setLayoutX(clipX);
2646         }
2647 
2648         public void setClipY(double clipY) {
2649             setLayoutY(-clipY);
2650             clipRect.setLayoutY(clipY);
2651         }
2652 
2653         private final Rectangle clipRect;
2654 
2655         public ClippedContainer(final VirtualFlow<?> flow) {
2656             if (flow == null) {
2657                 throw new IllegalArgumentException("VirtualFlow can not be null");
2658             }
2659 
2660             getStyleClass().add("clipped-container");
2661 
2662             // clipping
2663             clipRect = new Rectangle();
2664             clipRect.setSmooth(false);
2665             setClip(clipRect);
2666             // --- clipping
2667             
2668             super.widthProperty().addListener(valueModel -> {
2669                 clipRect.setWidth(getWidth());
2670             });
2671             super.heightProperty().addListener(valueModel -> {
2672                 clipRect.setHeight(getHeight());
2673             });
2674         }
2675     }
2676 
2677     /**
2678      * A List-like implementation that is exceedingly efficient for the purposes
2679      * of the VirtualFlow. Typically there is not much variance in the number of
2680      * cells -- it is always some reasonably consistent number. Yet for efficiency
2681      * in code, we like to use a linked list implementation so as to append to
2682      * start or append to end. However, at times when we need to iterate, LinkedList
2683      * is expensive computationally as well as requiring the construction of
2684      * temporary iterators.
2685      * <p>
2686      * This linked list like implementation is done using an array. It begins by
2687      * putting the first item in the center of the allocated array, and then grows
2688      * outward (either towards the first or last of the array depending on whether
2689      * we are inserting at the head or tail). It maintains an index to the start
2690      * and end of the array, so that it can efficiently expose iteration.
2691      * <p>
2692      * This class is package private solely for the sake of testing.
2693      */
2694     public static class ArrayLinkedList<T> extends AbstractList<T> {
2695         /**
2696          * The array list backing this class. We default the size of the array
2697          * list to be fairly large so as not to require resizing during normal
2698          * use, and since that many ArrayLinkedLists won't be created it isn't
2699          * very painful to do so.
2700          */
2701         private final ArrayList<T> array;
2702 
2703         private int firstIndex = -1;
2704         private int lastIndex = -1;
2705 
2706         public ArrayLinkedList() {
2707             array = new ArrayList<T>(50);
2708 
2709             for (int i = 0; i < 50; i++) {
2710                 array.add(null);
2711             }
2712         }
2713 
2714         public T getFirst() {
2715             return firstIndex == -1 ? null : array.get(firstIndex);
2716         }
2717 
2718         public T getLast() {
2719             return lastIndex == -1 ? null : array.get(lastIndex);
2720         }
2721 
2722         public void addFirst(T cell) {
2723             // if firstIndex == -1 then that means this is the first item in the
2724             // list and we need to initialize firstIndex and lastIndex
2725             if (firstIndex == -1) {
2726                 firstIndex = lastIndex = array.size() / 2;
2727                 array.set(firstIndex, cell);
2728             } else if (firstIndex == 0) {
2729                 // we're already at the head of the array, so insert at position
2730                 // 0 and then increment the lastIndex to compensate
2731                 array.add(0, cell);
2732                 lastIndex++;
2733             } else {
2734                 // we're not yet at the head of the array, so insert at the
2735                 // firstIndex - 1 position and decrement first position
2736                 array.set(--firstIndex, cell);
2737             }
2738         }
2739 
2740         public void addLast(T cell) {
2741             // if lastIndex == -1 then that means this is the first item in the
2742             // list and we need to initialize the firstIndex and lastIndex
2743             if (firstIndex == -1) {
2744                 firstIndex = lastIndex = array.size() / 2;
2745                 array.set(lastIndex, cell);
2746             } else if (lastIndex == array.size() - 1) {
2747                 // we're at the end of the array so need to "add" so as to force
2748                 // the array to be expanded in size
2749                 array.add(++lastIndex, cell);
2750             } else {
2751                 array.set(++lastIndex, cell);
2752             }
2753         }
2754 
2755         public int size() {
2756             return firstIndex == -1 ? 0 : lastIndex - firstIndex + 1;
2757         }
2758 
2759         public boolean isEmpty() {
2760             return firstIndex == -1;
2761         }
2762 
2763         public T get(int index) {
2764             if (index > (lastIndex - firstIndex) || index < 0) {
2765                 // Commented out exception due to RT-29111
2766                 // throw new java.lang.ArrayIndexOutOfBoundsException();
2767                 return null;
2768             }
2769 
2770             return array.get(firstIndex + index);
2771         }
2772 
2773         public void clear() {
2774             for (int i = 0; i < array.size(); i++) {
2775                 array.set(i, null);
2776             }
2777 
2778             firstIndex = lastIndex = -1;
2779         }
2780 
2781         public T removeFirst() {
2782             if (isEmpty()) return null;
2783             return remove(0);
2784         }
2785 
2786         public T removeLast() {
2787             if (isEmpty()) return null;
2788             return remove(lastIndex - firstIndex);
2789         }
2790 
2791         public T remove(int index) {
2792             if (index > lastIndex - firstIndex || index < 0) {
2793                 throw new java.lang.ArrayIndexOutOfBoundsException();
2794             }
2795 
2796             // if the index == 0, then we're removing the first
2797             // item and can simply set it to null in the array and increment
2798             // the firstIndex unless there is only one item, in which case
2799             // we have to also set first & last index to -1.
2800             if (index == 0) {
2801                 T cell = array.get(firstIndex);
2802                 array.set(firstIndex, null);
2803                 if (firstIndex == lastIndex) {
2804                     firstIndex = lastIndex = -1;
2805                 } else {
2806                     firstIndex++;
2807                 }
2808                 return cell;
2809             } else if (index == lastIndex - firstIndex) {
2810                 // if the index == lastIndex - firstIndex, then we're removing the
2811                 // last item and can simply set it to null in the array and
2812                 // decrement the lastIndex
2813                 T cell = array.get(lastIndex);
2814                 array.set(lastIndex--, null);
2815                 return cell;
2816             } else {
2817                 // if the index is somewhere in between, then we have to remove the
2818                 // item and decrement the lastIndex
2819                 T cell = array.get(firstIndex + index);
2820                 array.set(firstIndex + index, null);
2821                 for (int i = (firstIndex + index + 1); i <= lastIndex; i++) {
2822                     array.set(i - 1, array.get(i));
2823                 }
2824                 array.set(lastIndex--, null);
2825                 return cell;
2826             }
2827         }
2828     }
2829 
2830     Timeline sbTouchTimeline;
2831     KeyFrame sbTouchKF1;
2832     KeyFrame sbTouchKF2;
2833 
2834     private boolean needBreadthBar;
2835     private boolean needLengthBar;
2836     private boolean tempVisibility = false;
2837 
2838     protected void startSBReleasedAnimation() {
2839         if (sbTouchTimeline == null) {
2840             /*
2841             ** timeline to leave the scrollbars visible for a short
2842             ** while after a scroll/drag
2843             */
2844             sbTouchTimeline = new Timeline();
2845             sbTouchKF1 = new KeyFrame(Duration.millis(0), event -> {
2846                 tempVisibility = true;
2847                 requestLayout();
2848             });
2849 
2850             sbTouchKF2 = new KeyFrame(Duration.millis(1000), event -> {
2851                 if (touchDetected == false && mouseDown == false) {
2852                     tempVisibility = false;
2853                     requestLayout();
2854                 }
2855             });
2856             sbTouchTimeline.getKeyFrames().addAll(sbTouchKF1, sbTouchKF2);
2857         }
2858         sbTouchTimeline.playFromStart();
2859     }
2860 
2861     protected void scrollBarOn() {
2862         tempVisibility = true;
2863         requestLayout();
2864     }
2865 }