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