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