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 }