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