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