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