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