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