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.NodeHelper; 29 import com.sun.javafx.scene.control.skin.Utils; 30 import java.util.ArrayList; 31 import java.util.Collections; 32 import java.util.List; 33 34 import javafx.animation.Animation; 35 import javafx.animation.KeyFrame; 36 import javafx.animation.KeyValue; 37 import javafx.animation.Timeline; 38 import javafx.beans.property.BooleanProperty; 39 import javafx.beans.property.IntegerProperty; 40 import javafx.beans.property.ObjectProperty; 41 import javafx.beans.value.WritableValue; 42 import javafx.collections.FXCollections; 43 import javafx.collections.ObservableList; 44 import javafx.geometry.NodeOrientation; 45 import javafx.geometry.VPos; 46 import javafx.scene.Node; 47 import javafx.scene.control.Control; 48 import javafx.scene.control.ProgressIndicator; 49 import javafx.scene.control.SkinBase; 50 import javafx.scene.layout.Pane; 51 import javafx.scene.layout.Region; 52 import javafx.scene.layout.StackPane; 53 import javafx.scene.paint.Color; 54 import javafx.scene.paint.Paint; 55 import javafx.scene.shape.Arc; 56 import javafx.scene.shape.ArcType; 57 import javafx.scene.shape.Circle; 58 import javafx.scene.text.Text; 59 import javafx.scene.text.TextBoundsType; 60 import javafx.scene.transform.Scale; 61 import javafx.util.Duration; 62 import javafx.css.CssMetaData; 63 import javafx.css.StyleableObjectProperty; 64 import javafx.css.StyleableProperty; 65 import javafx.css.StyleableBooleanProperty; 66 import javafx.css.StyleableIntegerProperty; 67 import javafx.css.converter.BooleanConverter; 68 import javafx.css.converter.PaintConverter; 69 import javafx.css.converter.SizeConverter; 70 import com.sun.javafx.scene.control.skin.resources.ControlResources; 71 import javafx.css.Styleable; 72 73 /** 74 * Default skin implementation for the {@link ProgressIndicator} control. 75 * 76 * @see ProgressIndicator 77 * @since 9 78 */ 79 public class ProgressIndicatorSkin extends SkinBase<ProgressIndicator> { 80 81 /*************************************************************************** 82 * * 83 * Static fields * 84 * * 85 **************************************************************************/ 86 87 88 89 /*************************************************************************** 90 * * 91 * Private fields * 92 * * 93 **************************************************************************/ 94 95 // JDK-8149818: This constant should not be static, because the 96 // Locale may change between instances. 97 98 /** DONE string is just used to know the size of Done as that is the biggest text we need to allow for */ 99 private final String DONE = ControlResources.getString("ProgressIndicator.doneString"); 100 101 final Duration CLIPPED_DELAY = new Duration(300); 102 final Duration UNCLIPPED_DELAY = new Duration(0); 103 104 private IndeterminateSpinner spinner; 105 private DeterminateIndicator determinateIndicator; 106 private ProgressIndicator control; 107 108 Animation indeterminateTransition; 109 110 111 112 /*************************************************************************** 113 * * 114 * Constructors * 115 * * 116 **************************************************************************/ 117 118 /** 119 * Creates a new ProgressIndicatorSkin instance, installing the necessary child 120 * nodes into the Control {@link Control#getChildren() children} list. 121 * 122 * @param control The control that this skin should be installed onto. 123 */ 124 public ProgressIndicatorSkin(ProgressIndicator control) { 125 super(control); 126 127 this.control = control; 128 129 // register listeners 130 registerChangeListener(control.indeterminateProperty(), e -> initialize()); 131 registerChangeListener(control.progressProperty(), e -> updateProgress()); 132 registerChangeListener(NodeHelper.treeShowingProperty(control), e -> updateAnimation()); 133 134 initialize(); 135 updateAnimation(); 136 } 137 138 139 140 /*************************************************************************** 141 * * 142 * Properties * 143 * * 144 **************************************************************************/ 145 146 /** 147 * The colour of the progress segment. 148 */ 149 private ObjectProperty<Paint> progressColor = new StyleableObjectProperty<Paint>(null) { 150 @Override protected void invalidated() { 151 final Paint value = get(); 152 if (value != null && !(value instanceof Color)) { 153 if (isBound()) { 154 unbind(); 155 } 156 set(null); 157 throw new IllegalArgumentException("Only Color objects are supported"); 158 } 159 if (spinner!=null) spinner.setFillOverride(value); 160 if (determinateIndicator!=null) determinateIndicator.setFillOverride(value); 161 } 162 163 @Override public Object getBean() { 164 return ProgressIndicatorSkin.this; 165 } 166 167 @Override public String getName() { 168 return "progressColorProperty"; 169 } 170 171 @Override public CssMetaData<ProgressIndicator,Paint> getCssMetaData() { 172 return PROGRESS_COLOR; 173 } 174 }; 175 176 Paint getProgressColor() { 177 return progressColor.get(); 178 } 179 180 /** 181 * The number of segments in the spinner. 182 */ 183 private IntegerProperty indeterminateSegmentCount = new StyleableIntegerProperty(8) { 184 @Override protected void invalidated() { 185 if (spinner!=null) spinner.rebuild(); 186 } 187 188 @Override public Object getBean() { 189 return ProgressIndicatorSkin.this; 190 } 191 192 @Override public String getName() { 193 return "indeterminateSegmentCount"; 194 } 195 196 @Override public CssMetaData<ProgressIndicator,Number> getCssMetaData() { 197 return INDETERMINATE_SEGMENT_COUNT; 198 } 199 }; 200 201 /** 202 * True if the progress indicator should rotate as well as animate opacity. 203 */ 204 private final BooleanProperty spinEnabled = new StyleableBooleanProperty(false) { 205 @Override protected void invalidated() { 206 if (spinner!=null) spinner.setSpinEnabled(get()); 207 } 208 209 @Override public CssMetaData<ProgressIndicator,Boolean> getCssMetaData() { 210 return SPIN_ENABLED; 211 } 212 213 @Override public Object getBean() { 214 return ProgressIndicatorSkin.this; 215 } 216 217 @Override public String getName() { 218 return "spinEnabled"; 219 } 220 }; 221 222 223 224 /*************************************************************************** 225 * * 226 * Public API * 227 * * 228 **************************************************************************/ 229 230 /** {@inheritDoc} */ 231 @Override public void dispose() { 232 super.dispose(); 233 234 if (indeterminateTransition != null) { 235 indeterminateTransition.stop(); 236 indeterminateTransition = null; 237 } 238 239 if (spinner != null) { 240 spinner = null; 241 } 242 243 control = null; 244 } 245 246 /** {@inheritDoc} */ 247 @Override protected void layoutChildren(final double x, final double y, 248 final double w, final double h) { 249 if (spinner != null && control.isIndeterminate()) { 250 spinner.layoutChildren(); 251 spinner.resizeRelocate(0, 0, w, h); 252 } else if (determinateIndicator != null) { 253 determinateIndicator.layoutChildren(); 254 determinateIndicator.resizeRelocate(0, 0, w, h); 255 } 256 } 257 258 /** {@inheritDoc} */ 259 @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 260 double minWidth = 0.0; 261 262 if (spinner != null && control.isIndeterminate()) { 263 minWidth = spinner.minWidth(-1); 264 } else if (determinateIndicator != null) { 265 minWidth = determinateIndicator.minWidth(-1); 266 } 267 return minWidth; 268 } 269 270 /** {@inheritDoc} */ 271 @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 272 double minHeight = 0.0; 273 274 if (spinner != null && control.isIndeterminate()) { 275 minHeight = spinner.minHeight(-1); 276 } else if (determinateIndicator != null) { 277 minHeight = determinateIndicator.minHeight(-1); 278 } 279 return minHeight; 280 } 281 282 /** {@inheritDoc} */ 283 @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 284 double prefWidth = 0.0; 285 286 if (spinner != null && control.isIndeterminate()) { 287 prefWidth = spinner.prefWidth(height); 288 } else if (determinateIndicator != null) { 289 prefWidth = determinateIndicator.prefWidth(height); 290 } 291 return prefWidth; 292 } 293 294 /** {@inheritDoc} */ 295 @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 296 double prefHeight = 0.0; 297 298 if (spinner != null && control.isIndeterminate()) { 299 prefHeight = spinner.prefHeight(width); 300 } else if (determinateIndicator != null) { 301 prefHeight = determinateIndicator.prefHeight(width); 302 } 303 return prefHeight; 304 } 305 306 /** {@inheritDoc} */ 307 @Override protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 308 return computePrefWidth(height, topInset, rightInset, bottomInset, leftInset); 309 } 310 311 /** {@inheritDoc} */ 312 @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 313 return computePrefHeight(width, topInset, rightInset, bottomInset, leftInset); 314 } 315 316 317 /*************************************************************************** 318 * * 319 * Private implementation * 320 * * 321 **************************************************************************/ 322 323 void initialize() { 324 boolean isIndeterminate = control.isIndeterminate(); 325 if (isIndeterminate) { 326 // clean up determinateIndicator 327 determinateIndicator = null; 328 329 // create spinner 330 spinner = new IndeterminateSpinner(spinEnabled.get(), progressColor.get()); 331 getChildren().setAll(spinner); 332 if (NodeHelper.isTreeShowing(control)) { 333 if (indeterminateTransition != null) { 334 indeterminateTransition.play(); 335 } 336 } 337 } else { 338 // clean up after spinner 339 if (spinner != null) { 340 if (indeterminateTransition != null) { 341 indeterminateTransition.stop(); 342 } 343 spinner = null; 344 } 345 346 // create determinateIndicator 347 determinateIndicator = new DeterminateIndicator(control, this, progressColor.get()); 348 getChildren().setAll(determinateIndicator); 349 } 350 } 351 352 void updateProgress() { 353 if (determinateIndicator != null) { 354 determinateIndicator.updateProgress(control.getProgress()); 355 } 356 } 357 358 void createIndeterminateTimeline() { 359 if (spinner != null) { 360 spinner.rebuildTimeline(); 361 } 362 } 363 364 void pauseTimeline(boolean pause) { 365 if (getSkinnable().isIndeterminate()) { 366 if (indeterminateTransition == null) { 367 createIndeterminateTimeline(); 368 } 369 if (pause) { 370 indeterminateTransition.pause(); 371 } else { 372 indeterminateTransition.play(); 373 } 374 } 375 } 376 377 void updateAnimation() { 378 ProgressIndicator control = getSkinnable(); 379 final boolean isTreeShowing = NodeHelper.isTreeShowing(control); 380 if (indeterminateTransition != null) { 381 pauseTimeline(!isTreeShowing); 382 } else if (isTreeShowing) { 383 createIndeterminateTimeline(); 384 } 385 } 386 387 388 389 /*************************************************************************** 390 * * 391 * Stylesheet Handling * 392 * * 393 **************************************************************************/ 394 395 private static final CssMetaData<ProgressIndicator,Paint> PROGRESS_COLOR = 396 new CssMetaData<ProgressIndicator,Paint>("-fx-progress-color", 397 PaintConverter.getInstance(), null) { 398 399 @Override 400 public boolean isSettable(ProgressIndicator n) { 401 final ProgressIndicatorSkin skin = (ProgressIndicatorSkin) n.getSkin(); 402 return skin.progressColor == null || 403 !skin.progressColor.isBound(); 404 } 405 406 @Override 407 public StyleableProperty<Paint> getStyleableProperty(ProgressIndicator n) { 408 final ProgressIndicatorSkin skin = (ProgressIndicatorSkin) n.getSkin(); 409 return (StyleableProperty<Paint>)(WritableValue<Paint>)skin.progressColor; 410 } 411 }; 412 private static final CssMetaData<ProgressIndicator,Number> INDETERMINATE_SEGMENT_COUNT = 413 new CssMetaData<ProgressIndicator,Number>("-fx-indeterminate-segment-count", 414 SizeConverter.getInstance(), 8) { 415 416 @Override public boolean isSettable(ProgressIndicator n) { 417 final ProgressIndicatorSkin skin = (ProgressIndicatorSkin) n.getSkin(); 418 return skin.indeterminateSegmentCount == null || 419 !skin.indeterminateSegmentCount.isBound(); 420 } 421 422 @Override public StyleableProperty<Number> getStyleableProperty(ProgressIndicator n) { 423 final ProgressIndicatorSkin skin = (ProgressIndicatorSkin) n.getSkin(); 424 return (StyleableProperty<Number>)(WritableValue<Number>)skin.indeterminateSegmentCount; 425 } 426 }; 427 private static final CssMetaData<ProgressIndicator,Boolean> SPIN_ENABLED = 428 new CssMetaData<ProgressIndicator,Boolean>("-fx-spin-enabled", BooleanConverter.getInstance(), Boolean.FALSE) { 429 430 @Override public boolean isSettable(ProgressIndicator node) { 431 final ProgressIndicatorSkin skin = (ProgressIndicatorSkin) node.getSkin(); 432 return skin.spinEnabled == null || !skin.spinEnabled.isBound(); 433 } 434 435 @Override public StyleableProperty<Boolean> getStyleableProperty(ProgressIndicator node) { 436 final ProgressIndicatorSkin skin = (ProgressIndicatorSkin) node.getSkin(); 437 return (StyleableProperty<Boolean>)(WritableValue<Boolean>)skin.spinEnabled; 438 } 439 }; 440 441 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; 442 static { 443 final List<CssMetaData<? extends Styleable, ?>> styleables = 444 new ArrayList<CssMetaData<? extends Styleable, ?>>(SkinBase.getClassCssMetaData()); 445 styleables.add(PROGRESS_COLOR); 446 styleables.add(INDETERMINATE_SEGMENT_COUNT); 447 styleables.add(SPIN_ENABLED); 448 STYLEABLES = Collections.unmodifiableList(styleables); 449 } 450 451 /** 452 * Returns the CssMetaData associated with this class, which may include the 453 * CssMetaData of its superclasses. 454 * @return the CssMetaData associated with this class, which may include the 455 * CssMetaData of its superclasses 456 */ 457 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { 458 return STYLEABLES; 459 } 460 461 /** 462 * {@inheritDoc} 463 */ 464 @Override 465 public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() { 466 return getClassCssMetaData(); 467 } 468 469 470 471 /*************************************************************************** 472 * * 473 * Support classes * 474 * * 475 **************************************************************************/ 476 477 private final class DeterminateIndicator extends Region { 478 private double textGap = 2.0F; 479 480 // only update progress text on whole percentages 481 private int intProgress; 482 483 // only update pie arc to nearest degree 484 private int degProgress; 485 private Text text; 486 private StackPane indicator; 487 private StackPane progress; 488 private StackPane tick; 489 private Arc arcShape; 490 private Circle indicatorCircle; 491 private double doneTextWidth; 492 private double doneTextHeight; 493 494 public DeterminateIndicator(ProgressIndicator control, ProgressIndicatorSkin s, Paint fillOverride) { 495 496 getStyleClass().add("determinate-indicator"); 497 498 intProgress = (int) Math.round(control.getProgress() * 100.0) ; 499 degProgress = (int) (360 * control.getProgress()); 500 501 getChildren().clear(); 502 503 text = new Text((control.getProgress() >= 1) ? (DONE) : ("" + intProgress + "%")); 504 text.setTextOrigin(VPos.TOP); 505 text.getStyleClass().setAll("text", "percentage"); 506 507 registerChangeListener(text.fontProperty(), o -> { 508 doneTextWidth = Utils.computeTextWidth(text.getFont(), DONE, 0); 509 doneTextHeight = Utils.computeTextHeight(text.getFont(), DONE, 0, TextBoundsType.LOGICAL_VERTICAL_CENTER); 510 }); 511 512 // The circular background for the progress pie piece 513 indicator = new StackPane(); 514 indicator.setScaleShape(false); 515 indicator.setCenterShape(false); 516 indicator.getStyleClass().setAll("indicator"); 517 indicatorCircle = new Circle(); 518 indicator.setShape(indicatorCircle); 519 520 // The shape for our progress pie piece 521 arcShape = new Arc(); 522 arcShape.setType(ArcType.ROUND); 523 arcShape.setStartAngle(90.0F); 524 525 // Our progress pie piece 526 progress = new StackPane(); 527 progress.getStyleClass().setAll("progress"); 528 progress.setScaleShape(false); 529 progress.setCenterShape(false); 530 progress.setShape(arcShape); 531 progress.getChildren().clear(); 532 setFillOverride(fillOverride); 533 534 // The check mark that's drawn at 100% 535 tick = new StackPane(); 536 tick.getStyleClass().setAll("tick"); 537 538 getChildren().setAll(indicator, progress, text, tick); 539 updateProgress(control.getProgress()); 540 } 541 542 private void setFillOverride(Paint fillOverride) { 543 if (fillOverride instanceof Color) { 544 Color c = (Color)fillOverride; 545 progress.setStyle("-fx-background-color: rgba("+((int)(255*c.getRed()))+","+((int)(255*c.getGreen()))+","+((int)(255*c.getBlue()))+","+c.getOpacity()+");"); 546 } else { 547 progress.setStyle(null); 548 } 549 } 550 551 @Override public boolean usesMirroring() { 552 // This is used instead of setting NodeOrientation, 553 // allowing the Text node to inherit the current 554 // orientation. 555 return false; 556 } 557 558 private void updateProgress(double progress) { 559 intProgress = (int) Math.round(progress * 100.0) ; 560 text.setText((progress >= 1) ? (DONE) : ("" + intProgress + "%")); 561 562 degProgress = (int) (360 * progress); 563 arcShape.setLength(-degProgress); 564 requestLayout(); 565 } 566 567 @Override protected void layoutChildren() { 568 // Position and size the circular background 569 final double left = control.snappedLeftInset(); 570 final double right = control.snappedRightInset(); 571 final double top = control.snappedTopInset(); 572 final double bottom = control.snappedBottomInset(); 573 574 /* 575 ** use the min of width, or height, keep it a circle 576 */ 577 final double areaW = control.getWidth() - left - right; 578 final double areaH = control.getHeight() - top - bottom - textGap - doneTextHeight; 579 final double radiusW = areaW / 2; 580 final double radiusH = areaH / 2; 581 final double radius = Math.floor(Math.min(radiusW, radiusH)); 582 final double centerX = snapPosition(left + radiusW); 583 final double centerY = snapPosition(top + radius); 584 585 // find radius that fits inside radius - insetsPadding 586 final double iLeft = indicator.snappedLeftInset(); 587 final double iRight = indicator.snappedRightInset(); 588 final double iTop = indicator.snappedTopInset(); 589 final double iBottom = indicator.snappedBottomInset(); 590 final double progressRadius = snapSize(Math.min( 591 Math.min(radius - iLeft, radius - iRight), 592 Math.min(radius - iTop, radius - iBottom))); 593 594 indicatorCircle.setRadius(radius); 595 indicator.setLayoutX(centerX); 596 indicator.setLayoutY(centerY); 597 598 arcShape.setRadiusX(progressRadius); 599 arcShape.setRadiusY(progressRadius); 600 progress.setLayoutX(centerX); 601 progress.setLayoutY(centerY); 602 603 // find radius that fits inside progressRadius - progressInsets 604 final double pLeft = progress.snappedLeftInset(); 605 final double pRight = progress.snappedRightInset(); 606 final double pTop = progress.snappedTopInset(); 607 final double pBottom = progress.snappedBottomInset(); 608 final double indicatorRadius = snapSize(Math.min( 609 Math.min(progressRadius - pLeft, progressRadius - pRight), 610 Math.min(progressRadius - pTop, progressRadius - pBottom))); 611 612 // find size of spare box that fits inside indicator radius 613 double squareBoxHalfWidth = Math.ceil(Math.sqrt((indicatorRadius * indicatorRadius) / 2)); 614 615 tick.setLayoutX(centerX - squareBoxHalfWidth); 616 tick.setLayoutY(centerY - squareBoxHalfWidth); 617 tick.resize(squareBoxHalfWidth + squareBoxHalfWidth, squareBoxHalfWidth + squareBoxHalfWidth); 618 tick.setVisible(control.getProgress() >= 1); 619 620 // if the % text can't fit anywhere in the bounds then don't display it 621 double textWidth = text.getLayoutBounds().getWidth(); 622 double textHeight = text.getLayoutBounds().getHeight(); 623 if (control.getWidth() >= textWidth && control.getHeight() >= textHeight) { 624 if (!text.isVisible()) text.setVisible(true); 625 text.setLayoutY(snapPosition(centerY + radius + textGap)); 626 text.setLayoutX(snapPosition(centerX - (textWidth/2))); 627 } else { 628 if (text.isVisible()) text.setVisible(false); 629 } 630 } 631 632 @Override protected double computePrefWidth(double height) { 633 final double left = control.snappedLeftInset(); 634 final double right = control.snappedRightInset(); 635 final double iLeft = indicator.snappedLeftInset(); 636 final double iRight = indicator.snappedRightInset(); 637 final double iTop = indicator.snappedTopInset(); 638 final double iBottom = indicator.snappedBottomInset(); 639 final double indicatorMax = snapSize(Math.max(Math.max(iLeft, iRight), Math.max(iTop, iBottom))); 640 final double pLeft = progress.snappedLeftInset(); 641 final double pRight = progress.snappedRightInset(); 642 final double pTop = progress.snappedTopInset(); 643 final double pBottom = progress.snappedBottomInset(); 644 final double progressMax = snapSize(Math.max(Math.max(pLeft, pRight), Math.max(pTop, pBottom))); 645 final double tLeft = tick.snappedLeftInset(); 646 final double tRight = tick.snappedRightInset(); 647 final double indicatorWidth = indicatorMax + progressMax + tLeft + tRight + progressMax + indicatorMax; 648 return left + Math.max(indicatorWidth, doneTextWidth) + right; 649 } 650 651 @Override protected double computePrefHeight(double width) { 652 final double top = control.snappedTopInset(); 653 final double bottom = control.snappedBottomInset(); 654 final double iLeft = indicator.snappedLeftInset(); 655 final double iRight = indicator.snappedRightInset(); 656 final double iTop = indicator.snappedTopInset(); 657 final double iBottom = indicator.snappedBottomInset(); 658 final double indicatorMax = snapSize(Math.max(Math.max(iLeft, iRight), Math.max(iTop, iBottom))); 659 final double pLeft = progress.snappedLeftInset(); 660 final double pRight = progress.snappedRightInset(); 661 final double pTop = progress.snappedTopInset(); 662 final double pBottom = progress.snappedBottomInset(); 663 final double progressMax = snapSize(Math.max(Math.max(pLeft, pRight), Math.max(pTop, pBottom))); 664 final double tTop = tick.snappedTopInset(); 665 final double tBottom = tick.snappedBottomInset(); 666 final double indicatorHeight = indicatorMax + progressMax + tTop + tBottom + progressMax + indicatorMax; 667 return top + indicatorHeight + textGap + doneTextHeight + bottom; 668 } 669 670 @Override protected double computeMaxWidth(double height) { 671 return computePrefWidth(height); 672 } 673 674 @Override protected double computeMaxHeight(double width) { 675 return computePrefHeight(width); 676 } 677 } 678 679 680 private final class IndeterminateSpinner extends Region { 681 private IndicatorPaths pathsG; 682 private final List<Double> opacities = new ArrayList<>(); 683 private boolean spinEnabled = false; 684 private Paint fillOverride = null; 685 686 private IndeterminateSpinner(boolean spinEnabled, Paint fillOverride) { 687 this.spinEnabled = spinEnabled; 688 this.fillOverride = fillOverride; 689 690 setNodeOrientation(NodeOrientation.LEFT_TO_RIGHT); 691 getStyleClass().setAll("spinner"); 692 693 pathsG = new IndicatorPaths(); 694 getChildren().add(pathsG); 695 rebuild(); 696 697 rebuildTimeline(); 698 699 } 700 701 public void setFillOverride(Paint fillOverride) { 702 this.fillOverride = fillOverride; 703 rebuild(); 704 } 705 706 public void setSpinEnabled(boolean spinEnabled) { 707 this.spinEnabled = spinEnabled; 708 rebuildTimeline(); 709 } 710 711 private void rebuildTimeline() { 712 if (spinEnabled) { 713 if (indeterminateTransition == null) { 714 indeterminateTransition = new Timeline(); 715 indeterminateTransition.setCycleCount(Timeline.INDEFINITE); 716 indeterminateTransition.setDelay(UNCLIPPED_DELAY); 717 } else { 718 indeterminateTransition.stop(); 719 ((Timeline)indeterminateTransition).getKeyFrames().clear(); 720 } 721 final ObservableList<KeyFrame> keyFrames = FXCollections.<KeyFrame>observableArrayList(); 722 723 keyFrames.add(new KeyFrame(Duration.millis(1), new KeyValue(pathsG.rotateProperty(), 360))); 724 keyFrames.add(new KeyFrame(Duration.millis(3900), new KeyValue(pathsG.rotateProperty(), 0))); 725 726 for (int i = 100; i <= 3900; i += 100) { 727 keyFrames.add(new KeyFrame(Duration.millis(i), event -> shiftColors())); 728 } 729 730 ((Timeline)indeterminateTransition).getKeyFrames().setAll(keyFrames); 731 indeterminateTransition.playFromStart(); 732 } else { 733 if (indeterminateTransition != null) { 734 indeterminateTransition.stop(); 735 ((Timeline)indeterminateTransition).getKeyFrames().clear(); 736 indeterminateTransition = null; 737 } 738 } 739 } 740 741 private class IndicatorPaths extends Pane { 742 @Override protected double computePrefWidth(double height) { 743 double w = 0; 744 for(Node child: getChildren()) { 745 if (child instanceof Region) { 746 Region region = (Region)child; 747 if (region.getShape() != null) { 748 w = Math.max(w,region.getShape().getLayoutBounds().getMaxX()); 749 } else { 750 w = Math.max(w,region.prefWidth(height)); 751 } 752 } 753 } 754 return w; 755 } 756 757 @Override protected double computePrefHeight(double width) { 758 double h = 0; 759 for(Node child: getChildren()) { 760 if (child instanceof Region) { 761 Region region = (Region)child; 762 if (region.getShape() != null) { 763 h = Math.max(h,region.getShape().getLayoutBounds().getMaxY()); 764 } else { 765 h = Math.max(h,region.prefHeight(width)); 766 } 767 } 768 } 769 return h; 770 } 771 772 @Override protected void layoutChildren() { 773 // calculate scale 774 double scale = getWidth() / computePrefWidth(-1); 775 for(Node child: getChildren()) { 776 if (child instanceof Region) { 777 Region region = (Region)child; 778 if (region.getShape() != null) { 779 region.resize( 780 region.getShape().getLayoutBounds().getMaxX(), 781 region.getShape().getLayoutBounds().getMaxY() 782 ); 783 region.getTransforms().setAll(new Scale(scale,scale,0,0)); 784 } else { 785 region.autosize(); 786 } 787 } 788 } 789 } 790 } 791 792 @Override protected void layoutChildren() { 793 final double w = control.getWidth() - control.snappedLeftInset() - control.snappedRightInset(); 794 final double h = control.getHeight() - control.snappedTopInset() - control.snappedBottomInset(); 795 final double prefW = pathsG.prefWidth(-1); 796 final double prefH = pathsG.prefHeight(-1); 797 double scaleX = w / prefW; 798 double scale = scaleX; 799 if ((scaleX * prefH) > h) { 800 scale = h / prefH; 801 } 802 double indicatorW = prefW * scale; 803 double indicatorH = prefH * scale; 804 pathsG.resizeRelocate((w - indicatorW) / 2, (h - indicatorH) / 2, indicatorW, indicatorH); 805 } 806 807 private void rebuild() { 808 // update indeterminate indicator 809 final int segments = indeterminateSegmentCount.get(); 810 opacities.clear(); 811 pathsG.getChildren().clear(); 812 final double step = 0.8/(segments-1); 813 for (int i = 0; i < segments; i++) { 814 Region region = new Region(); 815 region.setScaleShape(false); 816 region.setCenterShape(false); 817 region.getStyleClass().addAll("segment", "segment" + i); 818 if (fillOverride instanceof Color) { 819 Color c = (Color)fillOverride; 820 region.setStyle("-fx-background-color: rgba("+((int)(255*c.getRed()))+","+((int)(255*c.getGreen()))+","+((int)(255*c.getBlue()))+","+c.getOpacity()+");"); 821 } else { 822 region.setStyle(null); 823 } 824 pathsG.getChildren().add(region); 825 opacities.add(Math.max(0.1, (1.0 - (step*i)))); 826 } 827 } 828 829 private void shiftColors() { 830 if (opacities.size() <= 0) return; 831 final int segments = indeterminateSegmentCount.get(); 832 Collections.rotate(opacities, -1); 833 for (int i = 0; i < segments; i++) { 834 pathsG.getChildren().get(i).setOpacity(opacities.get(i)); 835 } 836 } 837 } 838 }