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 */ 399 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { 400 return STYLEABLES; 401 } 402 403 /** 404 * {@inheritDoc} 405 */ 406 @Override 407 public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() { 408 return getClassCssMetaData(); 409 } 410 411 412 413 /*************************************************************************** 414 * * 415 * Support classes * 416 * * 417 **************************************************************************/ 418 419 private final class DeterminateIndicator extends Region { 420 private double textGap = 2.0F; 421 422 // only update progress text on whole percentages 423 private int intProgress; 424 425 // only update pie arc to nearest degree 426 private int degProgress; 427 private Text text; 428 private StackPane indicator; 429 private StackPane progress; 430 private StackPane tick; 431 private Arc arcShape; 432 private Circle indicatorCircle; 433 434 public DeterminateIndicator(ProgressIndicator control, ProgressIndicatorSkin s, Paint fillOverride) { 435 436 getStyleClass().add("determinate-indicator"); 437 438 intProgress = (int) Math.round(control.getProgress() * 100.0) ; 439 degProgress = (int) (360 * control.getProgress()); 440 441 getChildren().clear(); 442 443 text = new Text((control.getProgress() >= 1) ? (DONE) : ("" + intProgress + "%")); 444 text.setTextOrigin(VPos.TOP); 445 text.getStyleClass().setAll("text", "percentage"); 446 447 // The circular background for the progress pie piece 448 indicator = new StackPane(); 449 indicator.setScaleShape(false); 450 indicator.setCenterShape(false); 451 indicator.getStyleClass().setAll("indicator"); 452 indicatorCircle = new Circle(); 453 indicator.setShape(indicatorCircle); 454 455 // The shape for our progress pie piece 456 arcShape = new Arc(); 457 arcShape.setType(ArcType.ROUND); 458 arcShape.setStartAngle(90.0F); 459 460 // Our progress pie piece 461 progress = new StackPane(); 462 progress.getStyleClass().setAll("progress"); 463 progress.setScaleShape(false); 464 progress.setCenterShape(false); 465 progress.setShape(arcShape); 466 progress.getChildren().clear(); 467 setFillOverride(fillOverride); 468 469 // The check mark that's drawn at 100% 470 tick = new StackPane(); 471 tick.getStyleClass().setAll("tick"); 472 473 getChildren().setAll(indicator, progress, text, tick); 474 updateProgress(control.getProgress()); 475 } 476 477 private void setFillOverride(Paint fillOverride) { 478 if (fillOverride instanceof Color) { 479 Color c = (Color)fillOverride; 480 progress.setStyle("-fx-background-color: rgba("+((int)(255*c.getRed()))+","+((int)(255*c.getGreen()))+","+((int)(255*c.getBlue()))+","+c.getOpacity()+");"); 481 } else { 482 progress.setStyle(null); 483 } 484 } 485 486 @Override public boolean usesMirroring() { 487 // This is used instead of setting NodeOrientation, 488 // allowing the Text node to inherit the current 489 // orientation. 490 return false; 491 } 492 493 private void updateProgress(double progress) { 494 intProgress = (int) Math.round(progress * 100.0) ; 495 text.setText((progress >= 1) ? (DONE) : ("" + intProgress + "%")); 496 497 degProgress = (int) (360 * progress); 498 arcShape.setLength(-degProgress); 499 requestLayout(); 500 } 501 502 @Override protected void layoutChildren() { 503 // Position and size the circular background 504 double doneTextHeight = doneText.getLayoutBounds().getHeight(); 505 final double left = control.snappedLeftInset(); 506 final double right = control.snappedRightInset(); 507 final double top = control.snappedTopInset(); 508 final double bottom = control.snappedBottomInset(); 509 510 /* 511 ** use the min of width, or height, keep it a circle 512 */ 513 final double areaW = control.getWidth() - left - right; 514 final double areaH = control.getHeight() - top - bottom - textGap - doneTextHeight; 515 final double radiusW = areaW / 2; 516 final double radiusH = areaH / 2; 517 final double radius = Math.floor(Math.min(radiusW, radiusH)); 518 final double centerX = snapPosition(left + radiusW); 519 final double centerY = snapPosition(top + radius); 520 521 // find radius that fits inside radius - insetsPadding 522 final double iLeft = indicator.snappedLeftInset(); 523 final double iRight = indicator.snappedRightInset(); 524 final double iTop = indicator.snappedTopInset(); 525 final double iBottom = indicator.snappedBottomInset(); 526 final double progressRadius = snapSize(Math.min( 527 Math.min(radius - iLeft, radius - iRight), 528 Math.min(radius - iTop, radius - iBottom))); 529 530 indicatorCircle.setRadius(radius); 531 indicator.setLayoutX(centerX); 532 indicator.setLayoutY(centerY); 533 534 arcShape.setRadiusX(progressRadius); 535 arcShape.setRadiusY(progressRadius); 536 progress.setLayoutX(centerX); 537 progress.setLayoutY(centerY); 538 539 // find radius that fits inside progressRadius - progressInsets 540 final double pLeft = progress.snappedLeftInset(); 541 final double pRight = progress.snappedRightInset(); 542 final double pTop = progress.snappedTopInset(); 543 final double pBottom = progress.snappedBottomInset(); 544 final double indicatorRadius = snapSize(Math.min( 545 Math.min(progressRadius - pLeft, progressRadius - pRight), 546 Math.min(progressRadius - pTop, progressRadius - pBottom))); 547 548 // find size of spare box that fits inside indicator radius 549 double squareBoxHalfWidth = Math.ceil(Math.sqrt((indicatorRadius * indicatorRadius) / 2)); 550 551 tick.setLayoutX(centerX - squareBoxHalfWidth); 552 tick.setLayoutY(centerY - squareBoxHalfWidth); 553 tick.resize(squareBoxHalfWidth + squareBoxHalfWidth, squareBoxHalfWidth + squareBoxHalfWidth); 554 tick.setVisible(control.getProgress() >= 1); 555 556 // if the % text can't fit anywhere in the bounds then don't display it 557 double textWidth = text.getLayoutBounds().getWidth(); 558 double textHeight = text.getLayoutBounds().getHeight(); 559 if (control.getWidth() >= textWidth && control.getHeight() >= textHeight) { 560 if (!text.isVisible()) text.setVisible(true); 561 text.setLayoutY(snapPosition(centerY + radius + textGap)); 562 text.setLayoutX(snapPosition(centerX - (textWidth/2))); 563 } else { 564 if (text.isVisible()) text.setVisible(false); 565 } 566 } 567 568 @Override protected double computePrefWidth(double height) { 569 final double left = control.snappedLeftInset(); 570 final double right = control.snappedRightInset(); 571 final double iLeft = indicator.snappedLeftInset(); 572 final double iRight = indicator.snappedRightInset(); 573 final double iTop = indicator.snappedTopInset(); 574 final double iBottom = indicator.snappedBottomInset(); 575 final double indicatorMax = snapSize(Math.max(Math.max(iLeft, iRight), Math.max(iTop, iBottom))); 576 final double pLeft = progress.snappedLeftInset(); 577 final double pRight = progress.snappedRightInset(); 578 final double pTop = progress.snappedTopInset(); 579 final double pBottom = progress.snappedBottomInset(); 580 final double progressMax = snapSize(Math.max(Math.max(pLeft, pRight), Math.max(pTop, pBottom))); 581 final double tLeft = tick.snappedLeftInset(); 582 final double tRight = tick.snappedRightInset(); 583 final double indicatorWidth = indicatorMax + progressMax + tLeft + tRight + progressMax + indicatorMax; 584 return left + Math.max(indicatorWidth, doneText.getLayoutBounds().getWidth()) + right; 585 } 586 587 @Override protected double computePrefHeight(double width) { 588 final double top = control.snappedTopInset(); 589 final double bottom = control.snappedBottomInset(); 590 final double iLeft = indicator.snappedLeftInset(); 591 final double iRight = indicator.snappedRightInset(); 592 final double iTop = indicator.snappedTopInset(); 593 final double iBottom = indicator.snappedBottomInset(); 594 final double indicatorMax = snapSize(Math.max(Math.max(iLeft, iRight), Math.max(iTop, iBottom))); 595 final double pLeft = progress.snappedLeftInset(); 596 final double pRight = progress.snappedRightInset(); 597 final double pTop = progress.snappedTopInset(); 598 final double pBottom = progress.snappedBottomInset(); 599 final double progressMax = snapSize(Math.max(Math.max(pLeft, pRight), Math.max(pTop, pBottom))); 600 final double tTop = tick.snappedTopInset(); 601 final double tBottom = tick.snappedBottomInset(); 602 final double indicatorHeight = indicatorMax + progressMax + tTop + tBottom + progressMax + indicatorMax; 603 return top + indicatorHeight + textGap + doneText.getLayoutBounds().getHeight() + bottom; 604 } 605 606 @Override protected double computeMaxWidth(double height) { 607 return computePrefWidth(height); 608 } 609 610 @Override protected double computeMaxHeight(double width) { 611 return computePrefHeight(width); 612 } 613 } 614 615 616 private final class IndeterminateSpinner extends Region { 617 private IndicatorPaths pathsG; 618 private final List<Double> opacities = new ArrayList<>(); 619 private boolean spinEnabled = false; 620 private Paint fillOverride = null; 621 622 private IndeterminateSpinner(boolean spinEnabled, Paint fillOverride) { 623 this.spinEnabled = spinEnabled; 624 this.fillOverride = fillOverride; 625 626 setNodeOrientation(NodeOrientation.LEFT_TO_RIGHT); 627 getStyleClass().setAll("spinner"); 628 629 pathsG = new IndicatorPaths(); 630 getChildren().add(pathsG); 631 rebuild(); 632 633 rebuildTimeline(); 634 635 } 636 637 public void setFillOverride(Paint fillOverride) { 638 this.fillOverride = fillOverride; 639 rebuild(); 640 } 641 642 public void setSpinEnabled(boolean spinEnabled) { 643 this.spinEnabled = spinEnabled; 644 rebuildTimeline(); 645 } 646 647 private void rebuildTimeline() { 648 if (spinEnabled) { 649 if (indeterminateTransition == null) { 650 indeterminateTransition = new Timeline(); 651 indeterminateTransition.setCycleCount(Timeline.INDEFINITE); 652 indeterminateTransition.setDelay(UNCLIPPED_DELAY); 653 } else { 654 indeterminateTransition.stop(); 655 ((Timeline)indeterminateTransition).getKeyFrames().clear(); 656 } 657 final ObservableList<KeyFrame> keyFrames = FXCollections.<KeyFrame>observableArrayList(); 658 659 keyFrames.add(new KeyFrame(Duration.millis(1), new KeyValue(pathsG.rotateProperty(), 360))); 660 keyFrames.add(new KeyFrame(Duration.millis(3900), new KeyValue(pathsG.rotateProperty(), 0))); 661 662 for (int i = 100; i <= 3900; i += 100) { 663 keyFrames.add(new KeyFrame(Duration.millis(i), event -> shiftColors())); 664 } 665 666 ((Timeline)indeterminateTransition).getKeyFrames().setAll(keyFrames); 667 indeterminateTransition.playFromStart(); 668 } else { 669 if (indeterminateTransition != null) { 670 indeterminateTransition.stop(); 671 ((Timeline)indeterminateTransition).getKeyFrames().clear(); 672 indeterminateTransition = null; 673 } 674 } 675 } 676 677 private class IndicatorPaths extends Pane { 678 @Override protected double computePrefWidth(double height) { 679 double w = 0; 680 for(Node child: getChildren()) { 681 if (child instanceof Region) { 682 Region region = (Region)child; 683 if (region.getShape() != null) { 684 w = Math.max(w,region.getShape().getLayoutBounds().getMaxX()); 685 } else { 686 w = Math.max(w,region.prefWidth(height)); 687 } 688 } 689 } 690 return w; 691 } 692 693 @Override protected double computePrefHeight(double width) { 694 double h = 0; 695 for(Node child: getChildren()) { 696 if (child instanceof Region) { 697 Region region = (Region)child; 698 if (region.getShape() != null) { 699 h = Math.max(h,region.getShape().getLayoutBounds().getMaxY()); 700 } else { 701 h = Math.max(h,region.prefHeight(width)); 702 } 703 } 704 } 705 return h; 706 } 707 708 @Override protected void layoutChildren() { 709 // calculate scale 710 double scale = getWidth() / computePrefWidth(-1); 711 for(Node child: getChildren()) { 712 if (child instanceof Region) { 713 Region region = (Region)child; 714 if (region.getShape() != null) { 715 region.resize( 716 region.getShape().getLayoutBounds().getMaxX(), 717 region.getShape().getLayoutBounds().getMaxY() 718 ); 719 region.getTransforms().setAll(new Scale(scale,scale,0,0)); 720 } else { 721 region.autosize(); 722 } 723 } 724 } 725 } 726 } 727 728 @Override protected void layoutChildren() { 729 final double w = control.getWidth() - control.snappedLeftInset() - control.snappedRightInset(); 730 final double h = control.getHeight() - control.snappedTopInset() - control.snappedBottomInset(); 731 final double prefW = pathsG.prefWidth(-1); 732 final double prefH = pathsG.prefHeight(-1); 733 double scaleX = w / prefW; 734 double scale = scaleX; 735 if ((scaleX * prefH) > h) { 736 scale = h / prefH; 737 } 738 double indicatorW = prefW * scale; 739 double indicatorH = prefH * scale; 740 pathsG.resizeRelocate((w - indicatorW) / 2, (h - indicatorH) / 2, indicatorW, indicatorH); 741 } 742 743 private void rebuild() { 744 // update indeterminate indicator 745 final int segments = indeterminateSegmentCount.get(); 746 opacities.clear(); 747 pathsG.getChildren().clear(); 748 final double step = 0.8/(segments-1); 749 for (int i = 0; i < segments; i++) { 750 Region region = new Region(); 751 region.setScaleShape(false); 752 region.setCenterShape(false); 753 region.getStyleClass().addAll("segment", "segment" + i); 754 if (fillOverride instanceof Color) { 755 Color c = (Color)fillOverride; 756 region.setStyle("-fx-background-color: rgba("+((int)(255*c.getRed()))+","+((int)(255*c.getGreen()))+","+((int)(255*c.getBlue()))+","+c.getOpacity()+");"); 757 } else { 758 region.setStyle(null); 759 } 760 pathsG.getChildren().add(region); 761 opacities.add(Math.max(0.1, (1.0 - (step*i)))); 762 } 763 } 764 765 private void shiftColors() { 766 if (opacities.size() <= 0) return; 767 final int segments = indeterminateSegmentCount.get(); 768 Collections.rotate(opacities, -1); 769 for (int i = 0; i < segments; i++) { 770 pathsG.getChildren().get(i).setOpacity(opacities.get(i)); 771 } 772 } 773 } 774 }