1 /* 2 * Copyright (c) 2010, 2015, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package javafx.scene.control.skin; 27 28 import com.sun.javafx.scene.control.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. 176 */ 177 if (!dragDetected) { 178 updateVerticalSB(); 179 updateHorizontalSB(); 180 } 181 } 182 } 183 } 184 }; 185 186 187 /* 188 ** The content of the ScrollPane has just changed bounds, check scrollBar positions. 189 */ 190 private final ChangeListener<Bounds> boundsChangeListener = new ChangeListener<Bounds>() { 191 @Override public void changed(ObservableValue<? extends Bounds> observable, Bounds oldBounds, Bounds newBounds) { 192 193 /* 194 ** For a height change then we want to reduce 195 ** viewport vertical jumping as much as possible. 196 ** We set a new vsb value to try to keep the same 197 ** content position at the top of the viewport 198 */ 199 double oldHeight = oldBounds.getHeight(); 200 double newHeight = newBounds.getHeight(); 201 if (oldHeight > 0 && oldHeight != newHeight) { 202 double oldPositionY = (snapPosition(snappedTopInset() - posY / (vsb.getMax() - vsb.getMin()) * (oldHeight - contentHeight))); 203 double newPositionY = (snapPosition(snappedTopInset() - posY / (vsb.getMax() - vsb.getMin()) * (newHeight - contentHeight))); 204 205 double newValueY = (oldPositionY/newPositionY)*vsb.getValue(); 206 if (newValueY < 0.0) { 207 vsb.setValue(0.0); 208 } 209 else if (newValueY < 1.0) { 210 vsb.setValue(newValueY); 211 } 212 else if (newValueY > 1.0) { 213 vsb.setValue(1.0); 214 } 215 } 216 217 /* 218 ** For a width change then we want to reduce 219 ** viewport horizontal jumping as much as possible. 220 ** We set a new hsb value to try to keep the same 221 ** content position to the left of the viewport 222 */ 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); 662 663 corner = new StackPane(); 664 corner.getStyleClass().setAll("corner"); 665 666 viewContent = new StackPane() { 667 @Override public void requestLayout() { 668 // if scrollNode requested layout, will want to recompute 669 nodeSizeInvalid = true; 670 671 super.requestLayout(); // add as layout root for next layout pass 672 673 // Need to layout the ScrollPane as well in case scrollbars 674 // appeared or disappeared. 675 ScrollPaneSkin.this.getSkinnable().requestLayout(); 676 } 677 @Override protected void layoutChildren() { 678 if (nodeSizeInvalid) { 679 computeScrollNodeSize(getWidth(),getHeight()); 680 } 681 if (scrollNode != null && scrollNode.isResizable()) { 682 scrollNode.resize(snapSize(nodeWidth), snapSize(nodeHeight)); 683 if (vsbvis != determineVerticalSBVisible() || hsbvis != determineHorizontalSBVisible()) { 684 getSkinnable().requestLayout(); 685 } 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 }); 836 837 838 /* 839 ** don't allow the ScrollBar to handle the ScrollEvent, 840 ** In a ScrollPane a vertical scroll should scroll on the vertical only, 841 ** whereas in a horizontal ScrollBar it can scroll horizontally. 842 */ 843 // block the event from being passed down to children 844 final EventDispatcher blockEventDispatcher = (event, tail) -> event; 845 // block ScrollEvent from being passed down to scrollbar's skin 846 final EventDispatcher oldHsbEventDispatcher = hsb.getEventDispatcher(); 847 hsb.setEventDispatcher((event, tail) -> { 848 if (event.getEventType() == ScrollEvent.SCROLL && 849 !((ScrollEvent)event).isDirect()) { 850 tail = tail.prepend(blockEventDispatcher); 851 tail = tail.prepend(oldHsbEventDispatcher); 852 return tail.dispatchEvent(event); 853 } 854 return oldHsbEventDispatcher.dispatchEvent(event, tail); 855 }); 856 // block ScrollEvent from being passed down to scrollbar's skin 857 final EventDispatcher oldVsbEventDispatcher = vsb.getEventDispatcher(); 858 vsb.setEventDispatcher((event, tail) -> { 859 if (event.getEventType() == ScrollEvent.SCROLL && 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))); 1053 nodeWidth = snapSize(boundedSize(control.isFitToWidth()? contentWidth : scrollNode.prefWidth(nodeHeight), 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 } 1138 1139 if (hsb.isVisible()) { 1140 updatePosX(); 1141 } else { 1142 if (nodeWidth > contentWidth) { 1143 updatePosX(); 1144 } else { 1145 viewContent.setLayoutX(0); 1146 } 1147 } 1148 } 1149 1150 private void updateVerticalSB() { 1151 double contentRatio = nodeHeight * (vsb.getMax() - vsb.getMin()); 1152 if (contentRatio > 0.0) { 1153 vsb.setVisibleAmount(contentHeight / contentRatio); 1154 vsb.setBlockIncrement(0.9 * vsb.getVisibleAmount()); 1155 vsb.setUnitIncrement(0.1 * vsb.getVisibleAmount()); 1156 } 1157 else { 1158 vsb.setVisibleAmount(0.0); 1159 vsb.setBlockIncrement(0.0); 1160 vsb.setUnitIncrement(0.0); 1161 } 1162 1163 if (vsb.isVisible()) { 1164 updatePosY(); 1165 } else { 1166 if (nodeHeight > contentHeight) { 1167 updatePosY(); 1168 } else { 1169 viewContent.setLayoutY(0); 1170 } 1171 } 1172 } 1173 1174 private double updatePosX() { 1175 final ScrollPane sp = getSkinnable(); 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 }