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