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 }