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