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