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 }