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