1 /*
   2  * Copyright (c) 2010, 2016, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javafx.scene.control.skin;
  27 
  28 import com.sun.javafx.scene.control.Properties;
  29 import com.sun.javafx.scene.control.behavior.BehaviorBase;
  30 import javafx.beans.value.ObservableValue;
  31 import javafx.geometry.Orientation;
  32 import javafx.geometry.Point2D;
  33 import javafx.scene.AccessibleAction;
  34 import javafx.scene.AccessibleAttribute;
  35 import javafx.scene.AccessibleRole;
  36 import javafx.scene.control.Accordion;
  37 import javafx.scene.control.Button;
  38 import javafx.scene.control.Control;
  39 import javafx.scene.control.ScrollBar;
  40 import javafx.scene.control.SkinBase;
  41 import javafx.scene.input.MouseButton;
  42 import javafx.scene.input.ScrollEvent;
  43 import javafx.scene.layout.Region;
  44 import javafx.scene.layout.StackPane;
  45 import javafx.scene.Node;
  46 import com.sun.javafx.util.Utils;
  47 import com.sun.javafx.scene.control.behavior.ScrollBarBehavior;
  48 
  49 import java.util.function.Consumer;
  50 
  51 /**
  52  * Default skin implementation for the {@link ScrollBar} control.
  53  *
  54  * @see ScrollBar
  55  * @since 9
  56  */
  57 public class ScrollBarSkin extends SkinBase<ScrollBar> {
  58 
  59     /***************************************************************************
  60      *                                                                         *
  61      * Private fields                                                          *
  62      *                                                                         *
  63      **************************************************************************/
  64 
  65     private final ScrollBarBehavior behavior;
  66 
  67     private StackPane thumb;
  68     private StackPane trackBackground;
  69     private StackPane track;
  70     private EndButton incButton;
  71     private EndButton decButton;
  72 
  73     private double trackLength;
  74     private double thumbLength;
  75 
  76     private double preDragThumbPos;
  77     private Point2D dragStart; // in the track's coord system
  78 
  79     private double trackPos;
  80 
  81 
  82 
  83     /***************************************************************************
  84      *                                                                         *
  85      * Constructors                                                            *
  86      *                                                                         *
  87      **************************************************************************/
  88 
  89     /**
  90      * Creates a new ScrollBarSkin instance, installing the necessary child
  91      * nodes into the Control {@link Control#getChildren() children} list, as
  92      * well as the necessary input mappings for handling key, mouse, etc events.
  93      *
  94      * @param control The control that this skin should be installed onto.
  95      */
  96     public ScrollBarSkin(ScrollBar control) {
  97         super(control);
  98 
  99         // install default input map for the ScrollBar control
 100         this.behavior = new ScrollBarBehavior(control);
 101 //        control.setInputMap(behavior.getInputMap());
 102 
 103         initialize();
 104         getSkinnable().requestLayout();
 105 
 106         // Register listeners
 107         final Consumer<ObservableValue<?>> consumer = e -> {
 108             positionThumb();
 109             getSkinnable().requestLayout();
 110         };
 111         registerChangeListener(control.minProperty(), consumer);
 112         registerChangeListener(control.maxProperty(), consumer);
 113         registerChangeListener(control.visibleAmountProperty(), consumer);
 114         registerChangeListener(control.valueProperty(), e -> positionThumb());
 115         registerChangeListener(control.orientationProperty(), e -> getSkinnable().requestLayout());
 116     }
 117 
 118 
 119 
 120     /***************************************************************************
 121      *                                                                         *
 122      * Public API                                                              *
 123      *                                                                         *
 124      **************************************************************************/
 125 
 126     /** {@inheritDoc} */
 127     @Override public void dispose() {
 128         super.dispose();
 129 
 130         if (behavior != null) {
 131             behavior.dispose();
 132         }
 133     }
 134 
 135     /** {@inheritDoc} */
 136     @Override protected void layoutChildren(final double x, final double y,
 137                                             final double w, final double h) {
 138 
 139         final ScrollBar s = getSkinnable();
 140 
 141         /**
 142          * Compute the percentage length of thumb as (visibleAmount/range)
 143          * if max isn't greater than min then there is nothing to do here
 144          */
 145         double visiblePortion;
 146         if (s.getMax() > s.getMin()) {
 147             visiblePortion = s.getVisibleAmount()/(s.getMax() - s.getMin());
 148         }
 149         else {
 150             visiblePortion = 1.0;
 151         }
 152 
 153         if (s.getOrientation() == Orientation.VERTICAL) {
 154             if (!Properties.IS_TOUCH_SUPPORTED) {
 155                 double decHeight = snapSize(decButton.prefHeight(-1));
 156                 double incHeight = snapSize(incButton.prefHeight(-1));
 157 
 158                 decButton.resize(w, decHeight);
 159                 incButton.resize(w, incHeight);
 160 
 161                 trackLength = snapSize(h - (decHeight + incHeight));
 162                 thumbLength = snapSize(Utils.clamp(minThumbLength(), (trackLength * visiblePortion), trackLength));
 163 
 164                 trackBackground.resizeRelocate(snapPosition(x), snapPosition(y), w, trackLength+decHeight+incHeight);
 165                 decButton.relocate(snapPosition(x), snapPosition(y));
 166                 incButton.relocate(snapPosition(x), snapPosition(y + h - incHeight));
 167                 track.resizeRelocate(snapPosition(x), snapPosition(y + decHeight), w, trackLength);
 168                 thumb.resize(snapSize(x >= 0 ? w : w + x), thumbLength); // Account for negative padding (see also RT-10719)
 169                 positionThumb();
 170             }
 171             else {
 172                 trackLength = snapSize(h);
 173                 thumbLength = snapSize(Utils.clamp(minThumbLength(), (trackLength * visiblePortion), trackLength));
 174 
 175                 track.resizeRelocate(snapPosition(x), snapPosition(y), w, trackLength);
 176                 thumb.resize(snapSize(x >= 0 ? w : w + x), thumbLength); // Account for negative padding (see also RT-10719)
 177                 positionThumb();
 178             }
 179         } else {
 180             if (!Properties.IS_TOUCH_SUPPORTED) {
 181                 double decWidth = snapSize(decButton.prefWidth(-1));
 182                 double incWidth = snapSize(incButton.prefWidth(-1));
 183 
 184                 decButton.resize(decWidth, h);
 185                 incButton.resize(incWidth, h);
 186 
 187                 trackLength = snapSize(w - (decWidth + incWidth));
 188                 thumbLength = snapSize(Utils.clamp(minThumbLength(), (trackLength * visiblePortion), trackLength));
 189 
 190                 trackBackground.resizeRelocate(snapPosition(x), snapPosition(y), trackLength+decWidth+incWidth, h);
 191                 decButton.relocate(snapPosition(x), snapPosition(y));
 192                 incButton.relocate(snapPosition(x + w - incWidth), snapPosition(y));
 193                 track.resizeRelocate(snapPosition(x + decWidth), snapPosition(y), trackLength, h);
 194                 thumb.resize(thumbLength, snapSize(y >= 0 ? h : h + y)); // Account for negative padding (see also RT-10719)
 195                 positionThumb();
 196             }
 197             else {
 198                 trackLength = snapSize(w);
 199                 thumbLength = snapSize(Utils.clamp(minThumbLength(), (trackLength * visiblePortion), trackLength));
 200 
 201                 track.resizeRelocate(snapPosition(x), snapPosition(y), trackLength, h);
 202                 thumb.resize(thumbLength, snapSize(y >= 0 ? h : h + y)); // Account for negative padding (see also RT-10719)
 203                 positionThumb();
 204             }
 205 
 206             s.resize(snapSize(s.getWidth()), snapSize(s.getHeight()));
 207         }
 208 
 209         // things should be invisible only when well below minimum length
 210         if (s.getOrientation() == Orientation.VERTICAL && h >= (computeMinHeight(-1, (int)y , snappedRightInset(), snappedBottomInset(), (int)x) - (y+snappedBottomInset())) ||
 211                 s.getOrientation() == Orientation.HORIZONTAL && w >= (computeMinWidth(-1, (int)y , snappedRightInset(), snappedBottomInset(), (int)x) - (x+snappedRightInset()))) {
 212             trackBackground.setVisible(true);
 213             track.setVisible(true);
 214             thumb.setVisible(true);
 215             if (!Properties.IS_TOUCH_SUPPORTED) {
 216                 incButton.setVisible(true);
 217                 decButton.setVisible(true);
 218             }
 219         }
 220         else {
 221             trackBackground.setVisible(false);
 222             track.setVisible(false);
 223             thumb.setVisible(false);
 224 
 225             if (!Properties.IS_TOUCH_SUPPORTED) {
 226                 /*
 227                 ** once the space is big enough for one button we
 228                 ** can look at drawing
 229                 */
 230                 if (h >= decButton.computeMinWidth(-1)) {
 231                     decButton.setVisible(true);
 232                 }
 233                 else {
 234                     decButton.setVisible(false);
 235                 }
 236                 if (h >= incButton.computeMinWidth(-1)) {
 237                     incButton.setVisible(true);
 238                 }
 239                 else {
 240                     incButton.setVisible(false);
 241                 }
 242             }
 243         }
 244     }
 245 
 246     /*
 247      * Minimum length is the length of the end buttons plus twice the
 248      * minimum thumb length, which should be enough for a reasonably-sized
 249      * track. Minimum breadth is determined by the breadths of the
 250      * end buttons.
 251      */
 252     /** {@inheritDoc} */
 253     @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 254         if (getSkinnable().getOrientation() == Orientation.VERTICAL) {
 255             return getBreadth();
 256         } else {
 257             if (!Properties.IS_TOUCH_SUPPORTED) {
 258                 return decButton.minWidth(-1) + incButton.minWidth(-1) + minTrackLength()+leftInset+rightInset;
 259             } else {
 260                 return minTrackLength()+leftInset+rightInset;
 261             }
 262         }
 263     }
 264 
 265     /** {@inheritDoc} */
 266     @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 267         if (getSkinnable().getOrientation() == Orientation.VERTICAL) {
 268             if (!Properties.IS_TOUCH_SUPPORTED) {
 269                 return decButton.minHeight(-1) + incButton.minHeight(-1) + minTrackLength()+topInset+bottomInset;
 270             } else {
 271                 return minTrackLength()+topInset+bottomInset;
 272             }
 273         } else {
 274             return getBreadth();
 275         }
 276     }
 277 
 278     /*
 279      * Preferred size. The breadth is determined by the breadth of
 280      * the end buttons. The length is a constant default length.
 281      * Usually applications or other components will either set a
 282      * specific length using LayoutInfo or will stretch the length
 283      * of the scrollbar to fit a container.
 284      */
 285     /** {@inheritDoc} */
 286     @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 287         final ScrollBar s = getSkinnable();
 288         return s.getOrientation() == Orientation.VERTICAL ? getBreadth() : Properties.DEFAULT_LENGTH+leftInset+rightInset;
 289     }
 290 
 291     /** {@inheritDoc} */
 292     @Override protected double computePrefHeight(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 293         final ScrollBar s = getSkinnable();
 294         return s.getOrientation() == Orientation.VERTICAL ? Properties.DEFAULT_LENGTH+topInset+bottomInset : getBreadth();
 295     }
 296 
 297     /** {@inheritDoc} */
 298     @Override protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 299         final ScrollBar s = getSkinnable();
 300         return s.getOrientation() == Orientation.VERTICAL ? s.prefWidth(-1) : Double.MAX_VALUE;
 301     }
 302 
 303     /** {@inheritDoc} */
 304     @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 305         final ScrollBar s = getSkinnable();
 306         return s.getOrientation() == Orientation.VERTICAL ? Double.MAX_VALUE : s.prefHeight(-1);
 307     }
 308 
 309 
 310 
 311     /***************************************************************************
 312      *                                                                         *
 313      * Private implementation                                                  *
 314      *                                                                         *
 315      **************************************************************************/
 316 
 317     /**
 318      * Initializes the ScrollBarSkin. Creates the scene and sets up all the
 319      * bindings for the group.
 320      */
 321     private void initialize() {
 322 
 323         track = new StackPane();
 324         track.getStyleClass().setAll("track");
 325 
 326         trackBackground = new StackPane();
 327         trackBackground.getStyleClass().setAll("track-background");
 328 
 329         thumb = new StackPane() {
 330             @Override
 331             public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
 332                 switch (attribute) {
 333                     case VALUE: return getSkinnable().getValue();
 334                     default: return super.queryAccessibleAttribute(attribute, parameters);
 335                 }
 336             }
 337         };
 338         thumb.getStyleClass().setAll("thumb");
 339         thumb.setAccessibleRole(AccessibleRole.THUMB);
 340 
 341 
 342         if (!Properties.IS_TOUCH_SUPPORTED) {
 343 
 344             incButton = new EndButton("increment-button", "increment-arrow") {
 345                 @Override
 346                 public void executeAccessibleAction(AccessibleAction action, Object... parameters) {
 347                     switch (action) {
 348                         case FIRE:
 349                             getSkinnable().increment();
 350                             break;
 351                         default: super.executeAccessibleAction(action, parameters);
 352                     }
 353                 }
 354             };
 355             incButton.setAccessibleRole(AccessibleRole.INCREMENT_BUTTON);
 356             incButton.setOnMousePressed(me -> {
 357                 /*
 358                 ** if the tracklenght isn't greater than do nothing....
 359                 */
 360                 if (!thumb.isVisible() || trackLength > thumbLength) {
 361                     behavior.incButtonPressed();
 362                 }
 363                 me.consume();
 364             });
 365             incButton.setOnMouseReleased(me -> {
 366                 /*
 367                 ** if the tracklenght isn't greater than do nothing....
 368                 */
 369                 if (!thumb.isVisible() || trackLength > thumbLength) {
 370                     behavior.incButtonReleased();
 371                 }
 372                 me.consume();
 373             });
 374 
 375             decButton = new EndButton("decrement-button", "decrement-arrow") {
 376                 @Override
 377                 public void executeAccessibleAction(AccessibleAction action, Object... parameters) {
 378                     switch (action) {
 379                         case FIRE:
 380                             getSkinnable().decrement();
 381                             break;
 382                         default: super.executeAccessibleAction(action, parameters);
 383                     }
 384                 }
 385             };
 386             decButton.setAccessibleRole(AccessibleRole.DECREMENT_BUTTON);
 387             decButton.setOnMousePressed(me -> {
 388                 /*
 389                 ** if the tracklenght isn't greater than do nothing....
 390                 */
 391                 if (!thumb.isVisible() || trackLength > thumbLength) {
 392                     behavior.decButtonPressed();
 393                 }
 394                 me.consume();
 395             });
 396             decButton.setOnMouseReleased(me -> {
 397                 /*
 398                 ** if the tracklenght isn't greater than do nothing....
 399                 */
 400                 if (!thumb.isVisible() || trackLength > thumbLength) {
 401                     behavior.decButtonReleased();
 402                 }
 403                 me.consume();
 404             });
 405         }
 406 
 407 
 408         track.setOnMousePressed(me -> {
 409             if (!thumb.isPressed() && me.getButton() == MouseButton.PRIMARY) {
 410                 if (getSkinnable().getOrientation() == Orientation.VERTICAL) {
 411                     if (trackLength != 0) {
 412                         behavior.trackPress(me.getY() / trackLength);
 413                         me.consume();
 414                     }
 415                 } else {
 416                     if (trackLength != 0) {
 417                         behavior.trackPress(me.getX() / trackLength);
 418                         me.consume();
 419                     }
 420                 }
 421             }
 422         });
 423 
 424         track.setOnMouseReleased(me -> {
 425             behavior.trackRelease();
 426             me.consume();
 427         });
 428 
 429         thumb.setOnMousePressed(me -> {
 430             if (me.isSynthesized()) {
 431                 // touch-screen events handled by Scroll handler
 432                 me.consume();
 433                 return;
 434             }
 435             /*
 436             ** if max isn't greater than min then there is nothing to do here
 437             */
 438             if (getSkinnable().getMax() > getSkinnable().getMin()) {
 439                 dragStart = thumb.localToParent(me.getX(), me.getY());
 440                 double clampedValue = Utils.clamp(getSkinnable().getMin(), getSkinnable().getValue(), getSkinnable().getMax());
 441                 preDragThumbPos = (clampedValue - getSkinnable().getMin()) / (getSkinnable().getMax() - getSkinnable().getMin());
 442                 me.consume();
 443             }
 444         });
 445 
 446 
 447         thumb.setOnMouseDragged(me -> {
 448             if (me.isSynthesized()) {
 449                 // touch-screen events handled by Scroll handler
 450                 me.consume();
 451                 return;
 452             }
 453             /*
 454             ** if max isn't greater than min then there is nothing to do here
 455             */
 456             if (getSkinnable().getMax() > getSkinnable().getMin()) {
 457                 /*
 458                 ** if the tracklength isn't greater then do nothing....
 459                 */
 460                 if (trackLength > thumbLength) {
 461                     Point2D cur = thumb.localToParent(me.getX(), me.getY());
 462                     if (dragStart == null) {
 463                         // we're getting dragged without getting a mouse press
 464                         dragStart = thumb.localToParent(me.getX(), me.getY());
 465                     }
 466                     double dragPos = getSkinnable().getOrientation() == Orientation.VERTICAL ? cur.getY() - dragStart.getY(): cur.getX() - dragStart.getX();
 467                     behavior.thumbDragged(preDragThumbPos + dragPos / (trackLength - thumbLength));
 468                 }
 469 
 470                 me.consume();
 471             }
 472         });
 473 
 474         thumb.setOnScrollStarted(se -> {
 475             if (se.isDirect()) {
 476                 /*
 477                 ** if max isn't greater than min then there is nothing to do here
 478                 */
 479                 if (getSkinnable().getMax() > getSkinnable().getMin()) {
 480                     dragStart = thumb.localToParent(se.getX(), se.getY());
 481                     double clampedValue = Utils.clamp(getSkinnable().getMin(), getSkinnable().getValue(), getSkinnable().getMax());
 482                     preDragThumbPos = (clampedValue - getSkinnable().getMin()) / (getSkinnable().getMax() - getSkinnable().getMin());
 483                     se.consume();
 484                 }
 485             }
 486         });
 487 
 488         thumb.setOnScroll(event -> {
 489             if (event.isDirect()) {
 490                 /*
 491                 ** if max isn't greater than min then there is nothing to do here
 492                 */
 493                 if (getSkinnable().getMax() > getSkinnable().getMin()) {
 494                     /*
 495                     ** if the tracklength isn't greater then do nothing....
 496                     */
 497                     if (trackLength > thumbLength) {
 498                         Point2D cur = thumb.localToParent(event.getX(), event.getY());
 499                         if (dragStart == null) {
 500                             // we're getting dragged without getting a mouse press
 501                             dragStart = thumb.localToParent(event.getX(), event.getY());
 502                         }
 503                         double dragPos = getSkinnable().getOrientation() == Orientation.VERTICAL ? cur.getY() - dragStart.getY(): cur.getX() - dragStart.getX();
 504                         behavior.thumbDragged(/*todo*/ preDragThumbPos + dragPos / (trackLength - thumbLength));
 505                     }
 506 
 507                     event.consume();
 508                     return;
 509                 }
 510             }
 511         });
 512 
 513 
 514         getSkinnable().addEventHandler(ScrollEvent.SCROLL, event -> {
 515             /*
 516             ** if the tracklength isn't greater then do nothing....
 517             */
 518             if (trackLength > thumbLength) {
 519 
 520                 double dx = event.getDeltaX();
 521                 double dy = event.getDeltaY();
 522 
 523                 /*
 524                 ** in 2.0 a horizontal scrollbar would scroll on a vertical
 525                 ** drag on a tracker-pad. We need to keep this behavior.
 526                 */
 527                 dx = (Math.abs(dx) < Math.abs(dy) ? dy : dx);
 528 
 529                 /*
 530                 ** we only consume an event that we've used.
 531                 */
 532                 ScrollBar sb = (ScrollBar) getSkinnable();
 533 
 534                 double delta = (getSkinnable().getOrientation() == Orientation.VERTICAL ? dy : dx);
 535 
 536                 /*
 537                 ** RT-22941 - If this is either a touch or inertia scroll
 538                 ** then we move to the position of the touch point.
 539                 *
 540                 * TODO: this fix causes RT-23406 ([ScrollBar, touch] Dragging scrollbar from the
 541                 * track on touchscreen causes flickering)
 542                 */
 543                 if (event.isDirect()) {
 544                     if (trackLength > thumbLength) {
 545                         behavior.thumbDragged((getSkinnable().getOrientation() == Orientation.VERTICAL ? event.getY() : event.getX()) / trackLength);
 546                         event.consume();
 547                     }
 548                 }
 549                 else {
 550                     if (delta > 0.0 && sb.getValue() > sb.getMin()) {
 551                         sb.decrement();
 552                         event.consume();
 553                     } else if (delta < 0.0 && sb.getValue() < sb.getMax()) {
 554                         sb.increment();
 555                         event.consume();
 556                     }
 557                 }
 558             }
 559         });
 560 
 561         getChildren().clear();
 562         if (!Properties.IS_TOUCH_SUPPORTED) {
 563             getChildren().addAll(trackBackground, incButton, decButton, track, thumb);
 564         }
 565         else {
 566             getChildren().addAll(track, thumb);
 567         }
 568     }
 569 
 570 
 571     /*
 572      * Gets the breadth of the scrollbar. The "breadth" is the distance
 573      * across the scrollbar, i.e. if vertical the width, otherwise the height.
 574      * On desktop this is determined by the greater of the breadths of the end-buttons.
 575      * Embedded doesn't have end-buttons, so currently we use a default breadth.
 576      * We should change this when we get width/height css properties.
 577      */
 578     double getBreadth() {
 579         if (!Properties.IS_TOUCH_SUPPORTED) {
 580             if (getSkinnable().getOrientation() == Orientation.VERTICAL) {
 581                 return Math.max(decButton.prefWidth(-1), incButton.prefWidth(-1)) +snappedLeftInset()+snappedRightInset();
 582             } else {
 583                 return Math.max(decButton.prefHeight(-1), incButton.prefHeight(-1)) +snappedTopInset()+snappedBottomInset();
 584             }
 585         }
 586         else {
 587             if (getSkinnable().getOrientation() == Orientation.VERTICAL) {
 588                 return Math.max(Properties.DEFAULT_EMBEDDED_SB_BREADTH, Properties.DEFAULT_EMBEDDED_SB_BREADTH)+snappedLeftInset()+snappedRightInset();
 589             } else {
 590                 return Math.max(Properties.DEFAULT_EMBEDDED_SB_BREADTH, Properties.DEFAULT_EMBEDDED_SB_BREADTH)+snappedTopInset()+snappedBottomInset();
 591             }
 592         }
 593     }
 594 
 595     double minThumbLength() {
 596         return 1.5f * getBreadth();
 597     }
 598 
 599     double minTrackLength() {
 600         return 2.0f * getBreadth();
 601     }
 602 
 603     /**
 604      * Called when ever either min, max or value changes, so thumb's layoutX, Y is recomputed.
 605      */
 606     void positionThumb() {
 607         ScrollBar s = getSkinnable();
 608         double clampedValue = Utils.clamp(s.getMin(), s.getValue(), s.getMax());
 609         trackPos = (s.getMax() - s.getMin() > 0) ? ((trackLength - thumbLength) * (clampedValue - s.getMin()) / (s.getMax() - s.getMin())) : (0.0F);
 610 
 611         if (!Properties.IS_TOUCH_SUPPORTED) {
 612             if (s.getOrientation() == Orientation.VERTICAL) {
 613                 trackPos += decButton.prefHeight(-1);
 614             } else {
 615                 trackPos += decButton.prefWidth(-1);
 616             }
 617         }
 618 
 619         thumb.setTranslateX( snapPosition(s.getOrientation() == Orientation.VERTICAL ? snappedLeftInset() : trackPos + snappedLeftInset()));
 620         thumb.setTranslateY( snapPosition(s.getOrientation() == Orientation.VERTICAL ? trackPos + snappedTopInset() : snappedTopInset()));
 621     }
 622 
 623     private Node getThumb() {
 624         return thumb;
 625     }
 626 
 627     private Node getTrack() {
 628         return track;
 629     }
 630 
 631     private Node getIncrementButton() {
 632         return incButton;
 633     }
 634 
 635     private Node getDecrementButton() {
 636         return decButton;
 637     }
 638 
 639 
 640 
 641     /***************************************************************************
 642      *                                                                         *
 643      * Support classes                                                         *
 644      *                                                                         *
 645      **************************************************************************/
 646 
 647     private static class EndButton extends Region {
 648         private Region arrow;
 649 
 650         private EndButton(String styleClass, String arrowStyleClass) {
 651             getStyleClass().setAll(styleClass);
 652             arrow = new Region();
 653             arrow.getStyleClass().setAll(arrowStyleClass);
 654             getChildren().setAll(arrow);
 655             requestLayout();
 656         }
 657 
 658         @Override protected void layoutChildren() {
 659             final double top = snappedTopInset();
 660             final double left = snappedLeftInset();
 661             final double bottom = snappedBottomInset();
 662             final double right = snappedRightInset();
 663             final double aw = snapSizeX(arrow.prefWidth(-1));
 664             final double ah = snapSizeY(arrow.prefHeight(-1));
 665             final double yPos = snapPositionY((getHeight() - (top + bottom + ah)) / 2.0);
 666             final double xPos = snapPositionX((getWidth() - (left + right + aw)) / 2.0);
 667             arrow.resizeRelocate(xPos + left, yPos + top, aw, ah);
 668         }
 669 
 670         @Override protected double computeMinHeight(double width) {
 671             return prefHeight(-1);
 672         }
 673 
 674         @Override protected double computeMinWidth(double height) {
 675             return prefWidth(-1);
 676         }
 677 
 678         @Override protected double computePrefWidth(double height) {
 679             final double left = snappedLeftInset();
 680             final double right = snappedRightInset();
 681             final double aw = snapSizeX(arrow.prefWidth(-1));
 682             return left + aw + right;
 683         }
 684 
 685         @Override protected double computePrefHeight(double width) {
 686             final double top = snappedTopInset();
 687             final double bottom = snappedBottomInset();
 688             final double ah = snapSizeY(arrow.prefHeight(-1));
 689             return top + ah + bottom;
 690         }
 691     }
 692 }