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