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 */ 398 public final ScrollBar getHorizontalScrollBar() { 399 return hsb; 400 } 401 402 /** 403 * Returns the vertical {@link ScrollBar} used in this ScrollPaneSkin 404 * instance. 405 */ 406 public final ScrollBar getVerticalScrollBar() { 407 return vsb; 408 } 409 410 /** {@inheritDoc} */ 411 @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 412 final ScrollPane sp = getSkinnable(); 413 414 double vsbWidth = computeVsbSizeHint(sp); 415 double minWidth = vsbWidth + snappedLeftInset() + snappedRightInset(); 416 417 if (sp.getPrefViewportWidth() > 0) { 418 return (sp.getPrefViewportWidth() + minWidth); 419 } 420 else if (sp.getContent() != null) { 421 return (sp.getContent().prefWidth(height) + minWidth); 422 } 423 else { 424 return Math.max(minWidth, DEFAULT_PREF_SIZE); 425 } 426 } 427 428 /** {@inheritDoc} */ 429 @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 430 final ScrollPane sp = getSkinnable(); 431 432 double hsbHeight = computeHsbSizeHint(sp); 433 double minHeight = hsbHeight + snappedTopInset() + snappedBottomInset(); 434 435 if (sp.getPrefViewportHeight() > 0) { 436 return (sp.getPrefViewportHeight() + minHeight); 437 } 438 else if (sp.getContent() != null) { 439 return (sp.getContent().prefHeight(width) + minHeight); 440 } 441 else { 442 return Math.max(minHeight, DEFAULT_PREF_SIZE); 443 } 444 } 445 446 /** {@inheritDoc} */ 447 @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 448 final ScrollPane sp = getSkinnable(); 449 450 double vsbWidth = computeVsbSizeHint(sp); 451 double minWidth = vsbWidth + snappedLeftInset() + snappedRightInset(); 452 453 if (sp.getMinViewportWidth() > 0) { 454 return (sp.getMinViewportWidth() + minWidth); 455 } else { 456 double w = corner.minWidth(-1); 457 return (w > 0) ? (3 * w) : (DEFAULT_MIN_SIZE); 458 } 459 460 } 461 462 /** {@inheritDoc} */ 463 @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 464 final ScrollPane sp = getSkinnable(); 465 466 double hsbHeight = computeHsbSizeHint(sp); 467 double minHeight = hsbHeight + snappedTopInset() + snappedBottomInset(); 468 469 if (sp.getMinViewportHeight() > 0) { 470 return (sp.getMinViewportHeight() + minHeight); 471 } else { 472 double h = corner.minHeight(-1); 473 return (h > 0) ? (3 * h) : (DEFAULT_MIN_SIZE); 474 } 475 } 476 477 @Override protected void layoutChildren(final double x, final double y, 478 final double w, final double h) { 479 final ScrollPane control = getSkinnable(); 480 final Insets padding = control.getPadding(); 481 final double rightPadding = snapSizeX(padding.getRight()); 482 final double leftPadding = snapSizeX(padding.getLeft()); 483 final double topPadding = snapSizeY(padding.getTop()); 484 final double bottomPadding = snapSizeY(padding.getBottom()); 485 486 vsb.setMin(control.getVmin()); 487 vsb.setMax(control.getVmax()); 488 489 //should only do this on css setup 490 hsb.setMin(control.getHmin()); 491 hsb.setMax(control.getHmax()); 492 493 contentWidth = w; 494 contentHeight = h; 495 496 /* 497 ** we want the scrollbars to go right to the border 498 */ 499 double hsbWidth = 0; 500 double vsbHeight = 0; 501 502 computeScrollNodeSize(contentWidth, contentHeight); 503 computeScrollBarSize(); 504 505 for (int i = 0; i < 2; ++i) { 506 vsbvis = determineVerticalSBVisible(); 507 hsbvis = determineHorizontalSBVisible(); 508 509 if (vsbvis && !Properties.IS_TOUCH_SUPPORTED) { 510 contentWidth = w - vsbWidth; 511 } 512 hsbWidth = w + leftPadding + rightPadding - (vsbvis ? vsbWidth : 0); 513 if (hsbvis && !Properties.IS_TOUCH_SUPPORTED) { 514 contentHeight = h - hsbHeight; 515 } 516 vsbHeight = h + topPadding + bottomPadding - (hsbvis ? hsbHeight : 0); 517 } 518 519 520 if (scrollNode != null && scrollNode.isResizable()) { 521 // maybe adjust size now that scrollbars may take up space 522 if (vsbvis && hsbvis) { 523 // adjust just once to accommodate 524 computeScrollNodeSize(contentWidth, contentHeight); 525 526 } else if (hsbvis && !vsbvis) { 527 computeScrollNodeSize(contentWidth, contentHeight); 528 vsbvis = determineVerticalSBVisible(); 529 if (vsbvis) { 530 // now both are visible 531 contentWidth -= vsbWidth; 532 hsbWidth -= vsbWidth; 533 computeScrollNodeSize(contentWidth, contentHeight); 534 } 535 } else if (vsbvis && !hsbvis) { 536 computeScrollNodeSize(contentWidth, contentHeight); 537 hsbvis = determineHorizontalSBVisible(); 538 if (hsbvis) { 539 // now both are visible 540 contentHeight -= hsbHeight; 541 vsbHeight -= hsbHeight; 542 computeScrollNodeSize(contentWidth, contentHeight); 543 } 544 } 545 } 546 547 // figure out the content area that is to be filled 548 double cx = snappedLeftInset() - leftPadding; 549 double cy = snappedTopInset() - topPadding; 550 551 vsb.setVisible(vsbvis); 552 if (vsbvis) { 553 /* 554 ** round up position of ScrollBar, round down it's size. 555 ** 556 ** Positioning the ScrollBar 557 ** The Padding should go between the content and the edge, 558 ** otherwise changes in padding move the ScrollBar, and could 559 ** in extreme cases size the ScrollBar to become unusable. 560 ** The -1, +1 plus one bit : 561 ** If padding in => 1 then we allow one pixel to appear as the 562 ** outside border of the Scrollbar, and the rest on the inside. 563 ** If padding is < 1 then we just stick to the edge. 564 */ 565 vsb.resizeRelocate(snappedLeftInset() + w - vsbWidth + (rightPadding < 1 ? 0 : rightPadding - 1) , 566 cy, vsbWidth, vsbHeight); 567 } 568 updateVerticalSB(); 569 570 hsb.setVisible(hsbvis); 571 if (hsbvis) { 572 /* 573 ** round up position of ScrollBar, round down it's size. 574 ** 575 ** Positioning the ScrollBar 576 ** The Padding should go between the content and the edge, 577 ** otherwise changes in padding move the ScrollBar, and could 578 ** in extreme cases size the ScrollBar to become unusable. 579 ** The -1, +1 plus one bit : 580 ** If padding in => 1 then we allow one pixel to appear as the 581 ** outside border of the Scrollbar, and the rest on the inside. 582 ** If padding is < 1 then we just stick to the edge. 583 */ 584 hsb.resizeRelocate(cx, snappedTopInset() + h - hsbHeight + (bottomPadding < 1 ? 0 : bottomPadding - 1), 585 hsbWidth, hsbHeight); 586 } 587 updateHorizontalSB(); 588 589 viewRect.resizeRelocate(snappedLeftInset(), snappedTopInset(), snapSizeX(contentWidth), snapSizeY(contentHeight)); 590 resetClip(); 591 592 if (vsbvis && hsbvis) { 593 corner.setVisible(true); 594 double cornerWidth = vsbWidth; 595 double cornerHeight = hsbHeight; 596 corner.resizeRelocate(snapPositionX(vsb.getLayoutX()), snapPositionY(hsb.getLayoutY()), snapSizeX(cornerWidth), snapSizeY(cornerHeight)); 597 } else { 598 corner.setVisible(false); 599 } 600 control.setViewportBounds(new BoundingBox(snapPositionX(viewContent.getLayoutX()), snapPositionY(viewContent.getLayoutY()), snapSizeX(contentWidth), snapSizeY(contentHeight))); 601 } 602 603 /** {@inheritDoc} */ 604 @Override protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { 605 switch (attribute) { 606 case VERTICAL_SCROLLBAR: return vsb; 607 case HORIZONTAL_SCROLLBAR: return hsb; 608 default: return super.queryAccessibleAttribute(attribute, parameters); 609 } 610 } 611 612 613 614 /*************************************************************************** 615 * * 616 * Private implementation * 617 * * 618 **************************************************************************/ 619 620 private void initialize() { 621 // requestLayout calls below should not trigger requestLayout above ScrollPane 622 // setManaged(false); 623 624 ScrollPane control = getSkinnable(); 625 scrollNode = control.getContent(); 626 627 ParentTraversalEngine traversalEngine = new ParentTraversalEngine(getSkinnable()); 628 traversalEngine.addTraverseListener((node, bounds) -> { 629 // auto-scroll so node is within (0,0),(contentWidth,contentHeight) 630 scrollBoundsIntoView(bounds); 631 }); 632 ParentHelper.setTraversalEngine(getSkinnable(), traversalEngine); 633 634 if (scrollNode != null) { 635 scrollNode.layoutBoundsProperty().addListener(nodeListener); 636 scrollNode.layoutBoundsProperty().addListener(boundsChangeListener); 637 } 638 639 viewRect = new StackPane() { 640 @Override protected void layoutChildren() { 641 viewContent.resize(getWidth(), getHeight()); 642 } 643 }; 644 // prevent requestLayout requests from within scrollNode from percolating up 645 viewRect.setManaged(false); 646 viewRect.setCache(true); 647 viewRect.getStyleClass().add("viewport"); 648 649 clipRect = new Rectangle(); 650 viewRect.setClip(clipRect); 651 652 hsb = new ScrollBar(); 653 654 vsb = new ScrollBar(); 655 vsb.setOrientation(Orientation.VERTICAL); 656 657 EventHandler<MouseEvent> barHandler = ev -> { 658 if (getSkinnable().isFocusTraversable()) { 659 getSkinnable().requestFocus(); 660 } 661 }; 662 663 hsb.addEventFilter(MouseEvent.MOUSE_PRESSED, barHandler); 664 vsb.addEventFilter(MouseEvent.MOUSE_PRESSED, barHandler); 665 666 corner = new StackPane(); 667 corner.getStyleClass().setAll("corner"); 668 669 viewContent = new StackPane() { 670 @Override public void requestLayout() { 671 // if scrollNode requested layout, will want to recompute 672 nodeSizeInvalid = true; 673 674 super.requestLayout(); // add as layout root for next layout pass 675 676 // Need to layout the ScrollPane as well in case scrollbars 677 // appeared or disappeared. 678 ScrollPaneSkin.this.getSkinnable().requestLayout(); 679 } 680 @Override protected void layoutChildren() { 681 if (nodeSizeInvalid) { 682 computeScrollNodeSize(getWidth(),getHeight()); 683 } 684 if (scrollNode != null && scrollNode.isResizable()) { 685 scrollNode.resize(snapSize(nodeWidth), snapSize(nodeHeight)); 686 if (vsbvis != determineVerticalSBVisible() || hsbvis != determineHorizontalSBVisible()) { 687 getSkinnable().requestLayout(); 688 } 689 } 690 if (scrollNode != null) { 691 scrollNode.relocate(0,0); 692 } 693 } 694 }; 695 viewRect.getChildren().add(viewContent); 696 697 if (scrollNode != null) { 698 viewContent.getChildren().add(scrollNode); 699 viewRect.nodeOrientationProperty().bind(scrollNode.nodeOrientationProperty()); 700 } 701 702 getChildren().clear(); 703 getChildren().addAll(viewRect, vsb, hsb, corner); 704 705 /* 706 ** listeners, and assorted housekeeping 707 */ 708 InvalidationListener vsbListener = valueModel -> { 709 if (!Properties.IS_TOUCH_SUPPORTED) { 710 posY = Utils.clamp(getSkinnable().getVmin(), vsb.getValue(), getSkinnable().getVmax()); 711 } 712 else { 713 posY = vsb.getValue(); 714 } 715 updatePosY(); 716 }; 717 vsb.valueProperty().addListener(vsbListener); 718 719 InvalidationListener hsbListener = valueModel -> { 720 if (!Properties.IS_TOUCH_SUPPORTED) { 721 posX = Utils.clamp(getSkinnable().getHmin(), hsb.getValue(), getSkinnable().getHmax()); 722 } 723 else { 724 posX = hsb.getValue(); 725 } 726 updatePosX(); 727 }; 728 hsb.valueProperty().addListener(hsbListener); 729 730 viewRect.setOnMousePressed(e -> { 731 mouseDown = true; 732 if (Properties.IS_TOUCH_SUPPORTED) { 733 startSBReleasedAnimation(); 734 } 735 pressX = e.getX(); 736 pressY = e.getY(); 737 ohvalue = hsb.getValue(); 738 ovvalue = vsb.getValue(); 739 }); 740 741 742 viewRect.setOnDragDetected(e -> { 743 if (Properties.IS_TOUCH_SUPPORTED) { 744 startSBReleasedAnimation(); 745 } 746 if (getSkinnable().isPannable()) { 747 dragDetected = true; 748 if (saveCursor == null) { 749 saveCursor = getSkinnable().getCursor(); 750 if (saveCursor == null) { 751 saveCursor = Cursor.DEFAULT; 752 } 753 getSkinnable().setCursor(Cursor.MOVE); 754 getSkinnable().requestLayout(); 755 } 756 } 757 }); 758 759 viewRect.addEventFilter(MouseEvent.MOUSE_RELEASED, e -> { 760 mouseDown = false; 761 if (dragDetected == true) { 762 if (saveCursor != null) { 763 getSkinnable().setCursor(saveCursor); 764 saveCursor = null; 765 getSkinnable().requestLayout(); 766 } 767 dragDetected = false; 768 } 769 770 /* 771 ** if the contents need repositioning, and there's is no 772 ** touch event in progress, then start the repositioning. 773 */ 774 if ((posY > getSkinnable().getVmax() || posY < getSkinnable().getVmin() || 775 posX > getSkinnable().getHmax() || posX < getSkinnable().getHmin()) && !touchDetected) { 776 startContentsToViewport(); 777 } 778 }); 779 viewRect.setOnMouseDragged(e -> { 780 if (Properties.IS_TOUCH_SUPPORTED) { 781 startSBReleasedAnimation(); 782 } 783 /* 784 ** for mobile-touch we allow drag, even if not pannagle 785 */ 786 if (getSkinnable().isPannable() || Properties.IS_TOUCH_SUPPORTED) { 787 double deltaX = pressX - e.getX(); 788 double deltaY = pressY - e.getY(); 789 /* 790 ** we only drag if not all of the content is visible. 791 */ 792 if (hsb.getVisibleAmount() > 0.0 && hsb.getVisibleAmount() < hsb.getMax()) { 793 if (Math.abs(deltaX) > PAN_THRESHOLD) { 794 if (isReverseNodeOrientation()) { 795 deltaX = -deltaX; 796 } 797 double newHVal = (ohvalue + deltaX / (nodeWidth - viewRect.getWidth()) * (hsb.getMax() - hsb.getMin())); 798 if (!Properties.IS_TOUCH_SUPPORTED) { 799 if (newHVal > hsb.getMax()) { 800 newHVal = hsb.getMax(); 801 } 802 else if (newHVal < hsb.getMin()) { 803 newHVal = hsb.getMin(); 804 } 805 hsb.setValue(newHVal); 806 } 807 else { 808 hsb.setValue(newHVal); 809 } 810 } 811 } 812 /* 813 ** we only drag if not all of the content is visible. 814 */ 815 if (vsb.getVisibleAmount() > 0.0 && vsb.getVisibleAmount() < vsb.getMax()) { 816 if (Math.abs(deltaY) > PAN_THRESHOLD) { 817 double newVVal = (ovvalue + deltaY / (nodeHeight - viewRect.getHeight()) * (vsb.getMax() - vsb.getMin())); 818 if (!Properties.IS_TOUCH_SUPPORTED) { 819 if (newVVal > vsb.getMax()) { 820 newVVal = vsb.getMax(); 821 } 822 else if (newVVal < vsb.getMin()) { 823 newVVal = vsb.getMin(); 824 } 825 vsb.setValue(newVVal); 826 } 827 else { 828 vsb.setValue(newVVal); 829 } 830 } 831 } 832 } 833 /* 834 ** we need to consume drag events, as we don't want 835 ** the scrollpane itself to be dragged on every mouse click 836 */ 837 e.consume(); 838 }); 839 840 841 /* 842 ** don't allow the ScrollBar to handle the ScrollEvent, 843 ** In a ScrollPane a vertical scroll should scroll on the vertical only, 844 ** whereas in a horizontal ScrollBar it can scroll horizontally. 845 */ 846 // block the event from being passed down to children 847 final EventDispatcher blockEventDispatcher = (event, tail) -> event; 848 // block ScrollEvent from being passed down to scrollbar's skin 849 final EventDispatcher oldHsbEventDispatcher = hsb.getEventDispatcher(); 850 hsb.setEventDispatcher((event, tail) -> { 851 if (event.getEventType() == ScrollEvent.SCROLL && 852 !((ScrollEvent)event).isDirect()) { 853 tail = tail.prepend(blockEventDispatcher); 854 tail = tail.prepend(oldHsbEventDispatcher); 855 return tail.dispatchEvent(event); 856 } 857 return oldHsbEventDispatcher.dispatchEvent(event, tail); 858 }); 859 // block ScrollEvent from being passed down to scrollbar's skin 860 final EventDispatcher oldVsbEventDispatcher = vsb.getEventDispatcher(); 861 vsb.setEventDispatcher((event, tail) -> { 862 if (event.getEventType() == ScrollEvent.SCROLL && 863 !((ScrollEvent)event).isDirect()) { 864 tail = tail.prepend(blockEventDispatcher); 865 tail = tail.prepend(oldVsbEventDispatcher); 866 return tail.dispatchEvent(event); 867 } 868 return oldVsbEventDispatcher.dispatchEvent(event, tail); 869 }); 870 871 /* 872 * listen for ScrollEvents over the whole of the ScrollPane 873 * area, the above dispatcher having removed the ScrollBars 874 * scroll event handling. 875 * 876 * Note that we use viewRect here, rather than setting the eventHandler 877 * on the ScrollPane itself. This is for RT-31582, and effectively 878 * allows for us to prioritise handling (and consuming) the event 879 * internally, before it is made available to users listening to events 880 * on the control. This is consistent with the VirtualFlow-based controls. 881 */ 882 viewRect.addEventHandler(ScrollEvent.SCROLL, event -> { 883 if (Properties.IS_TOUCH_SUPPORTED) { 884 startSBReleasedAnimation(); 885 } 886 /* 887 ** if we're completely visible then do nothing.... 888 ** we only consume an event that we've used. 889 */ 890 if (vsb.getVisibleAmount() < vsb.getMax()) { 891 double vRange = getSkinnable().getVmax()-getSkinnable().getVmin(); 892 double vPixelValue; 893 if (nodeHeight > 0.0) { 894 vPixelValue = vRange / nodeHeight; 895 } 896 else { 897 vPixelValue = 0.0; 898 } 899 double newValue = vsb.getValue()+(-event.getDeltaY())*vPixelValue; 900 if (!Properties.IS_TOUCH_SUPPORTED) { 901 if ((event.getDeltaY() > 0.0 && vsb.getValue() > vsb.getMin()) || 902 (event.getDeltaY() < 0.0 && vsb.getValue() < vsb.getMax())) { 903 vsb.setValue(newValue); 904 event.consume(); 905 } 906 } 907 else { 908 /* 909 ** if there is a repositioning in progress then we only 910 ** set the value for 'real' events 911 */ 912 if (!(((ScrollEvent)event).isInertia()) || (((ScrollEvent)event).isInertia()) && (contentsToViewTimeline == null || contentsToViewTimeline.getStatus() == Status.STOPPED)) { 913 vsb.setValue(newValue); 914 if ((newValue > vsb.getMax() || newValue < vsb.getMin()) && (!mouseDown && !touchDetected)) { 915 startContentsToViewport(); 916 } 917 event.consume(); 918 } 919 } 920 } 921 922 if (hsb.getVisibleAmount() < hsb.getMax()) { 923 double hRange = getSkinnable().getHmax()-getSkinnable().getHmin(); 924 double hPixelValue; 925 if (nodeWidth > 0.0) { 926 hPixelValue = hRange / nodeWidth; 927 } 928 else { 929 hPixelValue = 0.0; 930 } 931 932 double newValue = hsb.getValue()+(-event.getDeltaX())*hPixelValue; 933 if (!Properties.IS_TOUCH_SUPPORTED) { 934 if ((event.getDeltaX() > 0.0 && hsb.getValue() > hsb.getMin()) || 935 (event.getDeltaX() < 0.0 && hsb.getValue() < hsb.getMax())) { 936 hsb.setValue(newValue); 937 event.consume(); 938 } 939 } 940 else { 941 /* 942 ** if there is a repositioning in progress then we only 943 ** set the value for 'real' events 944 */ 945 if (!(((ScrollEvent)event).isInertia()) || (((ScrollEvent)event).isInertia()) && (contentsToViewTimeline == null || contentsToViewTimeline.getStatus() == Status.STOPPED)) { 946 hsb.setValue(newValue); 947 948 if ((newValue > hsb.getMax() || newValue < hsb.getMin()) && (!mouseDown && !touchDetected)) { 949 startContentsToViewport(); 950 } 951 event.consume(); 952 } 953 } 954 } 955 }); 956 957 /* 958 ** there are certain animations that need to know if the touch is 959 ** happening..... 960 */ 961 getSkinnable().addEventHandler(TouchEvent.TOUCH_PRESSED, e -> { 962 touchDetected = true; 963 startSBReleasedAnimation(); 964 e.consume(); 965 }); 966 967 getSkinnable().addEventHandler(TouchEvent.TOUCH_RELEASED, e -> { 968 touchDetected = false; 969 e.consume(); 970 }); 971 972 // ScrollPanes do not block all MouseEvents by default, unlike most other UI Controls. 973 consumeMouseEvents(false); 974 975 // update skin initial state to match control (see RT-35554) 976 hsb.setValue(control.getHvalue()); 977 vsb.setValue(control.getVvalue()); 978 } 979 980 void scrollBoundsIntoView(Bounds b) { 981 double dx = 0.0; 982 double dy = 0.0; 983 if (b.getMaxX() > contentWidth) { 984 dx = b.getMinX() - snappedLeftInset(); 985 } 986 if (b.getMinX() < snappedLeftInset()) { 987 dx = b.getMaxX() - contentWidth - snappedLeftInset(); 988 } 989 if (b.getMaxY() > snappedTopInset() + contentHeight) { 990 dy = b.getMinY() - snappedTopInset(); 991 } 992 if (b.getMinY() < snappedTopInset()) { 993 dy = b.getMaxY() - contentHeight - snappedTopInset(); 994 } 995 // We want to move contentPanel's layoutX,Y by (dx,dy). 996 // But to do this we have to set the scrollbars' values appropriately. 997 998 if (dx != 0) { 999 double sdx = dx * (hsb.getMax() - hsb.getMin()) / (nodeWidth - contentWidth); 1000 // Adjust back for some amount so that the Node border is not too close to view border 1001 sdx += -1 * Math.signum(sdx) * hsb.getUnitIncrement() / 5; // This accounts to 2% of view width 1002 hsb.setValue(hsb.getValue() + sdx); 1003 getSkinnable().requestLayout(); 1004 } 1005 if (dy != 0) { 1006 double sdy = dy * (vsb.getMax() - vsb.getMin()) / (nodeHeight - contentHeight); 1007 // Adjust back for some amount so that the Node border is not too close to view border 1008 sdy += -1 * Math.signum(sdy) * vsb.getUnitIncrement() / 5; // This accounts to 2% of view height 1009 vsb.setValue(vsb.getValue() + sdy); 1010 getSkinnable().requestLayout(); 1011 } 1012 1013 } 1014 1015 /** 1016 * Computes the size that should be reserved for horizontal scrollbar in size hints (min/pref height) 1017 */ 1018 private double computeHsbSizeHint(ScrollPane sp) { 1019 return ((sp.getHbarPolicy() == ScrollBarPolicy.ALWAYS) || 1020 (sp.getHbarPolicy() == ScrollBarPolicy.AS_NEEDED && (sp.getPrefViewportHeight() > 0 || sp.getMinViewportHeight() > 0))) 1021 ? hsb.prefHeight(ScrollBar.USE_COMPUTED_SIZE) 1022 : 0; 1023 } 1024 1025 /** 1026 * Computes the size that should be reserved for vertical scrollbar in size hints (min/pref width) 1027 */ 1028 private double computeVsbSizeHint(ScrollPane sp) { 1029 return ((sp.getVbarPolicy() == ScrollBarPolicy.ALWAYS) || 1030 (sp.getVbarPolicy() == ScrollBarPolicy.AS_NEEDED && (sp.getPrefViewportWidth() > 0 1031 || sp.getMinViewportWidth() > 0))) 1032 ? vsb.prefWidth(ScrollBar.USE_COMPUTED_SIZE) 1033 : 0; 1034 } 1035 1036 private void computeScrollNodeSize(double contentWidth, double contentHeight) { 1037 if (scrollNode != null) { 1038 if (scrollNode.isResizable()) { 1039 ScrollPane control = getSkinnable(); 1040 Orientation bias = scrollNode.getContentBias(); 1041 if (bias == null) { 1042 nodeWidth = snapSizeX(boundedSize(control.isFitToWidth()? contentWidth : scrollNode.prefWidth(-1), 1043 scrollNode.minWidth(-1),scrollNode.maxWidth(-1))); 1044 nodeHeight = snapSizeY(boundedSize(control.isFitToHeight()? contentHeight : scrollNode.prefHeight(-1), 1045 scrollNode.minHeight(-1), scrollNode.maxHeight(-1))); 1046 1047 } else if (bias == Orientation.HORIZONTAL) { 1048 nodeWidth = snapSizeX(boundedSize(control.isFitToWidth()? contentWidth : scrollNode.prefWidth(-1), 1049 scrollNode.minWidth(-1),scrollNode.maxWidth(-1))); 1050 nodeHeight = snapSizeY(boundedSize(control.isFitToHeight()? contentHeight : scrollNode.prefHeight(nodeWidth), 1051 scrollNode.minHeight(nodeWidth),scrollNode.maxHeight(nodeWidth))); 1052 1053 } else { // bias == VERTICAL 1054 nodeHeight = snapSizeY(boundedSize(control.isFitToHeight()? contentHeight : scrollNode.prefHeight(-1), 1055 scrollNode.minHeight(-1), scrollNode.maxHeight(-1))); 1056 nodeWidth = snapSizeX(boundedSize(control.isFitToWidth()? contentWidth : scrollNode.prefWidth(nodeHeight), 1057 scrollNode.minWidth(nodeHeight),scrollNode.maxWidth(nodeHeight))); 1058 } 1059 1060 } else { 1061 nodeWidth = snapSizeX(scrollNode.getLayoutBounds().getWidth()); 1062 nodeHeight = snapSizeY(scrollNode.getLayoutBounds().getHeight()); 1063 } 1064 nodeSizeInvalid = false; 1065 } 1066 } 1067 1068 private boolean isReverseNodeOrientation() { 1069 return (scrollNode != null && 1070 getSkinnable().getEffectiveNodeOrientation() != 1071 scrollNode.getEffectiveNodeOrientation()); 1072 } 1073 1074 private boolean determineHorizontalSBVisible() { 1075 final ScrollPane sp = getSkinnable(); 1076 1077 if (Properties.IS_TOUCH_SUPPORTED) { 1078 return (tempVisibility && (nodeWidth > contentWidth)); 1079 } 1080 else { 1081 // RT-17395: ScrollBarPolicy might be null. If so, treat it as "AS_NEEDED", which is the default 1082 ScrollBarPolicy hbarPolicy = sp.getHbarPolicy(); 1083 return (ScrollBarPolicy.NEVER == hbarPolicy) ? false : 1084 ((ScrollBarPolicy.ALWAYS == hbarPolicy) ? true : 1085 ((sp.isFitToWidth() && scrollNode != null ? scrollNode.isResizable() : false) ? 1086 (nodeWidth > contentWidth && scrollNode.minWidth(-1) > contentWidth) : (nodeWidth > contentWidth))); 1087 } 1088 } 1089 1090 private boolean determineVerticalSBVisible() { 1091 final ScrollPane sp = getSkinnable(); 1092 1093 if (Properties.IS_TOUCH_SUPPORTED) { 1094 return (tempVisibility && (nodeHeight > contentHeight)); 1095 } 1096 else { 1097 // RT-17395: ScrollBarPolicy might be null. If so, treat it as "AS_NEEDED", which is the default 1098 ScrollBarPolicy vbarPolicy = sp.getVbarPolicy(); 1099 return (ScrollBarPolicy.NEVER == vbarPolicy) ? false : 1100 ((ScrollBarPolicy.ALWAYS == vbarPolicy) ? true : 1101 ((sp.isFitToHeight() && scrollNode != null ? scrollNode.isResizable() : false) ? 1102 (nodeHeight > contentHeight && scrollNode.minHeight(-1) > contentHeight) : (nodeHeight > contentHeight))); 1103 } 1104 } 1105 1106 private void computeScrollBarSize() { 1107 vsbWidth = snapSizeX(vsb.prefWidth(-1)); 1108 if (vsbWidth == 0) { 1109 // println("*** WARNING ScrollPaneSkin: can't get scroll bar width, using {DEFAULT_SB_BREADTH}"); 1110 if (Properties.IS_TOUCH_SUPPORTED) { 1111 vsbWidth = DEFAULT_EMBEDDED_SB_BREADTH; 1112 } 1113 else { 1114 vsbWidth = DEFAULT_SB_BREADTH; 1115 } 1116 } 1117 hsbHeight = snapSizeY(hsb.prefHeight(-1)); 1118 if (hsbHeight == 0) { 1119 // println("*** WARNING ScrollPaneSkin: can't get scroll bar height, using {DEFAULT_SB_BREADTH}"); 1120 if (Properties.IS_TOUCH_SUPPORTED) { 1121 hsbHeight = DEFAULT_EMBEDDED_SB_BREADTH; 1122 } 1123 else { 1124 hsbHeight = DEFAULT_SB_BREADTH; 1125 } 1126 } 1127 } 1128 1129 private void updateHorizontalSB() { 1130 double contentRatio = nodeWidth * (hsb.getMax() - hsb.getMin()); 1131 if (contentRatio > 0.0) { 1132 hsb.setVisibleAmount(contentWidth / contentRatio); 1133 hsb.setBlockIncrement(0.9 * hsb.getVisibleAmount()); 1134 hsb.setUnitIncrement(0.1 * hsb.getVisibleAmount()); 1135 } 1136 else { 1137 hsb.setVisibleAmount(0.0); 1138 hsb.setBlockIncrement(0.0); 1139 hsb.setUnitIncrement(0.0); 1140 } 1141 1142 if (hsb.isVisible()) { 1143 updatePosX(); 1144 } else { 1145 if (nodeWidth > contentWidth) { 1146 updatePosX(); 1147 } else { 1148 viewContent.setLayoutX(0); 1149 } 1150 } 1151 } 1152 1153 private void updateVerticalSB() { 1154 double contentRatio = nodeHeight * (vsb.getMax() - vsb.getMin()); 1155 if (contentRatio > 0.0) { 1156 vsb.setVisibleAmount(contentHeight / contentRatio); 1157 vsb.setBlockIncrement(0.9 * vsb.getVisibleAmount()); 1158 vsb.setUnitIncrement(0.1 * vsb.getVisibleAmount()); 1159 } 1160 else { 1161 vsb.setVisibleAmount(0.0); 1162 vsb.setBlockIncrement(0.0); 1163 vsb.setUnitIncrement(0.0); 1164 } 1165 1166 if (vsb.isVisible()) { 1167 updatePosY(); 1168 } else { 1169 if (nodeHeight > contentHeight) { 1170 updatePosY(); 1171 } else { 1172 viewContent.setLayoutY(0); 1173 } 1174 } 1175 } 1176 1177 private double updatePosX() { 1178 final ScrollPane sp = getSkinnable(); 1179 double x = isReverseNodeOrientation() ? (hsb.getMax() - (posX - hsb.getMin())) : posX; 1180 double minX = Math.min((- x / (hsb.getMax() - hsb.getMin()) * (nodeWidth - contentWidth)), 0); 1181 viewContent.setLayoutX(snapPositionX(minX)); 1182 if (!sp.hvalueProperty().isBound()) sp.setHvalue(Utils.clamp(sp.getHmin(), posX, sp.getHmax())); 1183 return posX; 1184 } 1185 1186 private double updatePosY() { 1187 final ScrollPane sp = getSkinnable(); 1188 double minY = Math.min((- posY / (vsb.getMax() - vsb.getMin()) * (nodeHeight - contentHeight)), 0); 1189 viewContent.setLayoutY(snapPositionY(minY)); 1190 if (!sp.vvalueProperty().isBound()) sp.setVvalue(Utils.clamp(sp.getVmin(), posY, sp.getVmax())); 1191 return posY; 1192 } 1193 1194 private void resetClip() { 1195 clipRect.setWidth(snapSizeX(contentWidth)); 1196 clipRect.setHeight(snapSizeY(contentHeight)); 1197 } 1198 1199 private void startSBReleasedAnimation() { 1200 if (sbTouchTimeline == null) { 1201 /* 1202 ** timeline to leave the scrollbars visible for a short 1203 ** while after a scroll/drag 1204 */ 1205 sbTouchTimeline = new Timeline(); 1206 sbTouchKF1 = new KeyFrame(Duration.millis(0), event -> { 1207 tempVisibility = true; 1208 if (touchDetected == true || mouseDown == true) { 1209 sbTouchTimeline.playFromStart(); 1210 } 1211 }); 1212 1213 sbTouchKF2 = new KeyFrame(Duration.millis(1000), event -> { 1214 tempVisibility = false; 1215 getSkinnable().requestLayout(); 1216 }); 1217 sbTouchTimeline.getKeyFrames().addAll(sbTouchKF1, sbTouchKF2); 1218 } 1219 sbTouchTimeline.playFromStart(); 1220 } 1221 1222 private void startContentsToViewport() { 1223 double newPosX = posX; 1224 double newPosY = posY; 1225 1226 setContentPosX(posX); 1227 setContentPosY(posY); 1228 1229 if (posY > getSkinnable().getVmax()) { 1230 newPosY = getSkinnable().getVmax(); 1231 } 1232 else if (posY < getSkinnable().getVmin()) { 1233 newPosY = getSkinnable().getVmin(); 1234 } 1235 1236 1237 if (posX > getSkinnable().getHmax()) { 1238 newPosX = getSkinnable().getHmax(); 1239 } 1240 else if (posX < getSkinnable().getHmin()) { 1241 newPosX = getSkinnable().getHmin(); 1242 } 1243 1244 if (!Properties.IS_TOUCH_SUPPORTED) { 1245 startSBReleasedAnimation(); 1246 } 1247 1248 /* 1249 ** timeline to return the contents of the scrollpane to the viewport 1250 */ 1251 if (contentsToViewTimeline != null) { 1252 contentsToViewTimeline.stop(); 1253 } 1254 contentsToViewTimeline = new Timeline(); 1255 /* 1256 ** short pause before animation starts 1257 */ 1258 contentsToViewKF1 = new KeyFrame(Duration.millis(50)); 1259 /* 1260 ** reposition 1261 */ 1262 contentsToViewKF2 = new KeyFrame(Duration.millis(150), event -> { 1263 getSkinnable().requestLayout(); 1264 }, 1265 new KeyValue(contentPosX, newPosX), 1266 new KeyValue(contentPosY, newPosY) 1267 ); 1268 /* 1269 ** block out 'aftershocks', but real events will 1270 ** still reactivate 1271 */ 1272 contentsToViewKF3 = new KeyFrame(Duration.millis(1500)); 1273 contentsToViewTimeline.getKeyFrames().addAll(contentsToViewKF1, contentsToViewKF2, contentsToViewKF3); 1274 contentsToViewTimeline.playFromStart(); 1275 } 1276 }