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