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