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.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         registerChangeListener(control.minProperty(), e -> {
 119             if (showTickMarks && tickLine != null) {
 120                 tickLine.setLowerBound(control.getMin());
 121             }
 122             getSkinnable().requestLayout();
 123         });
 124         registerChangeListener(control.maxProperty(), e -> {
 125             if (showTickMarks && tickLine != null) {
 126                 tickLine.setUpperBound(control.getMax());
 127             }
 128             getSkinnable().requestLayout();
 129         });
 130         registerChangeListener(control.valueProperty(), e -> {
 131             // only animate thumb if the track was clicked - not if the thumb is dragged
 132             positionThumb(trackClicked);
 133         });
 134         registerChangeListener(control.orientationProperty(), e -> {
 135             if (showTickMarks && tickLine != null) {
 136                 tickLine.setSide(control.getOrientation() == Orientation.VERTICAL ? Side.RIGHT : (control.getOrientation() == null) ? Side.RIGHT: Side.BOTTOM);
 137             }
 138             getSkinnable().requestLayout();
 139         });
 140         registerChangeListener(control.showTickMarksProperty(), e -> setShowTickMarks(control.isShowTickMarks(), control.isShowTickLabels()));
 141         registerChangeListener(control.showTickLabelsProperty(), e -> setShowTickMarks(control.isShowTickMarks(), control.isShowTickLabels()));
 142         registerChangeListener(control.majorTickUnitProperty(), e -> {
 143             if (tickLine != null) {
 144                 tickLine.setTickUnit(control.getMajorTickUnit());
 145                 getSkinnable().requestLayout();
 146             }
 147         });
 148         registerChangeListener(control.minorTickCountProperty(), e -> {
 149             if (tickLine != null) {
 150                 tickLine.setMinorTickCount(Math.max(control.getMinorTickCount(), 0) + 1);
 151                 getSkinnable().requestLayout();
 152             }
 153         });
 154         registerChangeListener(control.labelFormatterProperty(), e -> {
 155             if (tickLine != null) {
 156                 if (control.getLabelFormatter() == null) {
 157                     tickLine.setTickLabelFormatter(null);
 158                 } else {
 159                     tickLine.setTickLabelFormatter(stringConverterWrapper);
 160                     tickLine.requestAxisLayout();
 161                 }
 162             }
 163         });
 164         registerChangeListener(control.snapToTicksProperty(), e -> {
 165             control.adjustValue(control.getValue());
 166         });
 167     }
 168 
 169 
 170 
 171     /***************************************************************************
 172      *                                                                         *
 173      * Public API                                                              *
 174      *                                                                         *
 175      **************************************************************************/
 176 
 177     /** {@inheritDoc} */
 178     @Override public void dispose() {
 179         super.dispose();
 180 
 181         if (behavior != null) {
 182             behavior.dispose();
 183         }
 184     }
 185 
 186     /** {@inheritDoc} */
 187     @Override protected void layoutChildren(final double x, final double y,
 188                                             final double w, final double h) {
 189         // calculate the available space
 190         // resize thumb to preferred size
 191         thumbWidth = snapSize(thumb.prefWidth(-1));
 192         thumbHeight = snapSize(thumb.prefHeight(-1));
 193         thumb.resize(thumbWidth, thumbHeight);
 194         // we are assuming the is common radius's for all corners on the track
 195         double trackRadius = track.getBackground() == null ? 0 : track.getBackground().getFills().size() > 0 ?
 196                 track.getBackground().getFills().get(0).getRadii().getTopLeftHorizontalRadius() : 0;
 197 
 198         if (getSkinnable().getOrientation() == Orientation.HORIZONTAL) {
 199             double tickLineHeight =  (showTickMarks) ? tickLine.prefHeight(-1) : 0;
 200             double trackHeight = snapSize(track.prefHeight(-1));
 201             double trackAreaHeight = Math.max(trackHeight,thumbHeight);
 202             double totalHeightNeeded = trackAreaHeight  + ((showTickMarks) ? trackToTickGap+tickLineHeight : 0);
 203             double startY = y + ((h - totalHeightNeeded)/2); // center slider in available height vertically
 204             trackLength = snapSize(w - thumbWidth);
 205             trackStart = snapPosition(x + (thumbWidth/2));
 206             double trackTop = (int)(startY + ((trackAreaHeight-trackHeight)/2));
 207             thumbTop = (int)(startY + ((trackAreaHeight-thumbHeight)/2));
 208 
 209             positionThumb(false);
 210             // layout track
 211             track.resizeRelocate((int)(trackStart - trackRadius),
 212                     trackTop ,
 213                     (int)(trackLength + trackRadius + trackRadius),
 214                     trackHeight);
 215             // layout tick line
 216             if (showTickMarks) {
 217                 tickLine.setLayoutX(trackStart);
 218                 tickLine.setLayoutY(trackTop+trackHeight+trackToTickGap);
 219                 tickLine.resize(trackLength, tickLineHeight);
 220                 tickLine.requestAxisLayout();
 221             } else {
 222                 if (tickLine != null) {
 223                     tickLine.resize(0,0);
 224                     tickLine.requestAxisLayout();
 225                 }
 226                 tickLine = null;
 227             }
 228         } else {
 229             double tickLineWidth = (showTickMarks) ? tickLine.prefWidth(-1) : 0;
 230             double trackWidth = snapSize(track.prefWidth(-1));
 231             double trackAreaWidth = Math.max(trackWidth,thumbWidth);
 232             double totalWidthNeeded = trackAreaWidth  + ((showTickMarks) ? trackToTickGap+tickLineWidth : 0) ;
 233             double startX = x + ((w - totalWidthNeeded)/2); // center slider in available width horizontally
 234             trackLength = snapSize(h - thumbHeight);
 235             trackStart = snapPosition(y + (thumbHeight/2));
 236             double trackLeft = (int)(startX + ((trackAreaWidth-trackWidth)/2));
 237             thumbLeft = (int)(startX + ((trackAreaWidth-thumbWidth)/2));
 238 
 239             positionThumb(false);
 240             // layout track
 241             track.resizeRelocate(trackLeft,
 242                     (int)(trackStart - trackRadius),
 243                     trackWidth,
 244                     (int)(trackLength + trackRadius + trackRadius));
 245             // layout tick line
 246             if (showTickMarks) {
 247                 tickLine.setLayoutX(trackLeft+trackWidth+trackToTickGap);
 248                 tickLine.setLayoutY(trackStart);
 249                 tickLine.resize(tickLineWidth, trackLength);
 250                 tickLine.requestAxisLayout();
 251             } else {
 252                 if (tickLine != null) {
 253                     tickLine.resize(0,0);
 254                     tickLine.requestAxisLayout();
 255                 }
 256                 tickLine = null;
 257             }
 258         }
 259     }
 260 
 261     /** {@inheritDoc} */
 262     @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 263         final Slider s = getSkinnable();
 264         if (s.getOrientation() == Orientation.HORIZONTAL) {
 265             return (leftInset + minTrackLength() + thumb.minWidth(-1) + rightInset);
 266         } else {
 267             return(leftInset + thumb.prefWidth(-1) + rightInset);
 268         }
 269     }
 270 
 271     /** {@inheritDoc} */
 272     @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 273         final Slider s = getSkinnable();
 274         if (s.getOrientation() == Orientation.HORIZONTAL) {
 275             double axisHeight = showTickMarks ? (tickLine.prefHeight(-1) + trackToTickGap) : 0;
 276             return topInset + thumb.prefHeight(-1) + axisHeight + bottomInset;
 277         } else {
 278             return topInset + minTrackLength() + thumb.prefHeight(-1) + bottomInset;
 279         }
 280     }
 281 
 282     /** {@inheritDoc} */
 283     @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 284         final Slider s = getSkinnable();
 285         if (s.getOrientation() == Orientation.HORIZONTAL) {
 286             if(showTickMarks) {
 287                 return Math.max(140, tickLine.prefWidth(-1));
 288             } else {
 289                 return 140;
 290             }
 291         } else {
 292             double axisWidth = showTickMarks ? (tickLine.prefWidth(-1) + trackToTickGap) : 0;
 293             return leftInset + Math.max(thumb.prefWidth(-1), track.prefWidth(-1)) + axisWidth + rightInset;
 294         }
 295     }
 296 
 297     /** {@inheritDoc} */
 298     @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 299         final Slider s = getSkinnable();
 300         if (s.getOrientation() == Orientation.HORIZONTAL) {
 301             return topInset + Math.max(thumb.prefHeight(-1), track.prefHeight(-1)) +
 302                     ((showTickMarks) ? (trackToTickGap+tickLine.prefHeight(-1)) : 0)  + bottomInset;
 303         } else {
 304             if(showTickMarks) {
 305                 return Math.max(140, tickLine.prefHeight(-1));
 306             } else {
 307                 return 140;
 308             }
 309         }
 310     }
 311 
 312     /** {@inheritDoc} */
 313     @Override protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 314         if (getSkinnable().getOrientation() == Orientation.HORIZONTAL) {
 315             return Double.MAX_VALUE;
 316         } else {
 317             return getSkinnable().prefWidth(-1);
 318         }
 319     }
 320 
 321     /** {@inheritDoc} */
 322     @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 323         if (getSkinnable().getOrientation() == Orientation.HORIZONTAL) {
 324             return getSkinnable().prefHeight(width);
 325         } else {
 326             return Double.MAX_VALUE;
 327         }
 328     }
 329 
 330 
 331 
 332     /***************************************************************************
 333      *                                                                         *
 334      * Private implementation                                                  *
 335      *                                                                         *
 336      **************************************************************************/
 337 
 338     private void initialize() {
 339         thumb = new StackPane() {
 340             @Override
 341             public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
 342                 switch (attribute) {
 343                     case VALUE: return getSkinnable().getValue();
 344                     default: return super.queryAccessibleAttribute(attribute, parameters);
 345                 }
 346             }
 347         };
 348         thumb.getStyleClass().setAll("thumb");
 349         thumb.setAccessibleRole(AccessibleRole.THUMB);
 350         track = new StackPane();
 351         track.getStyleClass().setAll("track");
 352 //        horizontal = getSkinnable().isVertical();
 353 
 354         getChildren().clear();
 355         getChildren().addAll(track, thumb);
 356         setShowTickMarks(getSkinnable().isShowTickMarks(), getSkinnable().isShowTickLabels());
 357         track.setOnMousePressed(me -> {
 358             if (!thumb.isPressed()) {
 359                 trackClicked = true;
 360                 if (getSkinnable().getOrientation() == Orientation.HORIZONTAL) {
 361                     behavior.trackPress(me, (me.getX() / trackLength));
 362                 } else {
 363                     behavior.trackPress(me, (me.getY() / trackLength));
 364                 }
 365                 trackClicked = false;
 366             }
 367         });
 368 
 369         track.setOnMouseDragged(me -> {
 370             if (!thumb.isPressed()) {
 371                 if (getSkinnable().getOrientation() == Orientation.HORIZONTAL) {
 372                     behavior.trackPress(me, (me.getX() / trackLength));
 373                 } else {
 374                     behavior.trackPress(me, (me.getY() / trackLength));
 375                 }
 376             }
 377         });
 378 
 379         thumb.setOnMousePressed(me -> {
 380             behavior.thumbPressed(me, 0.0f);
 381             dragStart = thumb.localToParent(me.getX(), me.getY());
 382             preDragThumbPos = (getSkinnable().getValue() - getSkinnable().getMin()) /
 383                     (getSkinnable().getMax() - getSkinnable().getMin());
 384         });
 385 
 386         thumb.setOnMouseReleased(me -> {
 387             behavior.thumbReleased(me);
 388         });
 389 
 390         thumb.setOnMouseDragged(me -> {
 391             Point2D cur = thumb.localToParent(me.getX(), me.getY());
 392             double dragPos = (getSkinnable().getOrientation() == Orientation.HORIZONTAL) ?
 393                     cur.getX() - dragStart.getX() : -(cur.getY() - dragStart.getY());
 394             behavior.thumbDragged(me, preDragThumbPos + dragPos / trackLength);
 395         });
 396     }
 397 
 398     private void setShowTickMarks(boolean ticksVisible, boolean labelsVisible) {
 399         showTickMarks = (ticksVisible || labelsVisible);
 400         Slider slider = getSkinnable();
 401         if (showTickMarks) {
 402             if (tickLine == null) {
 403                 tickLine = new NumberAxis();
 404                 tickLine.setAutoRanging(false);
 405                 tickLine.setSide(slider.getOrientation() == Orientation.VERTICAL ? Side.RIGHT : (slider.getOrientation() == null) ? Side.RIGHT: Side.BOTTOM);
 406                 tickLine.setUpperBound(slider.getMax());
 407                 tickLine.setLowerBound(slider.getMin());
 408                 tickLine.setTickUnit(slider.getMajorTickUnit());
 409                 tickLine.setTickMarkVisible(ticksVisible);
 410                 tickLine.setTickLabelsVisible(labelsVisible);
 411                 tickLine.setMinorTickVisible(ticksVisible);
 412                 // add 1 to the slider minor tick count since the axis draws one
 413                 // less minor ticks than the number given.
 414                 tickLine.setMinorTickCount(Math.max(slider.getMinorTickCount(),0) + 1);
 415                 if (slider.getLabelFormatter() != null) {
 416                     tickLine.setTickLabelFormatter(stringConverterWrapper);
 417                 }
 418                 getChildren().clear();
 419                 getChildren().addAll(tickLine, track, thumb);
 420             } else {
 421                 tickLine.setTickLabelsVisible(labelsVisible);
 422                 tickLine.setTickMarkVisible(ticksVisible);
 423                 tickLine.setMinorTickVisible(ticksVisible);
 424             }
 425         }
 426         else  {
 427             getChildren().clear();
 428             getChildren().addAll(track, thumb);
 429 //            tickLine = null;
 430         }
 431 
 432         getSkinnable().requestLayout();
 433     }
 434 
 435     /**
 436      * Called when ever either min, max or value changes, so thumb's layoutX, Y is recomputed.
 437      */
 438     void positionThumb(final boolean animate) {
 439         Slider s = getSkinnable();
 440         if (s.getValue() > s.getMax()) return;// this can happen if we are bound to something
 441         boolean horizontal = s.getOrientation() == Orientation.HORIZONTAL;
 442         final double endX = (horizontal) ? trackStart + (((trackLength * ((s.getValue() - s.getMin()) /
 443                 (s.getMax() - s.getMin()))) - thumbWidth/2)) : thumbLeft;
 444         final double endY = (horizontal) ? thumbTop :
 445             snappedTopInset() + trackLength - (trackLength * ((s.getValue() - s.getMin()) /
 446                 (s.getMax() - s.getMin()))); //  - thumbHeight/2
 447 
 448         if (animate) {
 449             // lets animate the thumb transition
 450             final double startX = thumb.getLayoutX();
 451             final double startY = thumb.getLayoutY();
 452             Transition transition = new Transition() {
 453                 {
 454                     setCycleDuration(Duration.millis(200));
 455                 }
 456 
 457                 @Override protected void interpolate(double frac) {
 458                     if (!Double.isNaN(startX)) {
 459                         thumb.setLayoutX(startX + frac * (endX - startX));
 460                     }
 461                     if (!Double.isNaN(startY)) {
 462                         thumb.setLayoutY(startY + frac * (endY - startY));
 463                     }
 464                 }
 465             };
 466             transition.play();
 467         } else {
 468             thumb.setLayoutX(endX);
 469             thumb.setLayoutY(endY);
 470         }
 471     }
 472 
 473     double minTrackLength() {
 474         return 2*thumb.prefWidth(-1);
 475     }
 476 }
 477