1 /*
   2  * Copyright (c) 2010, 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.sun.javafx.scene.control.skin;
  27 
  28 import javafx.animation.Transition;
  29 import javafx.geometry.Orientation;
  30 import javafx.geometry.Point2D;
  31 import javafx.geometry.Side;
  32 import javafx.scene.AccessibleAttribute;
  33 import javafx.scene.AccessibleRole;
  34 import javafx.scene.chart.NumberAxis;
  35 import javafx.scene.control.Slider;
  36 import javafx.scene.layout.StackPane;
  37 import javafx.util.Duration;
  38 import javafx.util.StringConverter;
  39 
  40 import com.sun.javafx.scene.control.behavior.SliderBehavior;
  41 
  42 /**
  43  * Region/css based skin for Slider
  44 */
  45 public class SliderSkin extends BehaviorSkinBase<Slider, SliderBehavior> {
  46 
  47     /** Track if slider is vertical/horizontal and cause re layout */
  48 //    private boolean horizontal;
  49     private NumberAxis tickLine = null;
  50     private double trackToTickGap = 2;
  51 
  52     private boolean showTickMarks;
  53     private double thumbWidth;
  54     private double thumbHeight;
  55 
  56     private double trackStart;
  57     private double trackLength;
  58     private double thumbTop;
  59     private double thumbLeft;
  60     private double preDragThumbPos;
  61     private Point2D dragStart; // in skin coordinates
  62 
  63     private StackPane thumb;
  64     private StackPane track;
  65     private boolean trackClicked = false;
  66 //    private double visibleAmount = 16;
  67 
  68     public SliderSkin(Slider slider) {
  69         super(slider, new SliderBehavior(slider));
  70 
  71         initialize();
  72         slider.requestLayout();
  73         registerChangeListener(slider.minProperty(), "MIN");
  74         registerChangeListener(slider.maxProperty(), "MAX");
  75         registerChangeListener(slider.valueProperty(), "VALUE");
  76         registerChangeListener(slider.orientationProperty(), "ORIENTATION");
  77         registerChangeListener(slider.showTickMarksProperty(), "SHOW_TICK_MARKS");
  78         registerChangeListener(slider.showTickLabelsProperty(), "SHOW_TICK_LABELS");
  79         registerChangeListener(slider.majorTickUnitProperty(), "MAJOR_TICK_UNIT");
  80         registerChangeListener(slider.minorTickCountProperty(), "MINOR_TICK_COUNT");
  81         registerChangeListener(slider.labelFormatterProperty(), "TICK_LABEL_FORMATTER");
  82     }
  83 
  84     private void initialize() {
  85         thumb = new StackPane() {
  86             @Override
  87             public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
  88                 switch (attribute) {
  89                     case VALUE: return getSkinnable().getValue();
  90                     default: return super.queryAccessibleAttribute(attribute, parameters);
  91                 }
  92             }
  93         };
  94         thumb.getStyleClass().setAll("thumb");
  95         thumb.setAccessibleRole(AccessibleRole.THUMB);
  96         track = new StackPane();
  97         track.getStyleClass().setAll("track");
  98 //        horizontal = getSkinnable().isVertical();
  99 
 100         getChildren().clear();
 101         getChildren().addAll(track, thumb);
 102         setShowTickMarks(getSkinnable().isShowTickMarks(), getSkinnable().isShowTickLabels());
 103         track.setOnMousePressed(me -> {
 104             if (!thumb.isPressed()) {
 105                 trackClicked = true;
 106                 if (getSkinnable().getOrientation() == Orientation.HORIZONTAL) {
 107                     getBehavior().trackPress(me, (me.getX() / trackLength));
 108                 } else {
 109                     getBehavior().trackPress(me, (me.getY() / trackLength));
 110                 }
 111                 trackClicked = false;
 112             }
 113         });
 114         
 115         track.setOnMouseDragged(me -> {
 116             if (!thumb.isPressed()) {
 117                 if (getSkinnable().getOrientation() == Orientation.HORIZONTAL) {
 118                     getBehavior().trackPress(me, (me.getX() / trackLength));
 119                 } else {
 120                     getBehavior().trackPress(me, (me.getY() / trackLength));
 121                 }
 122             }
 123         });
 124 
 125         thumb.setOnMousePressed(me -> {
 126             getBehavior().thumbPressed(me, 0.0f);
 127             dragStart = thumb.localToParent(me.getX(), me.getY());
 128             preDragThumbPos = (getSkinnable().getValue() - getSkinnable().getMin()) /
 129                     (getSkinnable().getMax() - getSkinnable().getMin());
 130         });
 131 
 132         thumb.setOnMouseReleased(me -> {
 133             getBehavior().thumbReleased(me);
 134         });
 135 
 136         thumb.setOnMouseDragged(me -> {
 137             Point2D cur = thumb.localToParent(me.getX(), me.getY());
 138             double dragPos = (getSkinnable().getOrientation() == Orientation.HORIZONTAL)?
 139                 cur.getX() - dragStart.getX() : -(cur.getY() - dragStart.getY());
 140             getBehavior().thumbDragged(me, preDragThumbPos + dragPos / trackLength);
 141         });
 142     }
 143 
 144     StringConverter<Number> stringConverterWrapper = new StringConverter<Number>() {
 145         Slider slider = getSkinnable();
 146         @Override public String toString(Number object) {
 147             return(object != null) ? slider.getLabelFormatter().toString(object.doubleValue()) : "";
 148         }
 149         @Override public Number fromString(String string) {
 150             return slider.getLabelFormatter().fromString(string);
 151         }
 152     };
 153     
 154      private void setShowTickMarks(boolean ticksVisible, boolean labelsVisible) {
 155         showTickMarks = (ticksVisible || labelsVisible);
 156         Slider slider = getSkinnable();
 157         if (showTickMarks) {
 158             if (tickLine == null) {
 159                 tickLine = new NumberAxis();
 160                 tickLine.setAutoRanging(false);
 161                 tickLine.setSide(slider.getOrientation() == Orientation.VERTICAL ? Side.RIGHT : (slider.getOrientation() == null) ? Side.RIGHT: Side.BOTTOM);
 162                 tickLine.setUpperBound(slider.getMax());
 163                 tickLine.setLowerBound(slider.getMin());
 164                 tickLine.setTickUnit(slider.getMajorTickUnit());
 165                 tickLine.setTickMarkVisible(ticksVisible);
 166                 tickLine.setTickLabelsVisible(labelsVisible);
 167                 tickLine.setMinorTickVisible(ticksVisible);
 168                 // add 1 to the slider minor tick count since the axis draws one
 169                 // less minor ticks than the number given.
 170                 tickLine.setMinorTickCount(Math.max(slider.getMinorTickCount(),0) + 1);
 171                 if (slider.getLabelFormatter() != null) {
 172                     tickLine.setTickLabelFormatter(stringConverterWrapper);
 173                 }
 174                 getChildren().clear();
 175                 getChildren().addAll(tickLine, track, thumb);
 176             } else {
 177                 tickLine.setTickLabelsVisible(labelsVisible);
 178                 tickLine.setTickMarkVisible(ticksVisible);
 179                 tickLine.setMinorTickVisible(ticksVisible);
 180             }
 181         } 
 182         else  {
 183             getChildren().clear();
 184             getChildren().addAll(track, thumb);
 185 //            tickLine = null;
 186         }
 187 
 188         getSkinnable().requestLayout();
 189     }
 190 
 191     @Override protected void handleControlPropertyChanged(String p) {
 192         super.handleControlPropertyChanged(p);
 193         Slider slider = getSkinnable();
 194         if ("ORIENTATION".equals(p)) {
 195             if (showTickMarks && tickLine != null) {
 196                 tickLine.setSide(slider.getOrientation() == Orientation.VERTICAL ? Side.RIGHT : (slider.getOrientation() == null) ? Side.RIGHT: Side.BOTTOM);
 197             }
 198             getSkinnable().requestLayout();
 199         } else if ("VALUE".equals(p)) {
 200             // only animate thumb if the track was clicked - not if the thumb is dragged
 201             positionThumb(trackClicked);
 202         } else if ("MIN".equals(p) ) {
 203             if (showTickMarks && tickLine != null) {
 204                 tickLine.setLowerBound(slider.getMin());
 205             }
 206             getSkinnable().requestLayout();
 207         } else if ("MAX".equals(p)) {
 208             if (showTickMarks && tickLine != null) {
 209                 tickLine.setUpperBound(slider.getMax());
 210             }
 211             getSkinnable().requestLayout();
 212         } else if ("SHOW_TICK_MARKS".equals(p) || "SHOW_TICK_LABELS".equals(p)) {
 213             setShowTickMarks(slider.isShowTickMarks(), slider.isShowTickLabels());
 214         }  else if ("MAJOR_TICK_UNIT".equals(p)) {
 215             if (tickLine != null) {
 216                 tickLine.setTickUnit(slider.getMajorTickUnit());
 217                 getSkinnable().requestLayout();
 218             }
 219         } else if ("MINOR_TICK_COUNT".equals(p)) {
 220             if (tickLine != null) {
 221                 tickLine.setMinorTickCount(Math.max(slider.getMinorTickCount(),0) + 1);
 222                 getSkinnable().requestLayout();
 223             }
 224         } else if ("TICK_LABEL_FORMATTER".equals(p)) {
 225             if (tickLine != null) {
 226                 if (slider.getLabelFormatter() == null) {
 227                     tickLine.setTickLabelFormatter(null);
 228                 } else {
 229                     tickLine.setTickLabelFormatter(stringConverterWrapper);
 230                     tickLine.requestAxisLayout();
 231                 }
 232             }
 233         }
 234     }
 235 
 236     /**
 237      * Called when ever either min, max or value changes, so thumb's layoutX, Y is recomputed.
 238      */
 239     void positionThumb(final boolean animate) {
 240         Slider s = getSkinnable();
 241         if (s.getValue() > s.getMax()) return;// this can happen if we are bound to something 
 242         boolean horizontal = s.getOrientation() == Orientation.HORIZONTAL;
 243         final double endX = (horizontal) ? trackStart + (((trackLength * ((s.getValue() - s.getMin()) /
 244                 (s.getMax() - s.getMin()))) - thumbWidth/2)) : thumbLeft;
 245         final double endY = (horizontal) ? thumbTop :
 246             snappedTopInset() + trackLength - (trackLength * ((s.getValue() - s.getMin()) /
 247                 (s.getMax() - s.getMin()))); //  - thumbHeight/2
 248         
 249         if (animate) {
 250             // lets animate the thumb transition
 251             final double startX = thumb.getLayoutX();
 252             final double startY = thumb.getLayoutY();
 253             Transition transition = new Transition() {
 254                 {
 255                     setCycleDuration(Duration.millis(200));
 256                 }
 257 
 258                 @Override protected void interpolate(double frac) {
 259                     if (!Double.isNaN(startX)) {
 260                         thumb.setLayoutX(startX + frac * (endX - startX));
 261                     }
 262                     if (!Double.isNaN(startY)) {
 263                         thumb.setLayoutY(startY + frac * (endY - startY));
 264                     }
 265                 }
 266             };
 267             transition.play();
 268         } else {
 269             thumb.setLayoutX(endX);
 270             thumb.setLayoutY(endY);
 271         }
 272     }
 273 
 274     @Override protected void layoutChildren(final double x, final double y,
 275             final double w, final double h) {
 276          // calculate the available space
 277         // resize thumb to preferred size
 278         thumbWidth = snapSize(thumb.prefWidth(-1));
 279         thumbHeight = snapSize(thumb.prefHeight(-1));
 280         thumb.resize(thumbWidth, thumbHeight);
 281         // we are assuming the is common radius's for all corners on the track
 282         double trackRadius = track.getBackground() == null ? 0 : track.getBackground().getFills().size() > 0 ?
 283                 track.getBackground().getFills().get(0).getRadii().getTopLeftHorizontalRadius() : 0;
 284 
 285         if (getSkinnable().getOrientation() == Orientation.HORIZONTAL) {
 286             double tickLineHeight =  (showTickMarks) ? tickLine.prefHeight(-1) : 0;
 287             double trackHeight = snapSize(track.prefHeight(-1));
 288             double trackAreaHeight = Math.max(trackHeight,thumbHeight);
 289             double totalHeightNeeded = trackAreaHeight  + ((showTickMarks) ? trackToTickGap+tickLineHeight : 0);
 290             double startY = y + ((h - totalHeightNeeded)/2); // center slider in available height vertically
 291             trackLength = snapSize(w - thumbWidth);
 292             trackStart = snapPosition(x + (thumbWidth/2));
 293             double trackTop = (int)(startY + ((trackAreaHeight-trackHeight)/2));
 294             thumbTop = (int)(startY + ((trackAreaHeight-thumbHeight)/2));
 295 
 296             positionThumb(false);
 297             // layout track
 298             track.resizeRelocate((int)(trackStart - trackRadius),
 299                                  trackTop ,
 300                                  (int)(trackLength + trackRadius + trackRadius),
 301                                  trackHeight);
 302             // layout tick line
 303             if (showTickMarks) {
 304                 tickLine.setLayoutX(trackStart);
 305                 tickLine.setLayoutY(trackTop+trackHeight+trackToTickGap);
 306                 tickLine.resize(trackLength, tickLineHeight);
 307                 tickLine.requestAxisLayout();
 308             } else {
 309                 if (tickLine != null) {
 310                     tickLine.resize(0,0);
 311                     tickLine.requestAxisLayout();
 312                 }
 313                 tickLine = null;
 314             }
 315         } else {
 316             double tickLineWidth = (showTickMarks) ? tickLine.prefWidth(-1) : 0;
 317             double trackWidth = snapSize(track.prefWidth(-1));
 318             double trackAreaWidth = Math.max(trackWidth,thumbWidth);
 319             double totalWidthNeeded = trackAreaWidth  + ((showTickMarks) ? trackToTickGap+tickLineWidth : 0) ;
 320             double startX = x + ((w - totalWidthNeeded)/2); // center slider in available width horizontally
 321             trackLength = snapSize(h - thumbHeight);
 322             trackStart = snapPosition(y + (thumbHeight/2));
 323             double trackLeft = (int)(startX + ((trackAreaWidth-trackWidth)/2));
 324             thumbLeft = (int)(startX + ((trackAreaWidth-thumbWidth)/2));
 325 
 326             positionThumb(false);
 327             // layout track
 328             track.resizeRelocate(trackLeft,
 329                                  (int)(trackStart - trackRadius),
 330                                  trackWidth,
 331                                  (int)(trackLength + trackRadius + trackRadius));
 332             // layout tick line
 333             if (showTickMarks) {
 334                 tickLine.setLayoutX(trackLeft+trackWidth+trackToTickGap);
 335                 tickLine.setLayoutY(trackStart);
 336                 tickLine.resize(tickLineWidth, trackLength);
 337                 tickLine.requestAxisLayout();
 338             } else {
 339                 if (tickLine != null) {
 340                     tickLine.resize(0,0);
 341                     tickLine.requestAxisLayout();
 342                 }
 343                 tickLine = null;
 344             }
 345         }
 346     }
 347 
 348     double minTrackLength() {
 349         return 2*thumb.prefWidth(-1);
 350     }
 351 
 352     @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 353         final Slider s = getSkinnable();
 354         if (s.getOrientation() == Orientation.HORIZONTAL) {
 355             return (leftInset + minTrackLength() + thumb.minWidth(-1) + rightInset);
 356         } else {
 357             return(leftInset + thumb.prefWidth(-1) + rightInset);
 358         }
 359     }
 360 
 361     @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 362         final Slider s = getSkinnable();
 363         if (s.getOrientation() == Orientation.HORIZONTAL) {
 364             double axisHeight = showTickMarks ? (tickLine.prefHeight(-1) + trackToTickGap) : 0;
 365             return topInset + thumb.prefHeight(-1) + axisHeight + bottomInset;
 366         } else {
 367             return topInset + minTrackLength() + thumb.prefHeight(-1) + bottomInset;
 368         }
 369     }
 370 
 371     @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 372         final Slider s = getSkinnable();
 373         if (s.getOrientation() == Orientation.HORIZONTAL) {
 374             if(showTickMarks) {
 375                 return Math.max(140, tickLine.prefWidth(-1));
 376             } else {
 377                 return 140;
 378             }
 379         } else {
 380             double axisWidth = showTickMarks ? (tickLine.prefWidth(-1) + trackToTickGap) : 0;
 381             return leftInset + Math.max(thumb.prefWidth(-1), track.prefWidth(-1)) + axisWidth + rightInset;
 382         }
 383     }
 384 
 385     @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 386         final Slider s = getSkinnable();
 387         if (s.getOrientation() == Orientation.HORIZONTAL) {
 388             return topInset + Math.max(thumb.prefHeight(-1), track.prefHeight(-1)) +
 389              ((showTickMarks) ? (trackToTickGap+tickLine.prefHeight(-1)) : 0)  + bottomInset;
 390         } else {
 391             if(showTickMarks) {
 392                 return Math.max(140, tickLine.prefHeight(-1));
 393             } else {
 394                 return 140;
 395             }
 396         }
 397     }
 398 
 399     @Override protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 400         if (getSkinnable().getOrientation() == Orientation.HORIZONTAL) {
 401             return Double.MAX_VALUE;
 402         } else {
 403             return getSkinnable().prefWidth(-1);
 404         }
 405     }
 406 
 407     @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 408         if (getSkinnable().getOrientation() == Orientation.HORIZONTAL) {
 409             return getSkinnable().prefHeight(width);
 410         } else {
 411             return Double.MAX_VALUE;
 412         }
 413     }
 414 }
 415