1 /*
   2  * Copyright (c) 2010, 2017, 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.behavior.BehaviorBase;
  29 import javafx.animation.Transition;
  30 import javafx.geometry.Orientation;
  31 import javafx.geometry.Point2D;
  32 import javafx.geometry.Side;
  33 import javafx.scene.AccessibleAttribute;
  34 import javafx.scene.AccessibleRole;
  35 import javafx.scene.Node;
  36 import javafx.scene.chart.NumberAxis;
  37 import javafx.scene.control.Accordion;
  38 import javafx.scene.control.Button;
  39 import javafx.scene.control.Control;
  40 import javafx.scene.control.SkinBase;
  41 import javafx.scene.control.Slider;
  42 import javafx.scene.layout.StackPane;
  43 import javafx.util.Duration;
  44 import javafx.util.StringConverter;
  45 
  46 import com.sun.javafx.scene.control.behavior.SliderBehavior;
  47 
  48 /**
  49  * Default skin implementation for the {@link Slider} control.
  50  *
  51  * @see Slider
  52  * @since 9
  53  */
  54 public class SliderSkin extends SkinBase<Slider> {
  55 
  56     /***************************************************************************
  57      *                                                                         *
  58      * Private fields                                                          *
  59      *                                                                         *
  60      **************************************************************************/
  61 
  62     /** Track if slider is vertical/horizontal and cause re layout */
  63 //    private boolean horizontal;
  64     private NumberAxis tickLine = null;
  65     private double trackToTickGap = 2;
  66 
  67     private boolean showTickMarks;
  68     private double thumbWidth;
  69     private double thumbHeight;
  70 
  71     private double trackStart;
  72     private double trackLength;
  73     private double thumbTop;
  74     private double thumbLeft;
  75     private double preDragThumbPos;
  76     private Point2D dragStart; // in skin coordinates
  77 
  78     private StackPane thumb;
  79     private StackPane track;
  80     private boolean trackClicked = false;
  81 //    private double visibleAmount = 16;
  82 
  83     private final SliderBehavior behavior;
  84 
  85     StringConverter<Number> stringConverterWrapper = new StringConverter<Number>() {
  86         Slider slider = getSkinnable();
  87         @Override public String toString(Number object) {
  88             return(object != null) ? slider.getLabelFormatter().toString(object.doubleValue()) : "";
  89         }
  90         @Override public Number fromString(String string) {
  91             return slider.getLabelFormatter().fromString(string);
  92         }
  93     };
  94 
  95 
  96 
  97     /***************************************************************************
  98      *                                                                         *
  99      * Constructors                                                            *
 100      *                                                                         *
 101      **************************************************************************/
 102 
 103     /**
 104      * Creates a new SliderSkin instance, installing the necessary child
 105      * nodes into the Control {@link Control#getChildren() children} list, as
 106      * well as the necessary input mappings for handling key, mouse, etc events.
 107      *
 108      * @param control The control that this skin should be installed onto.
 109      */
 110     public SliderSkin(Slider control) {
 111         super(control);
 112 
 113         behavior = new SliderBehavior(control);
 114 //        control.setInputMap(behavior.getInputMap());
 115 
 116         initialize();
 117         control.requestLayout();
 118         
 119         if (control.getTooltip() != null) {
 120             control.getTooltip().setConsumeAutoHidingEvents(false);
 121         }
 122 
 123         registerChangeListener(control.minProperty(), e -> {
 124             if (showTickMarks && tickLine != null) {
 125                 tickLine.setLowerBound(control.getMin());
 126             }
 127             getSkinnable().requestLayout();
 128         });
 129         registerChangeListener(control.maxProperty(), e -> {
 130             if (showTickMarks && tickLine != null) {
 131                 tickLine.setUpperBound(control.getMax());
 132             }
 133             getSkinnable().requestLayout();
 134         });
 135         registerChangeListener(control.valueProperty(), e -> {
 136             // only animate thumb if the track was clicked - not if the thumb is dragged
 137             positionThumb(trackClicked);
 138         });
 139         registerChangeListener(control.orientationProperty(), e -> {
 140             if (showTickMarks && tickLine != null) {
 141                 tickLine.setSide(control.getOrientation() == Orientation.VERTICAL ? Side.RIGHT : (control.getOrientation() == null) ? Side.RIGHT: Side.BOTTOM);
 142             }
 143             getSkinnable().requestLayout();
 144         });
 145         registerChangeListener(control.showTickMarksProperty(), e -> setShowTickMarks(control.isShowTickMarks(), control.isShowTickLabels()));
 146         registerChangeListener(control.showTickLabelsProperty(), e -> setShowTickMarks(control.isShowTickMarks(), control.isShowTickLabels()));
 147         registerChangeListener(control.majorTickUnitProperty(), e -> {
 148             if (tickLine != null) {
 149                 tickLine.setTickUnit(control.getMajorTickUnit());
 150                 getSkinnable().requestLayout();
 151             }
 152         });
 153         registerChangeListener(control.minorTickCountProperty(), e -> {
 154             if (tickLine != null) {
 155                 tickLine.setMinorTickCount(Math.max(control.getMinorTickCount(), 0) + 1);
 156                 getSkinnable().requestLayout();
 157             }
 158         });
 159         registerChangeListener(control.labelFormatterProperty(), e -> {
 160             if (tickLine != null) {
 161                 if (control.getLabelFormatter() == null) {
 162                     tickLine.setTickLabelFormatter(null);
 163                 } else {
 164                     tickLine.setTickLabelFormatter(stringConverterWrapper);
 165                     tickLine.requestAxisLayout();
 166                 }
 167             }
 168         });
 169         registerChangeListener(control.snapToTicksProperty(), e -> {
 170             control.adjustValue(control.getValue());
 171         });
 172     }
 173 
 174 
 175 
 176     /***************************************************************************
 177      *                                                                         *
 178      * Public API                                                              *
 179      *                                                                         *
 180      **************************************************************************/
 181 
 182     /** {@inheritDoc} */
 183     @Override public void dispose() {
 184         super.dispose();
 185 
 186         if (behavior != null) {
 187             behavior.dispose();
 188         }
 189     }
 190 
 191     /** {@inheritDoc} */
 192     @Override protected void layoutChildren(final double x, final double y,
 193                                             final double w, final double h) {
 194         // calculate the available space
 195         // resize thumb to preferred size
 196         thumbWidth = snapSizeX(thumb.prefWidth(-1));
 197         thumbHeight = snapSizeY(thumb.prefHeight(-1));
 198         thumb.resize(thumbWidth, thumbHeight);
 199         // we are assuming the is common radius's for all corners on the track
 200         double trackRadius = track.getBackground() == null ? 0 : track.getBackground().getFills().size() > 0 ?
 201                 track.getBackground().getFills().get(0).getRadii().getTopLeftHorizontalRadius() : 0;
 202 
 203         if (getSkinnable().getOrientation() == Orientation.HORIZONTAL) {
 204             double tickLineHeight =  (showTickMarks) ? tickLine.prefHeight(-1) : 0;
 205             double trackHeight = snapSizeY(track.prefHeight(-1));
 206             double trackAreaHeight = Math.max(trackHeight,thumbHeight);
 207             double totalHeightNeeded = trackAreaHeight  + ((showTickMarks) ? trackToTickGap+tickLineHeight : 0);
 208             double startY = y + ((h - totalHeightNeeded)/2); // center slider in available height vertically
 209             trackLength = snapSizeX(w - thumbWidth);
 210             trackStart = snapPositionX(x + (thumbWidth/2));
 211             double trackTop = (int)(startY + ((trackAreaHeight-trackHeight)/2));
 212             thumbTop = (int)(startY + ((trackAreaHeight-thumbHeight)/2));
 213 
 214             positionThumb(false);
 215             // layout track
 216             track.resizeRelocate((int)(trackStart - trackRadius),
 217                     trackTop ,
 218                     (int)(trackLength + trackRadius + trackRadius),
 219                     trackHeight);
 220             // layout tick line
 221             if (showTickMarks) {
 222                 tickLine.setLayoutX(trackStart);
 223                 tickLine.setLayoutY(trackTop+trackHeight+trackToTickGap);
 224                 tickLine.resize(trackLength, tickLineHeight);
 225                 tickLine.requestAxisLayout();
 226             } else {
 227                 if (tickLine != null) {
 228                     tickLine.resize(0,0);
 229                     tickLine.requestAxisLayout();
 230                 }
 231                 tickLine = null;
 232             }
 233         } else {
 234             double tickLineWidth = (showTickMarks) ? tickLine.prefWidth(-1) : 0;
 235             double trackWidth = snapSizeX(track.prefWidth(-1));
 236             double trackAreaWidth = Math.max(trackWidth,thumbWidth);
 237             double totalWidthNeeded = trackAreaWidth  + ((showTickMarks) ? trackToTickGap+tickLineWidth : 0) ;
 238             double startX = x + ((w - totalWidthNeeded)/2); // center slider in available width horizontally
 239             trackLength = snapSizeY(h - thumbHeight);
 240             trackStart = snapPositionY(y + (thumbHeight/2));
 241             double trackLeft = (int)(startX + ((trackAreaWidth-trackWidth)/2));
 242             thumbLeft = (int)(startX + ((trackAreaWidth-thumbWidth)/2));
 243 
 244             positionThumb(false);
 245             // layout track
 246             track.resizeRelocate(trackLeft,
 247                     (int)(trackStart - trackRadius),
 248                     trackWidth,
 249                     (int)(trackLength + trackRadius + trackRadius));
 250             // layout tick line
 251             if (showTickMarks) {
 252                 tickLine.setLayoutX(trackLeft+trackWidth+trackToTickGap);
 253                 tickLine.setLayoutY(trackStart);
 254                 tickLine.resize(tickLineWidth, trackLength);
 255                 tickLine.requestAxisLayout();
 256             } else {
 257                 if (tickLine != null) {
 258                     tickLine.resize(0,0);
 259                     tickLine.requestAxisLayout();
 260                 }
 261                 tickLine = null;
 262             }
 263         }
 264     }
 265 
 266     /** {@inheritDoc} */
 267     @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 268         final Slider s = getSkinnable();
 269         if (s.getOrientation() == Orientation.HORIZONTAL) {
 270             return (leftInset + minTrackLength() + thumb.minWidth(-1) + rightInset);
 271         } else {
 272             return(leftInset + thumb.prefWidth(-1) + rightInset);
 273         }
 274     }
 275 
 276     /** {@inheritDoc} */
 277     @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 278         final Slider s = getSkinnable();
 279         if (s.getOrientation() == Orientation.HORIZONTAL) {
 280             double axisHeight = showTickMarks ? (tickLine.prefHeight(-1) + trackToTickGap) : 0;
 281             return topInset + thumb.prefHeight(-1) + axisHeight + bottomInset;
 282         } else {
 283             return topInset + minTrackLength() + thumb.prefHeight(-1) + bottomInset;
 284         }
 285     }
 286 
 287     /** {@inheritDoc} */
 288     @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 289         final Slider s = getSkinnable();
 290         if (s.getOrientation() == Orientation.HORIZONTAL) {
 291             if(showTickMarks) {
 292                 return Math.max(140, tickLine.prefWidth(-1));
 293             } else {
 294                 return 140;
 295             }
 296         } else {
 297             double axisWidth = showTickMarks ? (tickLine.prefWidth(-1) + trackToTickGap) : 0;
 298             return leftInset + Math.max(thumb.prefWidth(-1), track.prefWidth(-1)) + axisWidth + rightInset;
 299         }
 300     }
 301 
 302     /** {@inheritDoc} */
 303     @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 304         final Slider s = getSkinnable();
 305         if (s.getOrientation() == Orientation.HORIZONTAL) {
 306             return topInset + Math.max(thumb.prefHeight(-1), track.prefHeight(-1)) +
 307                     ((showTickMarks) ? (trackToTickGap+tickLine.prefHeight(-1)) : 0)  + bottomInset;
 308         } else {
 309             if(showTickMarks) {
 310                 return Math.max(140, tickLine.prefHeight(-1));
 311             } else {
 312                 return 140;
 313             }
 314         }
 315     }
 316 
 317     /** {@inheritDoc} */
 318     @Override protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 319         if (getSkinnable().getOrientation() == Orientation.HORIZONTAL) {
 320             return Double.MAX_VALUE;
 321         } else {
 322             return getSkinnable().prefWidth(-1);
 323         }
 324     }
 325 
 326     /** {@inheritDoc} */
 327     @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 328         if (getSkinnable().getOrientation() == Orientation.HORIZONTAL) {
 329             return getSkinnable().prefHeight(width);
 330         } else {
 331             return Double.MAX_VALUE;
 332         }
 333     }
 334 
 335 
 336 
 337     /***************************************************************************
 338      *                                                                         *
 339      * Private implementation                                                  *
 340      *                                                                         *
 341      **************************************************************************/
 342 
 343     private void initialize() {
 344         thumb = new StackPane() {
 345             @Override
 346             public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
 347                 switch (attribute) {
 348                     case VALUE: return getSkinnable().getValue();
 349                     default: return super.queryAccessibleAttribute(attribute, parameters);
 350                 }
 351             }
 352         };
 353         thumb.getStyleClass().setAll("thumb");
 354         thumb.setAccessibleRole(AccessibleRole.THUMB);
 355         track = new StackPane();
 356         track.getStyleClass().setAll("track");
 357 //        horizontal = getSkinnable().isVertical();
 358 
 359         getChildren().clear();
 360         getChildren().addAll(track, thumb);
 361         setShowTickMarks(getSkinnable().isShowTickMarks(), getSkinnable().isShowTickLabels());
 362         track.setOnMousePressed(me -> {
 363             if (!thumb.isPressed()) {
 364                 trackClicked = true;
 365                 if (getSkinnable().getOrientation() == Orientation.HORIZONTAL) {
 366                     behavior.trackPress(me, (me.getX() / trackLength));
 367                 } else {
 368                     behavior.trackPress(me, (me.getY() / trackLength));
 369                 }
 370                 trackClicked = false;
 371             }
 372         });
 373 
 374         track.setOnMouseDragged(me -> {
 375             if (!thumb.isPressed()) {
 376                 if (getSkinnable().getOrientation() == Orientation.HORIZONTAL) {
 377                     behavior.trackPress(me, (me.getX() / trackLength));
 378                 } else {
 379                     behavior.trackPress(me, (me.getY() / trackLength));
 380                 }
 381             }
 382         });
 383 
 384         thumb.setOnMousePressed(me -> {
 385             behavior.thumbPressed(me, 0.0f);
 386             dragStart = thumb.localToParent(me.getX(), me.getY());
 387             preDragThumbPos = (getSkinnable().getValue() - getSkinnable().getMin()) /
 388                     (getSkinnable().getMax() - getSkinnable().getMin());
 389         });
 390 
 391         thumb.setOnMouseReleased(me -> {
 392             behavior.thumbReleased(me);
 393         });
 394 
 395         thumb.setOnMouseDragged(me -> {
 396             Point2D cur = thumb.localToParent(me.getX(), me.getY());
 397             double dragPos = (getSkinnable().getOrientation() == Orientation.HORIZONTAL) ?
 398                     cur.getX() - dragStart.getX() : -(cur.getY() - dragStart.getY());
 399             behavior.thumbDragged(me, preDragThumbPos + dragPos / trackLength);
 400         });
 401     }
 402 
 403     private void setShowTickMarks(boolean ticksVisible, boolean labelsVisible) {
 404         showTickMarks = (ticksVisible || labelsVisible);
 405         Slider slider = getSkinnable();
 406         if (showTickMarks) {
 407             if (tickLine == null) {
 408                 tickLine = new NumberAxis();
 409                 tickLine.setAutoRanging(false);
 410                 tickLine.setSide(slider.getOrientation() == Orientation.VERTICAL ? Side.RIGHT : (slider.getOrientation() == null) ? Side.RIGHT: Side.BOTTOM);
 411                 tickLine.setUpperBound(slider.getMax());
 412                 tickLine.setLowerBound(slider.getMin());
 413                 tickLine.setTickUnit(slider.getMajorTickUnit());
 414                 tickLine.setTickMarkVisible(ticksVisible);
 415                 tickLine.setTickLabelsVisible(labelsVisible);
 416                 tickLine.setMinorTickVisible(ticksVisible);
 417                 // add 1 to the slider minor tick count since the axis draws one
 418                 // less minor ticks than the number given.
 419                 tickLine.setMinorTickCount(Math.max(slider.getMinorTickCount(),0) + 1);
 420                 if (slider.getLabelFormatter() != null) {
 421                     tickLine.setTickLabelFormatter(stringConverterWrapper);
 422                 }
 423                 getChildren().clear();
 424                 getChildren().addAll(tickLine, track, thumb);
 425             } else {
 426                 tickLine.setTickLabelsVisible(labelsVisible);
 427                 tickLine.setTickMarkVisible(ticksVisible);
 428                 tickLine.setMinorTickVisible(ticksVisible);
 429             }
 430         }
 431         else  {
 432             getChildren().clear();
 433             getChildren().addAll(track, thumb);
 434 //            tickLine = null;
 435         }
 436 
 437         getSkinnable().requestLayout();
 438     }
 439 
 440     /**
 441      * Called when ever either min, max or value changes, so thumb's layoutX, Y is recomputed.
 442      */
 443     void positionThumb(final boolean animate) {
 444         Slider s = getSkinnable();
 445         if (s.getValue() > s.getMax()) return;// this can happen if we are bound to something
 446         boolean horizontal = s.getOrientation() == Orientation.HORIZONTAL;
 447         final double endX = (horizontal) ? trackStart + (((trackLength * ((s.getValue() - s.getMin()) /
 448                 (s.getMax() - s.getMin()))) - thumbWidth/2)) : thumbLeft;
 449         final double endY = (horizontal) ? thumbTop :
 450             snappedTopInset() + trackLength - (trackLength * ((s.getValue() - s.getMin()) /
 451                 (s.getMax() - s.getMin()))); //  - thumbHeight/2
 452 
 453         if (animate) {
 454             // lets animate the thumb transition
 455             final double startX = thumb.getLayoutX();
 456             final double startY = thumb.getLayoutY();
 457             Transition transition = new Transition() {
 458                 {
 459                     setCycleDuration(Duration.millis(200));
 460                 }
 461 
 462                 @Override protected void interpolate(double frac) {
 463                     if (!Double.isNaN(startX)) {
 464                         thumb.setLayoutX(startX + frac * (endX - startX));
 465                     }
 466                     if (!Double.isNaN(startY)) {
 467                         thumb.setLayoutY(startY + frac * (endY - startY));
 468                     }
 469                 }
 470             };
 471             transition.play();
 472         } else {
 473             thumb.setLayoutX(endX);
 474             thumb.setLayoutY(endY);
 475         }
 476     }
 477 
 478     double minTrackLength() {
 479         return 2*thumb.prefWidth(-1);
 480     }
 481 }
 482