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