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