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.traversal.ParentTraversalEngine;
29 import javafx.animation.Animation.Status;
30 import javafx.animation.KeyFrame;
31 import javafx.animation.KeyValue;
32 import javafx.animation.Timeline;
33 import javafx.beans.InvalidationListener;
34 import javafx.beans.Observable;
35 import javafx.beans.property.DoubleProperty;
36 import javafx.beans.property.DoublePropertyBase;
37 import javafx.beans.value.ChangeListener;
38 import javafx.beans.value.ObservableValue;
39 import javafx.event.EventDispatcher;
40 import javafx.event.EventHandler;
41 import javafx.geometry.BoundingBox;
42 import javafx.geometry.Bounds;
43 import javafx.geometry.Orientation;
44 import javafx.scene.AccessibleAttribute;
45 import javafx.scene.Cursor;
46 import javafx.scene.Node;
47 import javafx.scene.control.ScrollBar;
48 import javafx.scene.control.ScrollPane;
49 import javafx.scene.control.ScrollPane.ScrollBarPolicy;
50 import javafx.scene.input.MouseEvent;
51 import javafx.scene.input.ScrollEvent;
52 import javafx.scene.input.TouchEvent;
53 import javafx.scene.layout.StackPane;
54 import javafx.scene.shape.Rectangle;
55 import javafx.util.Duration;
56 import com.sun.javafx.util.Utils;
57 import com.sun.javafx.scene.control.behavior.ScrollPaneBehavior;
58 import com.sun.javafx.scene.traversal.TraverseListener;
59 import static com.sun.javafx.scene.control.skin.Utils.*;
60 import javafx.geometry.Insets;
61
62 public class ScrollPaneSkin extends BehaviorSkinBase<ScrollPane, ScrollPaneBehavior> implements TraverseListener {
63 /***************************************************************************
64 * *
65 * UI Subcomponents *
66 * *
67 **************************************************************************/
68
69 private static final double DEFAULT_PREF_SIZE = 100.0;
70
71 private static final double DEFAULT_MIN_SIZE = 36.0;
72
73 private static final double DEFAULT_SB_BREADTH = 12.0;
74 private static final double DEFAULT_EMBEDDED_SB_BREADTH = 8.0;
75
76 private static final double PAN_THRESHOLD = 0.5;
77
78 // state from the control
79
80 private Node scrollNode;
81
82 private double nodeWidth;
83 private double nodeHeight;
84 private boolean nodeSizeInvalid = true;
85
86 private double posX;
87 private double posY;
88
89 // working state
90
91 private boolean hsbvis;
92 private boolean vsbvis;
93 private double hsbHeight;
94 private double vsbWidth;
95
96 // substructure
97
98 private StackPane viewRect;
99 private StackPane viewContent;
100 private double contentWidth;
101 private double contentHeight;
102 private StackPane corner;
103 protected ScrollBar hsb;
104 protected ScrollBar vsb;
105
106 double pressX;
107 double pressY;
108 double ohvalue;
109 double ovvalue;
110 private Cursor saveCursor = null;
111 private boolean dragDetected = false;
112 private boolean touchDetected = false;
113 private boolean mouseDown = false;
114
115 Rectangle clipRect;
116
117 /***************************************************************************
118 * *
119 * Constructors *
120 * *
121 **************************************************************************/
122
123 public ScrollPaneSkin(final ScrollPane scrollpane) {
124 super(scrollpane, new ScrollPaneBehavior(scrollpane));
125 initialize();
126 // Register listeners
127 registerChangeListener(scrollpane.contentProperty(), "NODE");
128 registerChangeListener(scrollpane.fitToWidthProperty(), "FIT_TO_WIDTH");
129 registerChangeListener(scrollpane.fitToHeightProperty(), "FIT_TO_HEIGHT");
130 registerChangeListener(scrollpane.hbarPolicyProperty(), "HBAR_POLICY");
131 registerChangeListener(scrollpane.vbarPolicyProperty(), "VBAR_POLICY");
132 registerChangeListener(scrollpane.hvalueProperty(), "HVALUE");
133 registerChangeListener(scrollpane.hmaxProperty(), "HMAX");
134 registerChangeListener(scrollpane.hminProperty(), "HMIN");
135 registerChangeListener(scrollpane.vvalueProperty(), "VVALUE");
136 registerChangeListener(scrollpane.vmaxProperty(), "VMAX");
137 registerChangeListener(scrollpane.vminProperty(), "VMIN");
138 registerChangeListener(scrollpane.prefViewportWidthProperty(), "VIEWPORT_SIZE_HINT");
139 registerChangeListener(scrollpane.prefViewportHeightProperty(), "VIEWPORT_SIZE_HINT");
140 registerChangeListener(scrollpane.minViewportWidthProperty(), "VIEWPORT_SIZE_HINT");
141 registerChangeListener(scrollpane.minViewportHeightProperty(), "VIEWPORT_SIZE_HINT");
142 }
143
144 private final InvalidationListener nodeListener = new InvalidationListener() {
145 @Override public void invalidated(Observable valueModel) {
146 if (!nodeSizeInvalid) {
147 final Bounds scrollNodeBounds = scrollNode.getLayoutBounds();
148 final double scrollNodeWidth = scrollNodeBounds.getWidth();
149 final double scrollNodeHeight = scrollNodeBounds.getHeight();
150
151 /*
152 ** if the new size causes scrollbar visibility to change, then need to relayout
153 ** we also need to correct the thumb size when the scrollnode's size changes
154 */
155 if (vsbvis != determineVerticalSBVisible() || hsbvis != determineHorizontalSBVisible() ||
156 (scrollNodeWidth != 0.0 && nodeWidth != scrollNodeWidth) ||
157 (scrollNodeHeight != 0.0 && nodeHeight != scrollNodeHeight)) {
158 getSkinnable().requestLayout();
159 } else {
160 /**
161 * we just need to update scrollbars based on new scrollNode size,
162 * but we don't do this while dragging, there's no need,
163 * and it jumps, as dragging updates the scrollbar too.
211 double oldWidth = oldBounds.getWidth();
212 double newWidth = newBounds.getWidth();
213 if (oldWidth > 0 && oldWidth != newWidth) {
214 double oldPositionX = (snapPosition(snappedLeftInset() - posX / (hsb.getMax() - hsb.getMin()) * (oldWidth - contentWidth)));
215 double newPositionX = (snapPosition(snappedLeftInset() - posX / (hsb.getMax() - hsb.getMin()) * (newWidth - contentWidth)));
216
217 double newValueX = (oldPositionX/newPositionX)*hsb.getValue();
218 if (newValueX < 0.0) {
219 hsb.setValue(0.0);
220 }
221 else if (newValueX < 1.0) {
222 hsb.setValue(newValueX);
223 }
224 else if (newValueX > 1.0) {
225 hsb.setValue(1.0);
226 }
227 }
228 }
229 };
230
231 private void initialize() {
232 // requestLayout calls below should not trigger requestLayout above ScrollPane
233 // setManaged(false);
234
235 ScrollPane control = getSkinnable();
236 scrollNode = control.getContent();
237
238 ParentTraversalEngine traversalEngine = new ParentTraversalEngine(getSkinnable());
239 traversalEngine.addTraverseListener(this);
240 getSkinnable().setImpl_traversalEngine(traversalEngine);
241
242 if (scrollNode != null) {
243 scrollNode.layoutBoundsProperty().addListener(nodeListener);
244 scrollNode.layoutBoundsProperty().addListener(boundsChangeListener);
245 }
246
247 viewRect = new StackPane() {
248
249 @Override
250 protected void layoutChildren() {
251 viewContent.resize(getWidth(), getHeight());
252 }
253
254 };
255 // prevent requestLayout requests from within scrollNode from percolating up
256 viewRect.setManaged(false);
257 viewRect.setCache(true);
258 viewRect.getStyleClass().add("viewport");
259
260 clipRect = new Rectangle();
261 viewRect.setClip(clipRect);
262
263 hsb = new ScrollBar();
264
265 vsb = new ScrollBar();
266 vsb.setOrientation(Orientation.VERTICAL);
267
268 EventHandler<MouseEvent> barHandler = ev -> {
269 getSkinnable().requestFocus();
270 };
271
272 hsb.addEventFilter(MouseEvent.MOUSE_PRESSED, barHandler);
273 vsb.addEventFilter(MouseEvent.MOUSE_PRESSED, barHandler);
298 }
299 if (scrollNode != null) {
300 scrollNode.relocate(0,0);
301 }
302 }
303 };
304 viewRect.getChildren().add(viewContent);
305
306 if (scrollNode != null) {
307 viewContent.getChildren().add(scrollNode);
308 viewRect.nodeOrientationProperty().bind(scrollNode.nodeOrientationProperty());
309 }
310
311 getChildren().clear();
312 getChildren().addAll(viewRect, vsb, hsb, corner);
313
314 /*
315 ** listeners, and assorted housekeeping
316 */
317 InvalidationListener vsbListener = valueModel -> {
318 if (!IS_TOUCH_SUPPORTED) {
319 posY = Utils.clamp(getSkinnable().getVmin(), vsb.getValue(), getSkinnable().getVmax());
320 }
321 else {
322 posY = vsb.getValue();
323 }
324 updatePosY();
325 };
326 vsb.valueProperty().addListener(vsbListener);
327
328 InvalidationListener hsbListener = valueModel -> {
329 if (!IS_TOUCH_SUPPORTED) {
330 posX = Utils.clamp(getSkinnable().getHmin(), hsb.getValue(), getSkinnable().getHmax());
331 }
332 else {
333 posX = hsb.getValue();
334 }
335 updatePosX();
336 };
337 hsb.valueProperty().addListener(hsbListener);
338
339 viewRect.setOnMousePressed(e -> {
340 mouseDown = true;
341 if (IS_TOUCH_SUPPORTED) {
342 startSBReleasedAnimation();
343 }
344 pressX = e.getX();
345 pressY = e.getY();
346 ohvalue = hsb.getValue();
347 ovvalue = vsb.getValue();
348 });
349
350
351 viewRect.setOnDragDetected(e -> {
352 if (IS_TOUCH_SUPPORTED) {
353 startSBReleasedAnimation();
354 }
355 if (getSkinnable().isPannable()) {
356 dragDetected = true;
357 if (saveCursor == null) {
358 saveCursor = getSkinnable().getCursor();
359 if (saveCursor == null) {
360 saveCursor = Cursor.DEFAULT;
361 }
362 getSkinnable().setCursor(Cursor.MOVE);
363 getSkinnable().requestLayout();
364 }
365 }
366 });
367
368 viewRect.addEventFilter(MouseEvent.MOUSE_RELEASED, e -> {
369 mouseDown = false;
370 if (dragDetected == true) {
371 if (saveCursor != null) {
372 getSkinnable().setCursor(saveCursor);
373 saveCursor = null;
374 getSkinnable().requestLayout();
375 }
376 dragDetected = false;
377 }
378
379 /*
380 ** if the contents need repositioning, and there's is no
381 ** touch event in progress, then start the repositioning.
382 */
383 if ((posY > getSkinnable().getVmax() || posY < getSkinnable().getVmin() ||
384 posX > getSkinnable().getHmax() || posX < getSkinnable().getHmin()) && !touchDetected) {
385 startContentsToViewport();
386 }
387 });
388 viewRect.setOnMouseDragged(e -> {
389 if (IS_TOUCH_SUPPORTED) {
390 startSBReleasedAnimation();
391 }
392 /*
393 ** for mobile-touch we allow drag, even if not pannagle
394 */
395 if (getSkinnable().isPannable() || IS_TOUCH_SUPPORTED) {
396 double deltaX = pressX - e.getX();
397 double deltaY = pressY - e.getY();
398 /*
399 ** we only drag if not all of the content is visible.
400 */
401 if (hsb.getVisibleAmount() > 0.0 && hsb.getVisibleAmount() < hsb.getMax()) {
402 if (Math.abs(deltaX) > PAN_THRESHOLD) {
403 if (isReverseNodeOrientation()) {
404 deltaX = -deltaX;
405 }
406 double newHVal = (ohvalue + deltaX / (nodeWidth - viewRect.getWidth()) * (hsb.getMax() - hsb.getMin()));
407 if (!IS_TOUCH_SUPPORTED) {
408 if (newHVal > hsb.getMax()) {
409 newHVal = hsb.getMax();
410 }
411 else if (newHVal < hsb.getMin()) {
412 newHVal = hsb.getMin();
413 }
414 hsb.setValue(newHVal);
415 }
416 else {
417 hsb.setValue(newHVal);
418 }
419 }
420 }
421 /*
422 ** we only drag if not all of the content is visible.
423 */
424 if (vsb.getVisibleAmount() > 0.0 && vsb.getVisibleAmount() < vsb.getMax()) {
425 if (Math.abs(deltaY) > PAN_THRESHOLD) {
426 double newVVal = (ovvalue + deltaY / (nodeHeight - viewRect.getHeight()) * (vsb.getMax() - vsb.getMin()));
427 if (!IS_TOUCH_SUPPORTED) {
428 if (newVVal > vsb.getMax()) {
429 newVVal = vsb.getMax();
430 }
431 else if (newVVal < vsb.getMin()) {
432 newVVal = vsb.getMin();
433 }
434 vsb.setValue(newVVal);
435 }
436 else {
437 vsb.setValue(newVVal);
438 }
439 }
440 }
441 }
442 /*
443 ** we need to consume drag events, as we don't want
444 ** the scrollpane itself to be dragged on every mouse click
445 */
446 e.consume();
447 });
472 !((ScrollEvent)event).isDirect()) {
473 tail = tail.prepend(blockEventDispatcher);
474 tail = tail.prepend(oldVsbEventDispatcher);
475 return tail.dispatchEvent(event);
476 }
477 return oldVsbEventDispatcher.dispatchEvent(event, tail);
478 });
479
480 /*
481 * listen for ScrollEvents over the whole of the ScrollPane
482 * area, the above dispatcher having removed the ScrollBars
483 * scroll event handling.
484 *
485 * Note that we use viewRect here, rather than setting the eventHandler
486 * on the ScrollPane itself. This is for RT-31582, and effectively
487 * allows for us to prioritise handling (and consuming) the event
488 * internally, before it is made available to users listening to events
489 * on the control. This is consistent with the VirtualFlow-based controls.
490 */
491 viewRect.addEventHandler(ScrollEvent.SCROLL, event -> {
492 if (IS_TOUCH_SUPPORTED) {
493 startSBReleasedAnimation();
494 }
495 /*
496 ** if we're completely visible then do nothing....
497 ** we only consume an event that we've used.
498 */
499 if (vsb.getVisibleAmount() < vsb.getMax()) {
500 double vRange = getSkinnable().getVmax()-getSkinnable().getVmin();
501 double vPixelValue;
502 if (nodeHeight > 0.0) {
503 vPixelValue = vRange / nodeHeight;
504 }
505 else {
506 vPixelValue = 0.0;
507 }
508 double newValue = vsb.getValue()+(-event.getDeltaY())*vPixelValue;
509 if (!IS_TOUCH_SUPPORTED) {
510 if ((event.getDeltaY() > 0.0 && vsb.getValue() > vsb.getMin()) ||
511 (event.getDeltaY() < 0.0 && vsb.getValue() < vsb.getMax())) {
512 vsb.setValue(newValue);
513 event.consume();
514 }
515 }
516 else {
517 /*
518 ** if there is a repositioning in progress then we only
519 ** set the value for 'real' events
520 */
521 if (!(((ScrollEvent)event).isInertia()) || (((ScrollEvent)event).isInertia()) && (contentsToViewTimeline == null || contentsToViewTimeline.getStatus() == Status.STOPPED)) {
522 vsb.setValue(newValue);
523 if ((newValue > vsb.getMax() || newValue < vsb.getMin()) && (!mouseDown && !touchDetected)) {
524 startContentsToViewport();
525 }
526 event.consume();
527 }
528 }
529 }
530
531 if (hsb.getVisibleAmount() < hsb.getMax()) {
532 double hRange = getSkinnable().getHmax()-getSkinnable().getHmin();
533 double hPixelValue;
534 if (nodeWidth > 0.0) {
535 hPixelValue = hRange / nodeWidth;
536 }
537 else {
538 hPixelValue = 0.0;
539 }
540
541 double newValue = hsb.getValue()+(-event.getDeltaX())*hPixelValue;
542 if (!IS_TOUCH_SUPPORTED) {
543 if ((event.getDeltaX() > 0.0 && hsb.getValue() > hsb.getMin()) ||
544 (event.getDeltaX() < 0.0 && hsb.getValue() < hsb.getMax())) {
545 hsb.setValue(newValue);
546 event.consume();
547 }
548 }
549 else {
550 /*
551 ** if there is a repositioning in progress then we only
552 ** set the value for 'real' events
553 */
554 if (!(((ScrollEvent)event).isInertia()) || (((ScrollEvent)event).isInertia()) && (contentsToViewTimeline == null || contentsToViewTimeline.getStatus() == Status.STOPPED)) {
555 hsb.setValue(newValue);
556
557 if ((newValue > hsb.getMax() || newValue < hsb.getMin()) && (!mouseDown && !touchDetected)) {
558 startContentsToViewport();
559 }
560 event.consume();
561 }
562 }
563 }
564 });
565
566 /*
567 ** there are certain animations that need to know if the touch is
568 ** happening.....
569 */
570 getSkinnable().addEventHandler(TouchEvent.TOUCH_PRESSED, e -> {
571 touchDetected = true;
572 startSBReleasedAnimation();
573 e.consume();
574 });
575
576 getSkinnable().addEventHandler(TouchEvent.TOUCH_RELEASED, e -> {
577 touchDetected = false;
578 e.consume();
579 });
580
581 // ScrollPanes do not block all MouseEvents by default, unlike most other UI Controls.
582 consumeMouseEvents(false);
583
584 // update skin initial state to match control (see RT-35554)
585 hsb.setValue(control.getHvalue());
586 vsb.setValue(control.getVvalue());
587 }
588
589
590 @Override protected void handleControlPropertyChanged(String p) {
591 super.handleControlPropertyChanged(p);
592 if ("NODE".equals(p)) {
593 if (scrollNode != getSkinnable().getContent()) {
594 if (scrollNode != null) {
595 scrollNode.layoutBoundsProperty().removeListener(nodeListener);
596 scrollNode.layoutBoundsProperty().removeListener(boundsChangeListener);
597 viewContent.getChildren().remove(scrollNode);
598 }
599 scrollNode = getSkinnable().getContent();
600 if (scrollNode != null) {
601 nodeWidth = snapSize(scrollNode.getLayoutBounds().getWidth());
602 nodeHeight = snapSize(scrollNode.getLayoutBounds().getHeight());
603 viewContent.getChildren().setAll(scrollNode);
604 scrollNode.layoutBoundsProperty().addListener(nodeListener);
605 scrollNode.layoutBoundsProperty().addListener(boundsChangeListener);
606 }
607 }
608 getSkinnable().requestLayout();
609 } else if ("FIT_TO_WIDTH".equals(p) || "FIT_TO_HEIGHT".equals(p)) {
610 getSkinnable().requestLayout();
611 viewRect.requestLayout();
612 } else if ("HBAR_POLICY".equals(p) || "VBAR_POLICY".equals(p)) {
613 // change might affect pref size, so requestLayout on control
614 getSkinnable().requestLayout();
615 } else if ("HVALUE".equals(p)) {
616 hsb.setValue(getSkinnable().getHvalue());
617 } else if ("HMAX".equals(p)) {
618 hsb.setMax(getSkinnable().getHmax());
619 } else if ("HMIN".equals(p)) {
620 hsb.setMin(getSkinnable().getHmin());
621 } else if ("VVALUE".equals(p)) {
622 vsb.setValue(getSkinnable().getVvalue());
623 } else if ("VMAX".equals(p)) {
624 vsb.setMax(getSkinnable().getVmax());
625 } else if ("VMIN".equals(p)) {
626 vsb.setMin(getSkinnable().getVmin());
627 } else if ("VIEWPORT_SIZE_HINT".equals(p)) {
628 // change affects pref size, so requestLayout on control
629 getSkinnable().requestLayout();
630 }
631 }
632
633 void scrollBoundsIntoView(Bounds b) {
634 double dx = 0.0;
635 double dy = 0.0;
636 if (b.getMaxX() > contentWidth) {
637 dx = b.getMinX() - snappedLeftInset();
638 }
639 if (b.getMinX() < snappedLeftInset()) {
640 dx = b.getMaxX() - contentWidth - snappedLeftInset();
641 }
642 if (b.getMaxY() > snappedTopInset() + contentHeight) {
643 dy = b.getMinY() - snappedTopInset();
644 }
645 if (b.getMinY() < snappedTopInset()) {
646 dy = b.getMaxY() - contentHeight - snappedTopInset();
647 }
648 // We want to move contentPanel's layoutX,Y by (dx,dy).
649 // But to do this we have to set the scrollbars' values appropriately.
650
651 if (dx != 0) {
652 double sdx = dx * (hsb.getMax() - hsb.getMin()) / (nodeWidth - contentWidth);
653 // Adjust back for some amount so that the Node border is not too close to view border
654 sdx += -1 * Math.signum(sdx) * hsb.getUnitIncrement() / 5; // This accounts to 2% of view width
655 hsb.setValue(hsb.getValue() + sdx);
656 getSkinnable().requestLayout();
657 }
658 if (dy != 0) {
659 double sdy = dy * (vsb.getMax() - vsb.getMin()) / (nodeHeight - contentHeight);
660 // Adjust back for some amount so that the Node border is not too close to view border
661 sdy += -1 * Math.signum(sdy) * vsb.getUnitIncrement() / 5; // This accounts to 2% of view height
662 vsb.setValue(vsb.getValue() + sdy);
663 getSkinnable().requestLayout();
664 }
665
666 }
667
668 /*
669 ** auto-scroll so node is within (0,0),(contentWidth,contentHeight)
670 */
671 @Override public void onTraverse(Node n, Bounds b) {
672 scrollBoundsIntoView(b);
673 }
674
675 public void hsbIncrement() {
676 if (hsb != null) hsb.increment();
677 }
678 public void hsbDecrement() {
679 if (hsb != null) hsb.decrement();
680 }
681
682 // TODO: add page increment and decrement
683 public void hsbPageIncrement() {
684 if (hsb != null) hsb.increment();
685 }
686 // TODO: add page increment and decrement
687 public void hsbPageDecrement() {
688 if (hsb != null) hsb.decrement();
689 }
690
691 public void vsbIncrement() {
692 if (vsb != null) vsb.increment();
693 }
694 public void vsbDecrement() {
695 if (vsb != null) vsb.decrement();
696 }
697
698 // TODO: add page increment and decrement
699 public void vsbPageIncrement() {
700 if (vsb != null) vsb.increment();
701 }
702 // TODO: add page increment and decrement
703 public void vsbPageDecrement() {
704 if (vsb != null) vsb.decrement();
705 }
706
707 /***************************************************************************
708 * *
709 * Layout *
710 * *
711 **************************************************************************/
712
713 @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
714 final ScrollPane sp = getSkinnable();
715
716 double vsbWidth = computeVsbSizeHint(sp);
717 double minWidth = vsbWidth + snappedLeftInset() + snappedRightInset();
718
719 if (sp.getPrefViewportWidth() > 0) {
720 return (sp.getPrefViewportWidth() + minWidth);
721 }
722 else if (sp.getContent() != null) {
723 return (sp.getContent().prefWidth(height) + minWidth);
724 }
725 else {
726 return Math.max(minWidth, DEFAULT_PREF_SIZE);
727 }
728 }
729
730 @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
731 final ScrollPane sp = getSkinnable();
732
733 double hsbHeight = computeHsbSizeHint(sp);
734 double minHeight = hsbHeight + snappedTopInset() + snappedBottomInset();
735
736 if (sp.getPrefViewportHeight() > 0) {
737 return (sp.getPrefViewportHeight() + minHeight);
738 }
739 else if (sp.getContent() != null) {
740 return (sp.getContent().prefHeight(width) + minHeight);
741 }
742 else {
743 return Math.max(minHeight, DEFAULT_PREF_SIZE);
744 }
745 }
746
747 @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
748 final ScrollPane sp = getSkinnable();
749
750 double vsbWidth = computeVsbSizeHint(sp);
751 double minWidth = vsbWidth + snappedLeftInset() + snappedRightInset();
752
753 if (sp.getMinViewportWidth() > 0) {
754 return (sp.getMinViewportWidth() + minWidth);
755 } else {
756 double w = corner.minWidth(-1);
757 return (w > 0) ? (3 * w) : (DEFAULT_MIN_SIZE);
758 }
759
760 }
761
762 @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
763 final ScrollPane sp = getSkinnable();
764
765 double hsbHeight = computeHsbSizeHint(sp);
766 double minHeight = hsbHeight + snappedTopInset() + snappedBottomInset();
767
768 if (sp.getMinViewportHeight() > 0) {
769 return (sp.getMinViewportHeight() + minHeight);
770 } else {
771 double h = corner.minHeight(-1);
772 return (h > 0) ? (3 * h) : (DEFAULT_MIN_SIZE);
773 }
774 }
775
776 /**
777 * Computes the size that should be reserved for horizontal scrollbar in size hints (min/pref height)
778 */
779 private double computeHsbSizeHint(ScrollPane sp) {
780 return ((sp.getHbarPolicy() == ScrollBarPolicy.ALWAYS) ||
781 (sp.getHbarPolicy() == ScrollBarPolicy.AS_NEEDED && (sp.getPrefViewportHeight() > 0 || sp.getMinViewportHeight() > 0)))
782 ? hsb.prefHeight(ScrollBar.USE_COMPUTED_SIZE)
783 : 0;
784 }
785
786 /**
787 * Computes the size that should be reserved for vertical scrollbar in size hints (min/pref width)
788 */
789 private double computeVsbSizeHint(ScrollPane sp) {
790 return ((sp.getVbarPolicy() == ScrollBarPolicy.ALWAYS) ||
791 (sp.getVbarPolicy() == ScrollBarPolicy.AS_NEEDED && (sp.getPrefViewportWidth() > 0
792 || sp.getMinViewportWidth() > 0)))
793 ? vsb.prefWidth(ScrollBar.USE_COMPUTED_SIZE)
794 : 0;
795 }
796
797 @Override protected void layoutChildren(final double x, final double y,
798 final double w, final double h) {
799 final ScrollPane control = getSkinnable();
800 final Insets padding = control.getPadding();
801 final double rightPadding = snapSize(padding.getRight());
802 final double leftPadding = snapSize(padding.getLeft());
803 final double topPadding = snapSize(padding.getTop());
804 final double bottomPadding = snapSize(padding.getBottom());
805
806 vsb.setMin(control.getVmin());
807 vsb.setMax(control.getVmax());
808
809 //should only do this on css setup
810 hsb.setMin(control.getHmin());
811 hsb.setMax(control.getHmax());
812
813 contentWidth = w;
814 contentHeight = h;
815
816 /*
817 ** we want the scrollbars to go right to the border
818 */
819 double hsbWidth = 0;
820 double vsbHeight = 0;
821
822 computeScrollNodeSize(contentWidth, contentHeight);
823 computeScrollBarSize();
824
825 for (int i = 0; i < 2; ++i) {
826 vsbvis = determineVerticalSBVisible();
827 hsbvis = determineHorizontalSBVisible();
828
829 if (vsbvis && !IS_TOUCH_SUPPORTED) {
830 contentWidth = w - vsbWidth;
831 }
832 hsbWidth = w + leftPadding + rightPadding - (vsbvis ? vsbWidth : 0);
833 if (hsbvis && !IS_TOUCH_SUPPORTED) {
834 contentHeight = h - hsbHeight;
835 }
836 vsbHeight = h + topPadding + bottomPadding - (hsbvis ? hsbHeight : 0);
837 }
838
839
840 if (scrollNode != null && scrollNode.isResizable()) {
841 // maybe adjust size now that scrollbars may take up space
842 if (vsbvis && hsbvis) {
843 // adjust just once to accommodate
844 computeScrollNodeSize(contentWidth, contentHeight);
845
846 } else if (hsbvis && !vsbvis) {
847 computeScrollNodeSize(contentWidth, contentHeight);
848 vsbvis = determineVerticalSBVisible();
849 if (vsbvis) {
850 // now both are visible
851 contentWidth -= vsbWidth;
852 hsbWidth -= vsbWidth;
853 computeScrollNodeSize(contentWidth, contentHeight);
854 }
855 } else if (vsbvis && !hsbvis) {
856 computeScrollNodeSize(contentWidth, contentHeight);
857 hsbvis = determineHorizontalSBVisible();
858 if (hsbvis) {
859 // now both are visible
860 contentHeight -= hsbHeight;
861 vsbHeight -= hsbHeight;
862 computeScrollNodeSize(contentWidth, contentHeight);
863 }
864 }
865 }
866
867 // figure out the content area that is to be filled
868 double cx = snappedLeftInset() - leftPadding;
869 double cy = snappedTopInset() - topPadding;
870
871 vsb.setVisible(vsbvis);
872 if (vsbvis) {
873 /*
874 ** round up position of ScrollBar, round down it's size.
875 **
876 ** Positioning the ScrollBar
877 ** The Padding should go between the content and the edge,
878 ** otherwise changes in padding move the ScrollBar, and could
879 ** in extreme cases size the ScrollBar to become unusable.
880 ** The -1, +1 plus one bit :
881 ** If padding in => 1 then we allow one pixel to appear as the
882 ** outside border of the Scrollbar, and the rest on the inside.
883 ** If padding is < 1 then we just stick to the edge.
884 */
885 vsb.resizeRelocate(snappedLeftInset() + w - vsbWidth + (rightPadding < 1 ? 0 : rightPadding - 1) ,
886 cy, vsbWidth, vsbHeight);
887 }
888 updateVerticalSB();
889
890 hsb.setVisible(hsbvis);
891 if (hsbvis) {
892 /*
893 ** round up position of ScrollBar, round down it's size.
894 **
895 ** Positioning the ScrollBar
896 ** The Padding should go between the content and the edge,
897 ** otherwise changes in padding move the ScrollBar, and could
898 ** in extreme cases size the ScrollBar to become unusable.
899 ** The -1, +1 plus one bit :
900 ** If padding in => 1 then we allow one pixel to appear as the
901 ** outside border of the Scrollbar, and the rest on the inside.
902 ** If padding is < 1 then we just stick to the edge.
903 */
904 hsb.resizeRelocate(cx, snappedTopInset() + h - hsbHeight + (bottomPadding < 1 ? 0 : bottomPadding - 1),
905 hsbWidth, hsbHeight);
906 }
907 updateHorizontalSB();
908
909 viewRect.resizeRelocate(snappedLeftInset(), snappedTopInset(), snapSize(contentWidth), snapSize(contentHeight));
910 resetClip();
911
912 if (vsbvis && hsbvis) {
913 corner.setVisible(true);
914 double cornerWidth = vsbWidth;
915 double cornerHeight = hsbHeight;
916 corner.resizeRelocate(snapPosition(vsb.getLayoutX()), snapPosition(hsb.getLayoutY()), snapSize(cornerWidth), snapSize(cornerHeight));
917 } else {
918 corner.setVisible(false);
919 }
920 control.setViewportBounds(new BoundingBox(snapPosition(viewContent.getLayoutX()), snapPosition(viewContent.getLayoutY()), snapSize(contentWidth), snapSize(contentHeight)));
921 }
922
923 private void computeScrollNodeSize(double contentWidth, double contentHeight) {
924 if (scrollNode != null) {
925 if (scrollNode.isResizable()) {
926 ScrollPane control = getSkinnable();
927 Orientation bias = scrollNode.getContentBias();
928 if (bias == null) {
929 nodeWidth = snapSize(boundedSize(control.isFitToWidth()? contentWidth : scrollNode.prefWidth(-1),
930 scrollNode.minWidth(-1),scrollNode.maxWidth(-1)));
931 nodeHeight = snapSize(boundedSize(control.isFitToHeight()? contentHeight : scrollNode.prefHeight(-1),
932 scrollNode.minHeight(-1), scrollNode.maxHeight(-1)));
933
934 } else if (bias == Orientation.HORIZONTAL) {
935 nodeWidth = snapSize(boundedSize(control.isFitToWidth()? contentWidth : scrollNode.prefWidth(-1),
936 scrollNode.minWidth(-1),scrollNode.maxWidth(-1)));
937 nodeHeight = snapSize(boundedSize(control.isFitToHeight()? contentHeight : scrollNode.prefHeight(nodeWidth),
938 scrollNode.minHeight(nodeWidth),scrollNode.maxHeight(nodeWidth)));
939
940 } else { // bias == VERTICAL
941 nodeHeight = snapSize(boundedSize(control.isFitToHeight()? contentHeight : scrollNode.prefHeight(-1),
942 scrollNode.minHeight(-1), scrollNode.maxHeight(-1)));
944 scrollNode.minWidth(nodeHeight),scrollNode.maxWidth(nodeHeight)));
945 }
946
947 } else {
948 nodeWidth = snapSize(scrollNode.getLayoutBounds().getWidth());
949 nodeHeight = snapSize(scrollNode.getLayoutBounds().getHeight());
950 }
951 nodeSizeInvalid = false;
952 }
953 }
954
955 private boolean isReverseNodeOrientation() {
956 return (scrollNode != null &&
957 getSkinnable().getEffectiveNodeOrientation() !=
958 scrollNode.getEffectiveNodeOrientation());
959 }
960
961 private boolean determineHorizontalSBVisible() {
962 final ScrollPane sp = getSkinnable();
963
964 if (IS_TOUCH_SUPPORTED) {
965 return (tempVisibility && (nodeWidth > contentWidth));
966 }
967 else {
968 // RT-17395: ScrollBarPolicy might be null. If so, treat it as "AS_NEEDED", which is the default
969 ScrollBarPolicy hbarPolicy = sp.getHbarPolicy();
970 return (ScrollBarPolicy.NEVER == hbarPolicy) ? false :
971 ((ScrollBarPolicy.ALWAYS == hbarPolicy) ? true :
972 ((sp.isFitToWidth() && scrollNode != null ? scrollNode.isResizable() : false) ?
973 (nodeWidth > contentWidth && scrollNode.minWidth(-1) > contentWidth) : (nodeWidth > contentWidth)));
974 }
975 }
976
977 private boolean determineVerticalSBVisible() {
978 final ScrollPane sp = getSkinnable();
979
980 if (IS_TOUCH_SUPPORTED) {
981 return (tempVisibility && (nodeHeight > contentHeight));
982 }
983 else {
984 // RT-17395: ScrollBarPolicy might be null. If so, treat it as "AS_NEEDED", which is the default
985 ScrollBarPolicy vbarPolicy = sp.getVbarPolicy();
986 return (ScrollBarPolicy.NEVER == vbarPolicy) ? false :
987 ((ScrollBarPolicy.ALWAYS == vbarPolicy) ? true :
988 ((sp.isFitToHeight() && scrollNode != null ? scrollNode.isResizable() : false) ?
989 (nodeHeight > contentHeight && scrollNode.minHeight(-1) > contentHeight) : (nodeHeight > contentHeight)));
990 }
991 }
992
993 private void computeScrollBarSize() {
994 vsbWidth = snapSize(vsb.prefWidth(-1));
995 if (vsbWidth == 0) {
996 // println("*** WARNING ScrollPaneSkin: can't get scroll bar width, using {DEFAULT_SB_BREADTH}");
997 if (IS_TOUCH_SUPPORTED) {
998 vsbWidth = DEFAULT_EMBEDDED_SB_BREADTH;
999 }
1000 else {
1001 vsbWidth = DEFAULT_SB_BREADTH;
1002 }
1003 }
1004 hsbHeight = snapSize(hsb.prefHeight(-1));
1005 if (hsbHeight == 0) {
1006 // println("*** WARNING ScrollPaneSkin: can't get scroll bar height, using {DEFAULT_SB_BREADTH}");
1007 if (IS_TOUCH_SUPPORTED) {
1008 hsbHeight = DEFAULT_EMBEDDED_SB_BREADTH;
1009 }
1010 else {
1011 hsbHeight = DEFAULT_SB_BREADTH;
1012 }
1013 }
1014 }
1015
1016 private void updateHorizontalSB() {
1017 double contentRatio = nodeWidth * (hsb.getMax() - hsb.getMin());
1018 if (contentRatio > 0.0) {
1019 hsb.setVisibleAmount(contentWidth / contentRatio);
1020 hsb.setBlockIncrement(0.9 * hsb.getVisibleAmount());
1021 hsb.setUnitIncrement(0.1 * hsb.getVisibleAmount());
1022 }
1023 else {
1024 hsb.setVisibleAmount(0.0);
1025 hsb.setBlockIncrement(0.0);
1026 hsb.setUnitIncrement(0.0);
1027 }
1066 double x = isReverseNodeOrientation() ? (hsb.getMax() - (posX - hsb.getMin())) : posX;
1067 double minX = Math.min((- x / (hsb.getMax() - hsb.getMin()) * (nodeWidth - contentWidth)), 0);
1068 viewContent.setLayoutX(snapPosition(minX));
1069 if (!sp.hvalueProperty().isBound()) sp.setHvalue(Utils.clamp(sp.getHmin(), posX, sp.getHmax()));
1070 return posX;
1071 }
1072
1073 private double updatePosY() {
1074 final ScrollPane sp = getSkinnable();
1075 double minY = Math.min((- posY / (vsb.getMax() - vsb.getMin()) * (nodeHeight - contentHeight)), 0);
1076 viewContent.setLayoutY(snapPosition(minY));
1077 if (!sp.vvalueProperty().isBound()) sp.setVvalue(Utils.clamp(sp.getVmin(), posY, sp.getVmax()));
1078 return posY;
1079 }
1080
1081 private void resetClip() {
1082 clipRect.setWidth(snapSize(contentWidth));
1083 clipRect.setHeight(snapSize(contentHeight));
1084 }
1085
1086 Timeline sbTouchTimeline;
1087 KeyFrame sbTouchKF1;
1088 KeyFrame sbTouchKF2;
1089 Timeline contentsToViewTimeline;
1090 KeyFrame contentsToViewKF1;
1091 KeyFrame contentsToViewKF2;
1092 KeyFrame contentsToViewKF3;
1093
1094 private boolean tempVisibility;
1095
1096
1097 protected void startSBReleasedAnimation() {
1098 if (sbTouchTimeline == null) {
1099 /*
1100 ** timeline to leave the scrollbars visible for a short
1101 ** while after a scroll/drag
1102 */
1103 sbTouchTimeline = new Timeline();
1104 sbTouchKF1 = new KeyFrame(Duration.millis(0), event -> {
1105 tempVisibility = true;
1106 if (touchDetected == true || mouseDown == true) {
1107 sbTouchTimeline.playFromStart();
1108 }
1109 });
1110
1111 sbTouchKF2 = new KeyFrame(Duration.millis(1000), event -> {
1112 tempVisibility = false;
1113 getSkinnable().requestLayout();
1114 });
1115 sbTouchTimeline.getKeyFrames().addAll(sbTouchKF1, sbTouchKF2);
1116 }
1117 sbTouchTimeline.playFromStart();
1118 }
1119
1120
1121
1122 protected void startContentsToViewport() {
1123 double newPosX = posX;
1124 double newPosY = posY;
1125
1126 setContentPosX(posX);
1127 setContentPosY(posY);
1128
1129 if (posY > getSkinnable().getVmax()) {
1130 newPosY = getSkinnable().getVmax();
1131 }
1132 else if (posY < getSkinnable().getVmin()) {
1133 newPosY = getSkinnable().getVmin();
1134 }
1135
1136
1137 if (posX > getSkinnable().getHmax()) {
1138 newPosX = getSkinnable().getHmax();
1139 }
1140 else if (posX < getSkinnable().getHmin()) {
1141 newPosX = getSkinnable().getHmin();
1142 }
1143
1144 if (!IS_TOUCH_SUPPORTED) {
1145 startSBReleasedAnimation();
1146 }
1147
1148 /*
1149 ** timeline to return the contents of the scrollpane to the viewport
1150 */
1151 if (contentsToViewTimeline != null) {
1152 contentsToViewTimeline.stop();
1153 }
1154 contentsToViewTimeline = new Timeline();
1155 /*
1156 ** short pause before animation starts
1157 */
1158 contentsToViewKF1 = new KeyFrame(Duration.millis(50));
1159 /*
1160 ** reposition
1161 */
1162 contentsToViewKF2 = new KeyFrame(Duration.millis(150), event -> {
1163 getSkinnable().requestLayout();
1164 },
1165 new KeyValue(contentPosX, newPosX),
1166 new KeyValue(contentPosY, newPosY)
1167 );
1168 /*
1169 ** block out 'aftershocks', but real events will
1170 ** still reactivate
1171 */
1172 contentsToViewKF3 = new KeyFrame(Duration.millis(1500));
1173 contentsToViewTimeline.getKeyFrames().addAll(contentsToViewKF1, contentsToViewKF2, contentsToViewKF3);
1174 contentsToViewTimeline.playFromStart();
1175 }
1176
1177
1178 private DoubleProperty contentPosX;
1179 private void setContentPosX(double value) { contentPosXProperty().set(value); }
1180 private double getContentPosX() { return contentPosX == null ? 0.0 : contentPosX.get(); }
1181 private DoubleProperty contentPosXProperty() {
1182 if (contentPosX == null) {
1183 contentPosX = new DoublePropertyBase() {
1184 @Override protected void invalidated() {
1185 hsb.setValue(getContentPosX());
1186 getSkinnable().requestLayout();
1187 }
1188
1189 @Override
1190 public Object getBean() {
1191 return ScrollPaneSkin.this;
1192 }
1193
1194 @Override
1195 public String getName() {
1196 return "contentPosX";
1197 }
1198 };
1199 }
1200 return contentPosX;
1201 }
1202
1203 private DoubleProperty contentPosY;
1204 private void setContentPosY(double value) { contentPosYProperty().set(value); }
1205 private double getContentPosY() { return contentPosY == null ? 0.0 : contentPosY.get(); }
1206 private DoubleProperty contentPosYProperty() {
1207 if (contentPosY == null) {
1208 contentPosY = new DoublePropertyBase() {
1209 @Override protected void invalidated() {
1210 vsb.setValue(getContentPosY());
1211 getSkinnable().requestLayout();
1212 }
1213
1214 @Override
1215 public Object getBean() {
1216 return ScrollPaneSkin.this;
1217 }
1218
1219 @Override
1220 public String getName() {
1221 return "contentPosY";
1222 }
1223 };
1224 }
1225 return contentPosY;
1226 }
1227
1228 @Override
1229 protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
1230 switch (attribute) {
1231 case VERTICAL_SCROLLBAR: return vsb;
1232 case HORIZONTAL_SCROLLBAR: return hsb;
1233 default: return super.queryAccessibleAttribute(attribute, parameters);
1234 }
1235 }
1236 }
|
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.Properties;
29 import com.sun.javafx.scene.control.behavior.BehaviorBase;
30 import com.sun.javafx.scene.traversal.ParentTraversalEngine;
31 import javafx.animation.Animation.Status;
32 import javafx.animation.KeyFrame;
33 import javafx.animation.KeyValue;
34 import javafx.animation.Timeline;
35 import javafx.beans.InvalidationListener;
36 import javafx.beans.Observable;
37 import javafx.beans.property.DoubleProperty;
38 import javafx.beans.property.DoublePropertyBase;
39 import javafx.beans.value.ChangeListener;
40 import javafx.beans.value.ObservableValue;
41 import javafx.event.EventDispatcher;
42 import javafx.event.EventHandler;
43 import javafx.geometry.BoundingBox;
44 import javafx.geometry.Bounds;
45 import javafx.geometry.Orientation;
46 import javafx.scene.AccessibleAttribute;
47 import javafx.scene.Cursor;
48 import javafx.scene.Node;
49 import javafx.scene.control.Button;
50 import javafx.scene.control.Control;
51 import javafx.scene.control.ScrollBar;
52 import javafx.scene.control.ScrollPane;
53 import javafx.scene.control.ScrollPane.ScrollBarPolicy;
54 import javafx.scene.control.SkinBase;
55 import javafx.scene.input.MouseEvent;
56 import javafx.scene.input.ScrollEvent;
57 import javafx.scene.input.TouchEvent;
58 import javafx.scene.layout.StackPane;
59 import javafx.scene.shape.Rectangle;
60 import javafx.util.Duration;
61 import com.sun.javafx.util.Utils;
62 import com.sun.javafx.scene.control.behavior.ScrollPaneBehavior;
63 import static com.sun.javafx.scene.control.skin.Utils.*;
64 import javafx.geometry.Insets;
65
66 import java.util.function.Consumer;
67
68 /**
69 * Default skin implementation for the {@link ScrollPane} control.
70 *
71 * @see ScrollPane
72 * @since 9
73 */
74 public class ScrollPaneSkin extends SkinBase<ScrollPane> {
75 /***************************************************************************
76 * *
77 * Static fields *
78 * *
79 **************************************************************************/
80
81 private static final double DEFAULT_PREF_SIZE = 100.0;
82
83 private static final double DEFAULT_MIN_SIZE = 36.0;
84
85 private static final double DEFAULT_SB_BREADTH = 12.0;
86 private static final double DEFAULT_EMBEDDED_SB_BREADTH = 8.0;
87
88 private static final double PAN_THRESHOLD = 0.5;
89
90
91
92 /***************************************************************************
93 * *
94 * Private fields *
95 * *
96 **************************************************************************/
97
98 // state from the control
99
100 private Node scrollNode;
101 private final BehaviorBase<ScrollPane> behavior;
102
103 private double nodeWidth;
104 private double nodeHeight;
105 private boolean nodeSizeInvalid = true;
106
107 private double posX;
108 private double posY;
109
110 // working state
111
112 private boolean hsbvis;
113 private boolean vsbvis;
114 private double hsbHeight;
115 private double vsbWidth;
116
117 // substructure
118
119 private StackPane viewRect;
120 private StackPane viewContent;
121 private double contentWidth;
122 private double contentHeight;
123 private StackPane corner;
124 ScrollBar hsb;
125 ScrollBar vsb;
126
127 double pressX;
128 double pressY;
129 double ohvalue;
130 double ovvalue;
131 private Cursor saveCursor = null;
132 private boolean dragDetected = false;
133 private boolean touchDetected = false;
134 private boolean mouseDown = false;
135
136 Rectangle clipRect;
137
138 Timeline sbTouchTimeline;
139 KeyFrame sbTouchKF1;
140 KeyFrame sbTouchKF2;
141 Timeline contentsToViewTimeline;
142 KeyFrame contentsToViewKF1;
143 KeyFrame contentsToViewKF2;
144 KeyFrame contentsToViewKF3;
145
146 private boolean tempVisibility;
147
148
149
150 /***************************************************************************
151 * *
152 * Listeners *
153 * *
154 **************************************************************************/
155
156 private final InvalidationListener nodeListener = new InvalidationListener() {
157 @Override public void invalidated(Observable valueModel) {
158 if (!nodeSizeInvalid) {
159 final Bounds scrollNodeBounds = scrollNode.getLayoutBounds();
160 final double scrollNodeWidth = scrollNodeBounds.getWidth();
161 final double scrollNodeHeight = scrollNodeBounds.getHeight();
162
163 /*
164 ** if the new size causes scrollbar visibility to change, then need to relayout
165 ** we also need to correct the thumb size when the scrollnode's size changes
166 */
167 if (vsbvis != determineVerticalSBVisible() || hsbvis != determineHorizontalSBVisible() ||
168 (scrollNodeWidth != 0.0 && nodeWidth != scrollNodeWidth) ||
169 (scrollNodeHeight != 0.0 && nodeHeight != scrollNodeHeight)) {
170 getSkinnable().requestLayout();
171 } else {
172 /**
173 * we just need to update scrollbars based on new scrollNode size,
174 * but we don't do this while dragging, there's no need,
175 * and it jumps, as dragging updates the scrollbar too.
223 double oldWidth = oldBounds.getWidth();
224 double newWidth = newBounds.getWidth();
225 if (oldWidth > 0 && oldWidth != newWidth) {
226 double oldPositionX = (snapPosition(snappedLeftInset() - posX / (hsb.getMax() - hsb.getMin()) * (oldWidth - contentWidth)));
227 double newPositionX = (snapPosition(snappedLeftInset() - posX / (hsb.getMax() - hsb.getMin()) * (newWidth - contentWidth)));
228
229 double newValueX = (oldPositionX/newPositionX)*hsb.getValue();
230 if (newValueX < 0.0) {
231 hsb.setValue(0.0);
232 }
233 else if (newValueX < 1.0) {
234 hsb.setValue(newValueX);
235 }
236 else if (newValueX > 1.0) {
237 hsb.setValue(1.0);
238 }
239 }
240 }
241 };
242
243
244
245 /***************************************************************************
246 * *
247 * Constructors *
248 * *
249 **************************************************************************/
250
251 /**
252 * Creates a new ScrollPaneSkin instance, installing the necessary child
253 * nodes into the Control {@link Control#getChildren() children} list, as
254 * well as the necessary input mappings for handling key, mouse, etc events.
255 *
256 * @param control The control that this skin should be installed onto.
257 */
258 public ScrollPaneSkin(final ScrollPane control) {
259 super(control);
260
261 // install default input map for the ScrollPane control
262 behavior = new ScrollPaneBehavior(control);
263 // control.setInputMap(behavior.getInputMap());
264
265 initialize();
266
267 // Register listeners
268 Consumer<ObservableValue<?>> viewportSizeHintConsumer = e -> {
269 // change affects pref size, so requestLayout on control
270 getSkinnable().requestLayout();
271 };
272 registerChangeListener(control.contentProperty(), e -> {
273 if (scrollNode != getSkinnable().getContent()) {
274 if (scrollNode != null) {
275 scrollNode.layoutBoundsProperty().removeListener(nodeListener);
276 scrollNode.layoutBoundsProperty().removeListener(boundsChangeListener);
277 viewContent.getChildren().remove(scrollNode);
278 }
279 scrollNode = getSkinnable().getContent();
280 if (scrollNode != null) {
281 nodeWidth = snapSize(scrollNode.getLayoutBounds().getWidth());
282 nodeHeight = snapSize(scrollNode.getLayoutBounds().getHeight());
283 viewContent.getChildren().setAll(scrollNode);
284 scrollNode.layoutBoundsProperty().addListener(nodeListener);
285 scrollNode.layoutBoundsProperty().addListener(boundsChangeListener);
286 }
287 }
288 getSkinnable().requestLayout();
289 });
290 registerChangeListener(control.fitToWidthProperty(), e -> {
291 getSkinnable().requestLayout();
292 viewRect.requestLayout();
293 });
294 registerChangeListener(control.fitToHeightProperty(), e -> {
295 getSkinnable().requestLayout();
296 viewRect.requestLayout();
297 });
298 registerChangeListener(control.hbarPolicyProperty(), e -> {
299 // change might affect pref size, so requestLayout on control
300 getSkinnable().requestLayout();
301 });
302 registerChangeListener(control.vbarPolicyProperty(), e -> {
303 // change might affect pref size, so requestLayout on control
304 getSkinnable().requestLayout();
305 });
306 registerChangeListener(control.hvalueProperty(), e -> hsb.setValue(getSkinnable().getHvalue()));
307 registerChangeListener(control.hmaxProperty(), e -> hsb.setMax(getSkinnable().getHmax()));
308 registerChangeListener(control.hminProperty(), e -> hsb.setMin(getSkinnable().getHmin()));
309 registerChangeListener(control.vvalueProperty(), e -> vsb.setValue(getSkinnable().getVvalue()));
310 registerChangeListener(control.vmaxProperty(), e -> vsb.setMax(getSkinnable().getVmax()));
311 registerChangeListener(control.vminProperty(), e -> vsb.setMin(getSkinnable().getVmin()));
312 registerChangeListener(control.prefViewportWidthProperty(), viewportSizeHintConsumer);
313 registerChangeListener(control.prefViewportHeightProperty(), viewportSizeHintConsumer);
314 registerChangeListener(control.minViewportWidthProperty(), viewportSizeHintConsumer);
315 registerChangeListener(control.minViewportHeightProperty(), viewportSizeHintConsumer);
316 }
317
318
319
320 /***************************************************************************
321 * *
322 * Properties *
323 * *
324 **************************************************************************/
325
326 private DoubleProperty contentPosX;
327 private final void setContentPosX(double value) { contentPosXProperty().set(value); }
328 private final double getContentPosX() { return contentPosX == null ? 0.0 : contentPosX.get(); }
329 private final DoubleProperty contentPosXProperty() {
330 if (contentPosX == null) {
331 contentPosX = new DoublePropertyBase() {
332 @Override protected void invalidated() {
333 hsb.setValue(getContentPosX());
334 getSkinnable().requestLayout();
335 }
336
337 @Override
338 public Object getBean() {
339 return ScrollPaneSkin.this;
340 }
341
342 @Override
343 public String getName() {
344 return "contentPosX";
345 }
346 };
347 }
348 return contentPosX;
349 }
350
351 private DoubleProperty contentPosY;
352 private final void setContentPosY(double value) { contentPosYProperty().set(value); }
353 private final double getContentPosY() { return contentPosY == null ? 0.0 : contentPosY.get(); }
354 private final DoubleProperty contentPosYProperty() {
355 if (contentPosY == null) {
356 contentPosY = new DoublePropertyBase() {
357 @Override protected void invalidated() {
358 vsb.setValue(getContentPosY());
359 getSkinnable().requestLayout();
360 }
361
362 @Override
363 public Object getBean() {
364 return ScrollPaneSkin.this;
365 }
366
367 @Override
368 public String getName() {
369 return "contentPosY";
370 }
371 };
372 }
373 return contentPosY;
374 }
375
376
377
378 /***************************************************************************
379 * *
380 * Public API *
381 * *
382 **************************************************************************/
383
384 /** {@inheritDoc} */
385 @Override public void dispose() {
386 super.dispose();
387
388 if (behavior != null) {
389 behavior.dispose();
390 }
391 }
392
393 /**
394 * Returns the horizontal {@link ScrollBar} used in this ScrollPaneSkin
395 * instance.
396 */
397 public final ScrollBar getHorizontalScrollBar() {
398 return hsb;
399 }
400
401 /**
402 * Returns the vertical {@link ScrollBar} used in this ScrollPaneSkin
403 * instance.
404 */
405 public final ScrollBar getVerticalScrollBar() {
406 return vsb;
407 }
408
409 /** {@inheritDoc} */
410 @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
411 final ScrollPane sp = getSkinnable();
412
413 double vsbWidth = computeVsbSizeHint(sp);
414 double minWidth = vsbWidth + snappedLeftInset() + snappedRightInset();
415
416 if (sp.getPrefViewportWidth() > 0) {
417 return (sp.getPrefViewportWidth() + minWidth);
418 }
419 else if (sp.getContent() != null) {
420 return (sp.getContent().prefWidth(height) + minWidth);
421 }
422 else {
423 return Math.max(minWidth, DEFAULT_PREF_SIZE);
424 }
425 }
426
427 /** {@inheritDoc} */
428 @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
429 final ScrollPane sp = getSkinnable();
430
431 double hsbHeight = computeHsbSizeHint(sp);
432 double minHeight = hsbHeight + snappedTopInset() + snappedBottomInset();
433
434 if (sp.getPrefViewportHeight() > 0) {
435 return (sp.getPrefViewportHeight() + minHeight);
436 }
437 else if (sp.getContent() != null) {
438 return (sp.getContent().prefHeight(width) + minHeight);
439 }
440 else {
441 return Math.max(minHeight, DEFAULT_PREF_SIZE);
442 }
443 }
444
445 /** {@inheritDoc} */
446 @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
447 final ScrollPane sp = getSkinnable();
448
449 double vsbWidth = computeVsbSizeHint(sp);
450 double minWidth = vsbWidth + snappedLeftInset() + snappedRightInset();
451
452 if (sp.getMinViewportWidth() > 0) {
453 return (sp.getMinViewportWidth() + minWidth);
454 } else {
455 double w = corner.minWidth(-1);
456 return (w > 0) ? (3 * w) : (DEFAULT_MIN_SIZE);
457 }
458
459 }
460
461 /** {@inheritDoc} */
462 @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
463 final ScrollPane sp = getSkinnable();
464
465 double hsbHeight = computeHsbSizeHint(sp);
466 double minHeight = hsbHeight + snappedTopInset() + snappedBottomInset();
467
468 if (sp.getMinViewportHeight() > 0) {
469 return (sp.getMinViewportHeight() + minHeight);
470 } else {
471 double h = corner.minHeight(-1);
472 return (h > 0) ? (3 * h) : (DEFAULT_MIN_SIZE);
473 }
474 }
475
476 @Override protected void layoutChildren(final double x, final double y,
477 final double w, final double h) {
478 final ScrollPane control = getSkinnable();
479 final Insets padding = control.getPadding();
480 final double rightPadding = snapSize(padding.getRight());
481 final double leftPadding = snapSize(padding.getLeft());
482 final double topPadding = snapSize(padding.getTop());
483 final double bottomPadding = snapSize(padding.getBottom());
484
485 vsb.setMin(control.getVmin());
486 vsb.setMax(control.getVmax());
487
488 //should only do this on css setup
489 hsb.setMin(control.getHmin());
490 hsb.setMax(control.getHmax());
491
492 contentWidth = w;
493 contentHeight = h;
494
495 /*
496 ** we want the scrollbars to go right to the border
497 */
498 double hsbWidth = 0;
499 double vsbHeight = 0;
500
501 computeScrollNodeSize(contentWidth, contentHeight);
502 computeScrollBarSize();
503
504 for (int i = 0; i < 2; ++i) {
505 vsbvis = determineVerticalSBVisible();
506 hsbvis = determineHorizontalSBVisible();
507
508 if (vsbvis && !Properties.IS_TOUCH_SUPPORTED) {
509 contentWidth = w - vsbWidth;
510 }
511 hsbWidth = w + leftPadding + rightPadding - (vsbvis ? vsbWidth : 0);
512 if (hsbvis && !Properties.IS_TOUCH_SUPPORTED) {
513 contentHeight = h - hsbHeight;
514 }
515 vsbHeight = h + topPadding + bottomPadding - (hsbvis ? hsbHeight : 0);
516 }
517
518
519 if (scrollNode != null && scrollNode.isResizable()) {
520 // maybe adjust size now that scrollbars may take up space
521 if (vsbvis && hsbvis) {
522 // adjust just once to accommodate
523 computeScrollNodeSize(contentWidth, contentHeight);
524
525 } else if (hsbvis && !vsbvis) {
526 computeScrollNodeSize(contentWidth, contentHeight);
527 vsbvis = determineVerticalSBVisible();
528 if (vsbvis) {
529 // now both are visible
530 contentWidth -= vsbWidth;
531 hsbWidth -= vsbWidth;
532 computeScrollNodeSize(contentWidth, contentHeight);
533 }
534 } else if (vsbvis && !hsbvis) {
535 computeScrollNodeSize(contentWidth, contentHeight);
536 hsbvis = determineHorizontalSBVisible();
537 if (hsbvis) {
538 // now both are visible
539 contentHeight -= hsbHeight;
540 vsbHeight -= hsbHeight;
541 computeScrollNodeSize(contentWidth, contentHeight);
542 }
543 }
544 }
545
546 // figure out the content area that is to be filled
547 double cx = snappedLeftInset() - leftPadding;
548 double cy = snappedTopInset() - topPadding;
549
550 vsb.setVisible(vsbvis);
551 if (vsbvis) {
552 /*
553 ** round up position of ScrollBar, round down it's size.
554 **
555 ** Positioning the ScrollBar
556 ** The Padding should go between the content and the edge,
557 ** otherwise changes in padding move the ScrollBar, and could
558 ** in extreme cases size the ScrollBar to become unusable.
559 ** The -1, +1 plus one bit :
560 ** If padding in => 1 then we allow one pixel to appear as the
561 ** outside border of the Scrollbar, and the rest on the inside.
562 ** If padding is < 1 then we just stick to the edge.
563 */
564 vsb.resizeRelocate(snappedLeftInset() + w - vsbWidth + (rightPadding < 1 ? 0 : rightPadding - 1) ,
565 cy, vsbWidth, vsbHeight);
566 }
567 updateVerticalSB();
568
569 hsb.setVisible(hsbvis);
570 if (hsbvis) {
571 /*
572 ** round up position of ScrollBar, round down it's size.
573 **
574 ** Positioning the ScrollBar
575 ** The Padding should go between the content and the edge,
576 ** otherwise changes in padding move the ScrollBar, and could
577 ** in extreme cases size the ScrollBar to become unusable.
578 ** The -1, +1 plus one bit :
579 ** If padding in => 1 then we allow one pixel to appear as the
580 ** outside border of the Scrollbar, and the rest on the inside.
581 ** If padding is < 1 then we just stick to the edge.
582 */
583 hsb.resizeRelocate(cx, snappedTopInset() + h - hsbHeight + (bottomPadding < 1 ? 0 : bottomPadding - 1),
584 hsbWidth, hsbHeight);
585 }
586 updateHorizontalSB();
587
588 viewRect.resizeRelocate(snappedLeftInset(), snappedTopInset(), snapSize(contentWidth), snapSize(contentHeight));
589 resetClip();
590
591 if (vsbvis && hsbvis) {
592 corner.setVisible(true);
593 double cornerWidth = vsbWidth;
594 double cornerHeight = hsbHeight;
595 corner.resizeRelocate(snapPosition(vsb.getLayoutX()), snapPosition(hsb.getLayoutY()), snapSize(cornerWidth), snapSize(cornerHeight));
596 } else {
597 corner.setVisible(false);
598 }
599 control.setViewportBounds(new BoundingBox(snapPosition(viewContent.getLayoutX()), snapPosition(viewContent.getLayoutY()), snapSize(contentWidth), snapSize(contentHeight)));
600 }
601
602 /** {@inheritDoc} */
603 @Override protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
604 switch (attribute) {
605 case VERTICAL_SCROLLBAR: return vsb;
606 case HORIZONTAL_SCROLLBAR: return hsb;
607 default: return super.queryAccessibleAttribute(attribute, parameters);
608 }
609 }
610
611
612
613 /***************************************************************************
614 * *
615 * Private implementation *
616 * *
617 **************************************************************************/
618
619 private void initialize() {
620 // requestLayout calls below should not trigger requestLayout above ScrollPane
621 // setManaged(false);
622
623 ScrollPane control = getSkinnable();
624 scrollNode = control.getContent();
625
626 ParentTraversalEngine traversalEngine = new ParentTraversalEngine(getSkinnable());
627 traversalEngine.addTraverseListener((node, bounds) -> {
628 // auto-scroll so node is within (0,0),(contentWidth,contentHeight)
629 scrollBoundsIntoView(bounds);
630 });
631 getSkinnable().setImpl_traversalEngine(traversalEngine);
632
633 if (scrollNode != null) {
634 scrollNode.layoutBoundsProperty().addListener(nodeListener);
635 scrollNode.layoutBoundsProperty().addListener(boundsChangeListener);
636 }
637
638 viewRect = new StackPane() {
639 @Override protected void layoutChildren() {
640 viewContent.resize(getWidth(), getHeight());
641 }
642 };
643 // prevent requestLayout requests from within scrollNode from percolating up
644 viewRect.setManaged(false);
645 viewRect.setCache(true);
646 viewRect.getStyleClass().add("viewport");
647
648 clipRect = new Rectangle();
649 viewRect.setClip(clipRect);
650
651 hsb = new ScrollBar();
652
653 vsb = new ScrollBar();
654 vsb.setOrientation(Orientation.VERTICAL);
655
656 EventHandler<MouseEvent> barHandler = ev -> {
657 getSkinnable().requestFocus();
658 };
659
660 hsb.addEventFilter(MouseEvent.MOUSE_PRESSED, barHandler);
661 vsb.addEventFilter(MouseEvent.MOUSE_PRESSED, barHandler);
686 }
687 if (scrollNode != null) {
688 scrollNode.relocate(0,0);
689 }
690 }
691 };
692 viewRect.getChildren().add(viewContent);
693
694 if (scrollNode != null) {
695 viewContent.getChildren().add(scrollNode);
696 viewRect.nodeOrientationProperty().bind(scrollNode.nodeOrientationProperty());
697 }
698
699 getChildren().clear();
700 getChildren().addAll(viewRect, vsb, hsb, corner);
701
702 /*
703 ** listeners, and assorted housekeeping
704 */
705 InvalidationListener vsbListener = valueModel -> {
706 if (!Properties.IS_TOUCH_SUPPORTED) {
707 posY = Utils.clamp(getSkinnable().getVmin(), vsb.getValue(), getSkinnable().getVmax());
708 }
709 else {
710 posY = vsb.getValue();
711 }
712 updatePosY();
713 };
714 vsb.valueProperty().addListener(vsbListener);
715
716 InvalidationListener hsbListener = valueModel -> {
717 if (!Properties.IS_TOUCH_SUPPORTED) {
718 posX = Utils.clamp(getSkinnable().getHmin(), hsb.getValue(), getSkinnable().getHmax());
719 }
720 else {
721 posX = hsb.getValue();
722 }
723 updatePosX();
724 };
725 hsb.valueProperty().addListener(hsbListener);
726
727 viewRect.setOnMousePressed(e -> {
728 mouseDown = true;
729 if (Properties.IS_TOUCH_SUPPORTED) {
730 startSBReleasedAnimation();
731 }
732 pressX = e.getX();
733 pressY = e.getY();
734 ohvalue = hsb.getValue();
735 ovvalue = vsb.getValue();
736 });
737
738
739 viewRect.setOnDragDetected(e -> {
740 if (Properties.IS_TOUCH_SUPPORTED) {
741 startSBReleasedAnimation();
742 }
743 if (getSkinnable().isPannable()) {
744 dragDetected = true;
745 if (saveCursor == null) {
746 saveCursor = getSkinnable().getCursor();
747 if (saveCursor == null) {
748 saveCursor = Cursor.DEFAULT;
749 }
750 getSkinnable().setCursor(Cursor.MOVE);
751 getSkinnable().requestLayout();
752 }
753 }
754 });
755
756 viewRect.addEventFilter(MouseEvent.MOUSE_RELEASED, e -> {
757 mouseDown = false;
758 if (dragDetected == true) {
759 if (saveCursor != null) {
760 getSkinnable().setCursor(saveCursor);
761 saveCursor = null;
762 getSkinnable().requestLayout();
763 }
764 dragDetected = false;
765 }
766
767 /*
768 ** if the contents need repositioning, and there's is no
769 ** touch event in progress, then start the repositioning.
770 */
771 if ((posY > getSkinnable().getVmax() || posY < getSkinnable().getVmin() ||
772 posX > getSkinnable().getHmax() || posX < getSkinnable().getHmin()) && !touchDetected) {
773 startContentsToViewport();
774 }
775 });
776 viewRect.setOnMouseDragged(e -> {
777 if (Properties.IS_TOUCH_SUPPORTED) {
778 startSBReleasedAnimation();
779 }
780 /*
781 ** for mobile-touch we allow drag, even if not pannagle
782 */
783 if (getSkinnable().isPannable() || Properties.IS_TOUCH_SUPPORTED) {
784 double deltaX = pressX - e.getX();
785 double deltaY = pressY - e.getY();
786 /*
787 ** we only drag if not all of the content is visible.
788 */
789 if (hsb.getVisibleAmount() > 0.0 && hsb.getVisibleAmount() < hsb.getMax()) {
790 if (Math.abs(deltaX) > PAN_THRESHOLD) {
791 if (isReverseNodeOrientation()) {
792 deltaX = -deltaX;
793 }
794 double newHVal = (ohvalue + deltaX / (nodeWidth - viewRect.getWidth()) * (hsb.getMax() - hsb.getMin()));
795 if (!Properties.IS_TOUCH_SUPPORTED) {
796 if (newHVal > hsb.getMax()) {
797 newHVal = hsb.getMax();
798 }
799 else if (newHVal < hsb.getMin()) {
800 newHVal = hsb.getMin();
801 }
802 hsb.setValue(newHVal);
803 }
804 else {
805 hsb.setValue(newHVal);
806 }
807 }
808 }
809 /*
810 ** we only drag if not all of the content is visible.
811 */
812 if (vsb.getVisibleAmount() > 0.0 && vsb.getVisibleAmount() < vsb.getMax()) {
813 if (Math.abs(deltaY) > PAN_THRESHOLD) {
814 double newVVal = (ovvalue + deltaY / (nodeHeight - viewRect.getHeight()) * (vsb.getMax() - vsb.getMin()));
815 if (!Properties.IS_TOUCH_SUPPORTED) {
816 if (newVVal > vsb.getMax()) {
817 newVVal = vsb.getMax();
818 }
819 else if (newVVal < vsb.getMin()) {
820 newVVal = vsb.getMin();
821 }
822 vsb.setValue(newVVal);
823 }
824 else {
825 vsb.setValue(newVVal);
826 }
827 }
828 }
829 }
830 /*
831 ** we need to consume drag events, as we don't want
832 ** the scrollpane itself to be dragged on every mouse click
833 */
834 e.consume();
835 });
860 !((ScrollEvent)event).isDirect()) {
861 tail = tail.prepend(blockEventDispatcher);
862 tail = tail.prepend(oldVsbEventDispatcher);
863 return tail.dispatchEvent(event);
864 }
865 return oldVsbEventDispatcher.dispatchEvent(event, tail);
866 });
867
868 /*
869 * listen for ScrollEvents over the whole of the ScrollPane
870 * area, the above dispatcher having removed the ScrollBars
871 * scroll event handling.
872 *
873 * Note that we use viewRect here, rather than setting the eventHandler
874 * on the ScrollPane itself. This is for RT-31582, and effectively
875 * allows for us to prioritise handling (and consuming) the event
876 * internally, before it is made available to users listening to events
877 * on the control. This is consistent with the VirtualFlow-based controls.
878 */
879 viewRect.addEventHandler(ScrollEvent.SCROLL, event -> {
880 if (Properties.IS_TOUCH_SUPPORTED) {
881 startSBReleasedAnimation();
882 }
883 /*
884 ** if we're completely visible then do nothing....
885 ** we only consume an event that we've used.
886 */
887 if (vsb.getVisibleAmount() < vsb.getMax()) {
888 double vRange = getSkinnable().getVmax()-getSkinnable().getVmin();
889 double vPixelValue;
890 if (nodeHeight > 0.0) {
891 vPixelValue = vRange / nodeHeight;
892 }
893 else {
894 vPixelValue = 0.0;
895 }
896 double newValue = vsb.getValue()+(-event.getDeltaY())*vPixelValue;
897 if (!Properties.IS_TOUCH_SUPPORTED) {
898 if ((event.getDeltaY() > 0.0 && vsb.getValue() > vsb.getMin()) ||
899 (event.getDeltaY() < 0.0 && vsb.getValue() < vsb.getMax())) {
900 vsb.setValue(newValue);
901 event.consume();
902 }
903 }
904 else {
905 /*
906 ** if there is a repositioning in progress then we only
907 ** set the value for 'real' events
908 */
909 if (!(((ScrollEvent)event).isInertia()) || (((ScrollEvent)event).isInertia()) && (contentsToViewTimeline == null || contentsToViewTimeline.getStatus() == Status.STOPPED)) {
910 vsb.setValue(newValue);
911 if ((newValue > vsb.getMax() || newValue < vsb.getMin()) && (!mouseDown && !touchDetected)) {
912 startContentsToViewport();
913 }
914 event.consume();
915 }
916 }
917 }
918
919 if (hsb.getVisibleAmount() < hsb.getMax()) {
920 double hRange = getSkinnable().getHmax()-getSkinnable().getHmin();
921 double hPixelValue;
922 if (nodeWidth > 0.0) {
923 hPixelValue = hRange / nodeWidth;
924 }
925 else {
926 hPixelValue = 0.0;
927 }
928
929 double newValue = hsb.getValue()+(-event.getDeltaX())*hPixelValue;
930 if (!Properties.IS_TOUCH_SUPPORTED) {
931 if ((event.getDeltaX() > 0.0 && hsb.getValue() > hsb.getMin()) ||
932 (event.getDeltaX() < 0.0 && hsb.getValue() < hsb.getMax())) {
933 hsb.setValue(newValue);
934 event.consume();
935 }
936 }
937 else {
938 /*
939 ** if there is a repositioning in progress then we only
940 ** set the value for 'real' events
941 */
942 if (!(((ScrollEvent)event).isInertia()) || (((ScrollEvent)event).isInertia()) && (contentsToViewTimeline == null || contentsToViewTimeline.getStatus() == Status.STOPPED)) {
943 hsb.setValue(newValue);
944
945 if ((newValue > hsb.getMax() || newValue < hsb.getMin()) && (!mouseDown && !touchDetected)) {
946 startContentsToViewport();
947 }
948 event.consume();
949 }
950 }
951 }
952 });
953
954 /*
955 ** there are certain animations that need to know if the touch is
956 ** happening.....
957 */
958 getSkinnable().addEventHandler(TouchEvent.TOUCH_PRESSED, e -> {
959 touchDetected = true;
960 startSBReleasedAnimation();
961 e.consume();
962 });
963
964 getSkinnable().addEventHandler(TouchEvent.TOUCH_RELEASED, e -> {
965 touchDetected = false;
966 e.consume();
967 });
968
969 // ScrollPanes do not block all MouseEvents by default, unlike most other UI Controls.
970 consumeMouseEvents(false);
971
972 // update skin initial state to match control (see RT-35554)
973 hsb.setValue(control.getHvalue());
974 vsb.setValue(control.getVvalue());
975 }
976
977 void scrollBoundsIntoView(Bounds b) {
978 double dx = 0.0;
979 double dy = 0.0;
980 if (b.getMaxX() > contentWidth) {
981 dx = b.getMinX() - snappedLeftInset();
982 }
983 if (b.getMinX() < snappedLeftInset()) {
984 dx = b.getMaxX() - contentWidth - snappedLeftInset();
985 }
986 if (b.getMaxY() > snappedTopInset() + contentHeight) {
987 dy = b.getMinY() - snappedTopInset();
988 }
989 if (b.getMinY() < snappedTopInset()) {
990 dy = b.getMaxY() - contentHeight - snappedTopInset();
991 }
992 // We want to move contentPanel's layoutX,Y by (dx,dy).
993 // But to do this we have to set the scrollbars' values appropriately.
994
995 if (dx != 0) {
996 double sdx = dx * (hsb.getMax() - hsb.getMin()) / (nodeWidth - contentWidth);
997 // Adjust back for some amount so that the Node border is not too close to view border
998 sdx += -1 * Math.signum(sdx) * hsb.getUnitIncrement() / 5; // This accounts to 2% of view width
999 hsb.setValue(hsb.getValue() + sdx);
1000 getSkinnable().requestLayout();
1001 }
1002 if (dy != 0) {
1003 double sdy = dy * (vsb.getMax() - vsb.getMin()) / (nodeHeight - contentHeight);
1004 // Adjust back for some amount so that the Node border is not too close to view border
1005 sdy += -1 * Math.signum(sdy) * vsb.getUnitIncrement() / 5; // This accounts to 2% of view height
1006 vsb.setValue(vsb.getValue() + sdy);
1007 getSkinnable().requestLayout();
1008 }
1009
1010 }
1011
1012 /**
1013 * Computes the size that should be reserved for horizontal scrollbar in size hints (min/pref height)
1014 */
1015 private double computeHsbSizeHint(ScrollPane sp) {
1016 return ((sp.getHbarPolicy() == ScrollBarPolicy.ALWAYS) ||
1017 (sp.getHbarPolicy() == ScrollBarPolicy.AS_NEEDED && (sp.getPrefViewportHeight() > 0 || sp.getMinViewportHeight() > 0)))
1018 ? hsb.prefHeight(ScrollBar.USE_COMPUTED_SIZE)
1019 : 0;
1020 }
1021
1022 /**
1023 * Computes the size that should be reserved for vertical scrollbar in size hints (min/pref width)
1024 */
1025 private double computeVsbSizeHint(ScrollPane sp) {
1026 return ((sp.getVbarPolicy() == ScrollBarPolicy.ALWAYS) ||
1027 (sp.getVbarPolicy() == ScrollBarPolicy.AS_NEEDED && (sp.getPrefViewportWidth() > 0
1028 || sp.getMinViewportWidth() > 0)))
1029 ? vsb.prefWidth(ScrollBar.USE_COMPUTED_SIZE)
1030 : 0;
1031 }
1032
1033 private void computeScrollNodeSize(double contentWidth, double contentHeight) {
1034 if (scrollNode != null) {
1035 if (scrollNode.isResizable()) {
1036 ScrollPane control = getSkinnable();
1037 Orientation bias = scrollNode.getContentBias();
1038 if (bias == null) {
1039 nodeWidth = snapSize(boundedSize(control.isFitToWidth()? contentWidth : scrollNode.prefWidth(-1),
1040 scrollNode.minWidth(-1),scrollNode.maxWidth(-1)));
1041 nodeHeight = snapSize(boundedSize(control.isFitToHeight()? contentHeight : scrollNode.prefHeight(-1),
1042 scrollNode.minHeight(-1), scrollNode.maxHeight(-1)));
1043
1044 } else if (bias == Orientation.HORIZONTAL) {
1045 nodeWidth = snapSize(boundedSize(control.isFitToWidth()? contentWidth : scrollNode.prefWidth(-1),
1046 scrollNode.minWidth(-1),scrollNode.maxWidth(-1)));
1047 nodeHeight = snapSize(boundedSize(control.isFitToHeight()? contentHeight : scrollNode.prefHeight(nodeWidth),
1048 scrollNode.minHeight(nodeWidth),scrollNode.maxHeight(nodeWidth)));
1049
1050 } else { // bias == VERTICAL
1051 nodeHeight = snapSize(boundedSize(control.isFitToHeight()? contentHeight : scrollNode.prefHeight(-1),
1052 scrollNode.minHeight(-1), scrollNode.maxHeight(-1)));
1054 scrollNode.minWidth(nodeHeight),scrollNode.maxWidth(nodeHeight)));
1055 }
1056
1057 } else {
1058 nodeWidth = snapSize(scrollNode.getLayoutBounds().getWidth());
1059 nodeHeight = snapSize(scrollNode.getLayoutBounds().getHeight());
1060 }
1061 nodeSizeInvalid = false;
1062 }
1063 }
1064
1065 private boolean isReverseNodeOrientation() {
1066 return (scrollNode != null &&
1067 getSkinnable().getEffectiveNodeOrientation() !=
1068 scrollNode.getEffectiveNodeOrientation());
1069 }
1070
1071 private boolean determineHorizontalSBVisible() {
1072 final ScrollPane sp = getSkinnable();
1073
1074 if (Properties.IS_TOUCH_SUPPORTED) {
1075 return (tempVisibility && (nodeWidth > contentWidth));
1076 }
1077 else {
1078 // RT-17395: ScrollBarPolicy might be null. If so, treat it as "AS_NEEDED", which is the default
1079 ScrollBarPolicy hbarPolicy = sp.getHbarPolicy();
1080 return (ScrollBarPolicy.NEVER == hbarPolicy) ? false :
1081 ((ScrollBarPolicy.ALWAYS == hbarPolicy) ? true :
1082 ((sp.isFitToWidth() && scrollNode != null ? scrollNode.isResizable() : false) ?
1083 (nodeWidth > contentWidth && scrollNode.minWidth(-1) > contentWidth) : (nodeWidth > contentWidth)));
1084 }
1085 }
1086
1087 private boolean determineVerticalSBVisible() {
1088 final ScrollPane sp = getSkinnable();
1089
1090 if (Properties.IS_TOUCH_SUPPORTED) {
1091 return (tempVisibility && (nodeHeight > contentHeight));
1092 }
1093 else {
1094 // RT-17395: ScrollBarPolicy might be null. If so, treat it as "AS_NEEDED", which is the default
1095 ScrollBarPolicy vbarPolicy = sp.getVbarPolicy();
1096 return (ScrollBarPolicy.NEVER == vbarPolicy) ? false :
1097 ((ScrollBarPolicy.ALWAYS == vbarPolicy) ? true :
1098 ((sp.isFitToHeight() && scrollNode != null ? scrollNode.isResizable() : false) ?
1099 (nodeHeight > contentHeight && scrollNode.minHeight(-1) > contentHeight) : (nodeHeight > contentHeight)));
1100 }
1101 }
1102
1103 private void computeScrollBarSize() {
1104 vsbWidth = snapSize(vsb.prefWidth(-1));
1105 if (vsbWidth == 0) {
1106 // println("*** WARNING ScrollPaneSkin: can't get scroll bar width, using {DEFAULT_SB_BREADTH}");
1107 if (Properties.IS_TOUCH_SUPPORTED) {
1108 vsbWidth = DEFAULT_EMBEDDED_SB_BREADTH;
1109 }
1110 else {
1111 vsbWidth = DEFAULT_SB_BREADTH;
1112 }
1113 }
1114 hsbHeight = snapSize(hsb.prefHeight(-1));
1115 if (hsbHeight == 0) {
1116 // println("*** WARNING ScrollPaneSkin: can't get scroll bar height, using {DEFAULT_SB_BREADTH}");
1117 if (Properties.IS_TOUCH_SUPPORTED) {
1118 hsbHeight = DEFAULT_EMBEDDED_SB_BREADTH;
1119 }
1120 else {
1121 hsbHeight = DEFAULT_SB_BREADTH;
1122 }
1123 }
1124 }
1125
1126 private void updateHorizontalSB() {
1127 double contentRatio = nodeWidth * (hsb.getMax() - hsb.getMin());
1128 if (contentRatio > 0.0) {
1129 hsb.setVisibleAmount(contentWidth / contentRatio);
1130 hsb.setBlockIncrement(0.9 * hsb.getVisibleAmount());
1131 hsb.setUnitIncrement(0.1 * hsb.getVisibleAmount());
1132 }
1133 else {
1134 hsb.setVisibleAmount(0.0);
1135 hsb.setBlockIncrement(0.0);
1136 hsb.setUnitIncrement(0.0);
1137 }
1176 double x = isReverseNodeOrientation() ? (hsb.getMax() - (posX - hsb.getMin())) : posX;
1177 double minX = Math.min((- x / (hsb.getMax() - hsb.getMin()) * (nodeWidth - contentWidth)), 0);
1178 viewContent.setLayoutX(snapPosition(minX));
1179 if (!sp.hvalueProperty().isBound()) sp.setHvalue(Utils.clamp(sp.getHmin(), posX, sp.getHmax()));
1180 return posX;
1181 }
1182
1183 private double updatePosY() {
1184 final ScrollPane sp = getSkinnable();
1185 double minY = Math.min((- posY / (vsb.getMax() - vsb.getMin()) * (nodeHeight - contentHeight)), 0);
1186 viewContent.setLayoutY(snapPosition(minY));
1187 if (!sp.vvalueProperty().isBound()) sp.setVvalue(Utils.clamp(sp.getVmin(), posY, sp.getVmax()));
1188 return posY;
1189 }
1190
1191 private void resetClip() {
1192 clipRect.setWidth(snapSize(contentWidth));
1193 clipRect.setHeight(snapSize(contentHeight));
1194 }
1195
1196 private void startSBReleasedAnimation() {
1197 if (sbTouchTimeline == null) {
1198 /*
1199 ** timeline to leave the scrollbars visible for a short
1200 ** while after a scroll/drag
1201 */
1202 sbTouchTimeline = new Timeline();
1203 sbTouchKF1 = new KeyFrame(Duration.millis(0), event -> {
1204 tempVisibility = true;
1205 if (touchDetected == true || mouseDown == true) {
1206 sbTouchTimeline.playFromStart();
1207 }
1208 });
1209
1210 sbTouchKF2 = new KeyFrame(Duration.millis(1000), event -> {
1211 tempVisibility = false;
1212 getSkinnable().requestLayout();
1213 });
1214 sbTouchTimeline.getKeyFrames().addAll(sbTouchKF1, sbTouchKF2);
1215 }
1216 sbTouchTimeline.playFromStart();
1217 }
1218
1219 private void startContentsToViewport() {
1220 double newPosX = posX;
1221 double newPosY = posY;
1222
1223 setContentPosX(posX);
1224 setContentPosY(posY);
1225
1226 if (posY > getSkinnable().getVmax()) {
1227 newPosY = getSkinnable().getVmax();
1228 }
1229 else if (posY < getSkinnable().getVmin()) {
1230 newPosY = getSkinnable().getVmin();
1231 }
1232
1233
1234 if (posX > getSkinnable().getHmax()) {
1235 newPosX = getSkinnable().getHmax();
1236 }
1237 else if (posX < getSkinnable().getHmin()) {
1238 newPosX = getSkinnable().getHmin();
1239 }
1240
1241 if (!Properties.IS_TOUCH_SUPPORTED) {
1242 startSBReleasedAnimation();
1243 }
1244
1245 /*
1246 ** timeline to return the contents of the scrollpane to the viewport
1247 */
1248 if (contentsToViewTimeline != null) {
1249 contentsToViewTimeline.stop();
1250 }
1251 contentsToViewTimeline = new Timeline();
1252 /*
1253 ** short pause before animation starts
1254 */
1255 contentsToViewKF1 = new KeyFrame(Duration.millis(50));
1256 /*
1257 ** reposition
1258 */
1259 contentsToViewKF2 = new KeyFrame(Duration.millis(150), event -> {
1260 getSkinnable().requestLayout();
1261 },
1262 new KeyValue(contentPosX, newPosX),
1263 new KeyValue(contentPosY, newPosY)
1264 );
1265 /*
1266 ** block out 'aftershocks', but real events will
1267 ** still reactivate
1268 */
1269 contentsToViewKF3 = new KeyFrame(Duration.millis(1500));
1270 contentsToViewTimeline.getKeyFrames().addAll(contentsToViewKF1, contentsToViewKF2, contentsToViewKF3);
1271 contentsToViewTimeline.playFromStart();
1272 }
1273 }
|