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