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