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