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.lang.ref.WeakReference;
  30 import java.util.ArrayList;
  31 import java.util.Collections;
  32 import java.util.List;
  33 
  34 import javafx.animation.Timeline;
  35 import javafx.animation.Transition;
  36 import javafx.beans.binding.When;
  37 import javafx.beans.property.BooleanProperty;
  38 import javafx.beans.property.DoubleProperty;
  39 import javafx.beans.value.WritableValue;
  40 import javafx.css.CssMetaData;
  41 import javafx.css.StyleableBooleanProperty;
  42 import javafx.css.StyleableDoubleProperty;
  43 import javafx.css.StyleableProperty;
  44 import javafx.scene.Node;
  45 import javafx.scene.control.Control;
  46 import javafx.scene.control.ProgressBar;
  47 import javafx.scene.control.ProgressIndicator;
  48 import javafx.scene.control.SkinBase;
  49 import javafx.scene.layout.Background;
  50 import javafx.scene.layout.BackgroundFill;
  51 import javafx.scene.layout.Region;
  52 import javafx.scene.layout.StackPane;
  53 import javafx.scene.paint.Color;
  54 import javafx.util.Duration;
  55 
  56 import javafx.css.converter.BooleanConverter;
  57 import javafx.css.converter.SizeConverter;
  58 
  59 import javafx.css.Styleable;
  60 
  61 /**
  62  * Default skin implementation for the {@link ProgressBar} control.
  63  *
  64  * @see ProgressBar
  65  * @since 9
  66  */
  67 public class ProgressBarSkin extends ProgressIndicatorSkin {
  68 
  69     /***************************************************************************
  70      *                                                                         *
  71      * Private fields                                                          *
  72      *                                                                         *
  73      **************************************************************************/
  74 
  75     private StackPane bar;
  76     private StackPane track;
  77     private Region clipRegion;
  78 
  79     // clean up progress so we never go out of bounds or update graphics more than twice per pixel
  80     private double barWidth;
  81 
  82 
  83 
  84     /***************************************************************************
  85      *                                                                         *
  86      * Constructors                                                            *
  87      *                                                                         *
  88      **************************************************************************/
  89 
  90     /**
  91      * Creates a new ProgressBarSkin instance, installing the necessary child
  92      * nodes into the Control {@link Control#getChildren() children} list.
  93      *
  94      * @param control The control that this skin should be installed onto.
  95      */
  96     public ProgressBarSkin(ProgressBar control) {
  97         super(control);
  98 
  99         barWidth = ((int) (control.getWidth() - snappedLeftInset() - snappedRightInset()) * 2 * Math.min(1, Math.max(0, control.getProgress()))) / 2.0F;
 100 
 101         control.widthProperty().addListener(observable -> updateProgress());
 102 
 103         initialize();
 104         getSkinnable().requestLayout();
 105     }
 106 
 107 
 108 
 109     /***************************************************************************
 110      *                                                                         *
 111      * Properties                                                              *
 112      *                                                                         *
 113      **************************************************************************/
 114 
 115     /**
 116      * The length of the bouncing progress bar in indeterminate state
 117      */
 118     private DoubleProperty indeterminateBarLength = null;
 119     private DoubleProperty indeterminateBarLengthProperty() {
 120         if (indeterminateBarLength == null) {
 121             indeterminateBarLength = new StyleableDoubleProperty(60.0) {
 122 
 123                 @Override
 124                 public Object getBean() {
 125                     return ProgressBarSkin.this;
 126                 }
 127 
 128                 @Override
 129                 public String getName() {
 130                     return "indeterminateBarLength";
 131                 }
 132 
 133                 @Override
 134                 public CssMetaData<ProgressBar,Number> getCssMetaData() {
 135                     return StyleableProperties.INDETERMINATE_BAR_LENGTH;
 136                 }
 137 
 138             };
 139         }
 140         return indeterminateBarLength;
 141     }
 142 
 143     private Double getIndeterminateBarLength() {
 144         return indeterminateBarLength == null ? 60.0 : indeterminateBarLength.get();
 145     }
 146 
 147     /**
 148      * If the progress bar should escape the ends of the progress bar region in indeterminate state
 149      */
 150     private BooleanProperty indeterminateBarEscape = null;
 151     private BooleanProperty indeterminateBarEscapeProperty() {
 152         if (indeterminateBarEscape == null) {
 153             indeterminateBarEscape = new StyleableBooleanProperty(true) {
 154 
 155                 @Override
 156                 public Object getBean() {
 157                     return ProgressBarSkin.this;
 158                 }
 159 
 160                 @Override
 161                 public String getName() {
 162                     return "indeterminateBarEscape";
 163                 }
 164 
 165                 @Override
 166                 public CssMetaData<ProgressBar,Boolean> getCssMetaData() {
 167                     return StyleableProperties.INDETERMINATE_BAR_ESCAPE;
 168                 }
 169 
 170 
 171             };
 172         }
 173         return indeterminateBarEscape;
 174     }
 175 
 176     private Boolean getIndeterminateBarEscape() {
 177         return indeterminateBarEscape == null ? true : indeterminateBarEscape.get();
 178     }
 179 
 180     /**
 181      * If the progress bar should flip when it gets to the ends in indeterminate state
 182      */
 183     private BooleanProperty indeterminateBarFlip = null;
 184     private BooleanProperty indeterminateBarFlipProperty() {
 185         if (indeterminateBarFlip == null) {
 186             indeterminateBarFlip = new StyleableBooleanProperty(true) {
 187 
 188                 @Override
 189                 public Object getBean() {
 190                     return ProgressBarSkin.this;
 191                 }
 192 
 193                 @Override
 194                 public String getName() {
 195                     return "indeterminateBarFlip";
 196                 }
 197 
 198                 @Override
 199                 public CssMetaData<ProgressBar,Boolean> getCssMetaData() {
 200                     return StyleableProperties.INDETERMINATE_BAR_FLIP;
 201                 }
 202 
 203             };
 204         }
 205         return indeterminateBarFlip;
 206     }
 207 
 208     private Boolean getIndeterminateBarFlip() {
 209         return indeterminateBarFlip == null ? true : indeterminateBarFlip.get();
 210     }
 211 
 212     /**
 213      * How many seconds it should take for the indeterminate bar to go from
 214      * one edge to the other
 215      */
 216     private DoubleProperty indeterminateBarAnimationTime = null;
 217 
 218     private DoubleProperty indeterminateBarAnimationTimeProperty() {
 219         if (indeterminateBarAnimationTime == null) {
 220             indeterminateBarAnimationTime = new StyleableDoubleProperty(2.0) {
 221 
 222                 @Override
 223                 public Object getBean() {
 224                     return ProgressBarSkin.this;
 225                 }
 226 
 227                 @Override
 228                 public String getName() {
 229                     return "indeterminateBarAnimationTime";
 230                 }
 231 
 232                 @Override
 233                 public CssMetaData<ProgressBar,Number> getCssMetaData() {
 234                     return StyleableProperties.INDETERMINATE_BAR_ANIMATION_TIME;
 235                 }
 236 
 237 
 238             };
 239         }
 240         return indeterminateBarAnimationTime;
 241     }
 242 
 243     private double getIndeterminateBarAnimationTime() {
 244         return indeterminateBarAnimationTime == null ? 2.0 : indeterminateBarAnimationTime.get();
 245     }
 246 
 247 
 248 
 249     /***************************************************************************
 250      *                                                                         *
 251      * Public API                                                              *
 252      *                                                                         *
 253      **************************************************************************/
 254 
 255     /** {@inheritDoc} */
 256     @Override public double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) {
 257         return Node.BASELINE_OFFSET_SAME_AS_HEIGHT;
 258     }
 259 
 260     /** {@inheritDoc} */
 261     @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 262         return Math.max(100, leftInset + bar.prefWidth(getSkinnable().getWidth()) + rightInset);
 263     }
 264 
 265     /** {@inheritDoc} */
 266     @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 267         return topInset + bar.prefHeight(width) + bottomInset;
 268     }
 269 
 270     /** {@inheritDoc} */
 271     @Override protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 272         return getSkinnable().prefWidth(height);
 273     }
 274 
 275     /** {@inheritDoc} */
 276     @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 277         return getSkinnable().prefHeight(width);
 278     }
 279 
 280     /** {@inheritDoc} */
 281     @Override protected void layoutChildren(final double x, final double y,
 282                                             final double w, final double h) {
 283 
 284         final ProgressIndicator control = getSkinnable();
 285         boolean isIndeterminate = control.isIndeterminate();
 286 
 287         // resize clip
 288         clipRegion.resizeRelocate(0, 0, w, h);
 289 
 290         track.resizeRelocate(x, y, w, h);
 291         bar.resizeRelocate(x, y, isIndeterminate ? getIndeterminateBarLength() : barWidth, h);
 292 
 293         // things should be invisible only when well below minimum length
 294         track.setVisible(true);
 295 
 296         // width might have changed so recreate our animation if needed
 297         if (isIndeterminate) {
 298             createIndeterminateTimeline();
 299             if (NodeHelper.isTreeShowing(getSkinnable())) {
 300                 indeterminateTransition.play();
 301             }
 302 
 303             // apply clip
 304             bar.setClip(clipRegion);
 305         } else if (indeterminateTransition != null) {
 306             indeterminateTransition.stop();
 307             indeterminateTransition = null;
 308 
 309             // remove clip
 310             bar.setClip(null);
 311             bar.setScaleX(1);
 312             bar.setTranslateX(0);
 313             clipRegion.translateXProperty().unbind();
 314         }
 315     }
 316 
 317 
 318 
 319     /***************************************************************************
 320      *                                                                         *
 321      * Private implementation                                                  *
 322      *                                                                         *
 323      **************************************************************************/
 324 
 325     /** {@inheritDoc} */
 326     @Override void initialize() {
 327         track = new StackPane();
 328         track.getStyleClass().setAll("track");
 329 
 330         bar = new StackPane();
 331         bar.getStyleClass().setAll("bar");
 332 
 333         getChildren().setAll(track, bar);
 334 
 335         // create a region to use as the clip for skin for animated indeterminate state
 336         clipRegion = new Region();
 337 
 338         // listen to the backgrounds on the bar and apply them to the clip but making them solid black for 100%
 339         // solid anywhere the bar draws
 340         bar.backgroundProperty().addListener((observable, oldValue, newValue) -> {
 341             if (newValue != null && !newValue.getFills().isEmpty()) {
 342                 final BackgroundFill[] fills = new BackgroundFill[newValue.getFills().size()];
 343                 for (int i = 0; i < newValue.getFills().size(); i++) {
 344                     BackgroundFill bf = newValue.getFills().get(i);
 345                     fills[i] = new BackgroundFill(Color.BLACK,bf.getRadii(),bf.getInsets());
 346                 }
 347                 clipRegion.setBackground(new Background(fills));
 348             }
 349         });
 350     }
 351 
 352     /** {@inheritDoc} */
 353     @Override void createIndeterminateTimeline() {
 354         if (indeterminateTransition != null) indeterminateTransition.stop();
 355 
 356         ProgressIndicator control = getSkinnable();
 357         final double w = control.getWidth() - (snappedLeftInset() + snappedRightInset());
 358         final double startX = getIndeterminateBarEscape() ? -getIndeterminateBarLength() : 0;
 359         final double endX = getIndeterminateBarEscape() ? w : w - getIndeterminateBarLength();
 360 
 361         // Set up the timeline.  We do not want to reverse if we are not flipping.
 362         indeterminateTransition = new IndeterminateTransition(startX, endX, this);
 363         indeterminateTransition.setCycleCount(Timeline.INDEFINITE);
 364 
 365         clipRegion.translateXProperty().bind(new When(bar.scaleXProperty().isEqualTo(-1.0, 1e-100)).
 366                 then(bar.translateXProperty().subtract(w).add(indeterminateBarLengthProperty())).
 367                 otherwise(bar.translateXProperty().negate()));
 368     }
 369 
 370     boolean wasIndeterminate = false;
 371 
 372     /** {@inheritDoc} */
 373     @Override void updateProgress() {
 374         ProgressIndicator control = getSkinnable();
 375         // RT-33789: if the ProgressBar was indeterminate and still is indeterminate, don't update the bar width
 376         final boolean isIndeterminate = control.isIndeterminate();
 377         if (!(isIndeterminate && wasIndeterminate)) {
 378             barWidth = ((int) (control.getWidth() - snappedLeftInset() - snappedRightInset()) * 2 * Math.min(1, Math.max(0, control.getProgress()))) / 2.0F;
 379             getSkinnable().requestLayout();
 380         }
 381         wasIndeterminate = isIndeterminate;
 382     }
 383 
 384 
 385 
 386     /***************************************************************************
 387      *                                                                         *
 388      * Stylesheet Handling                                                     *
 389      *                                                                         *
 390      **************************************************************************/
 391 
 392     /*
 393      * Super-lazy instantiation pattern from Bill Pugh.
 394      */
 395     private static class StyleableProperties {
 396         private static final CssMetaData<ProgressBar, Number> INDETERMINATE_BAR_LENGTH =
 397                 new CssMetaData<ProgressBar, Number>("-fx-indeterminate-bar-length",
 398                         SizeConverter.getInstance(), 60.0) {
 399 
 400                     @Override
 401                     public boolean isSettable(ProgressBar n) {
 402                         final ProgressBarSkin skin = (ProgressBarSkin) n.getSkin();
 403                         return skin.indeterminateBarLength == null ||
 404                                 !skin.indeterminateBarLength.isBound();
 405                     }
 406 
 407                     @Override
 408                     public StyleableProperty<Number> getStyleableProperty(ProgressBar n) {
 409                         final ProgressBarSkin skin = (ProgressBarSkin) n.getSkin();
 410                         return (StyleableProperty<Number>) (WritableValue<Number>) skin.indeterminateBarLengthProperty();
 411                     }
 412                 };
 413 
 414         private static final CssMetaData<ProgressBar, Boolean> INDETERMINATE_BAR_ESCAPE =
 415                 new CssMetaData<ProgressBar, Boolean>("-fx-indeterminate-bar-escape",
 416                         BooleanConverter.getInstance(), Boolean.TRUE) {
 417 
 418                     @Override
 419                     public boolean isSettable(ProgressBar n) {
 420                         final ProgressBarSkin skin = (ProgressBarSkin) n.getSkin();
 421                         return skin.indeterminateBarEscape == null ||
 422                                 !skin.indeterminateBarEscape.isBound();
 423                     }
 424 
 425                     @Override
 426                     public StyleableProperty<Boolean> getStyleableProperty(ProgressBar n) {
 427                         final ProgressBarSkin skin = (ProgressBarSkin) n.getSkin();
 428                         return (StyleableProperty<Boolean>) (WritableValue<Boolean>) skin.indeterminateBarEscapeProperty();
 429                     }
 430                 };
 431 
 432         private static final CssMetaData<ProgressBar, Boolean> INDETERMINATE_BAR_FLIP =
 433                 new CssMetaData<ProgressBar, Boolean>("-fx-indeterminate-bar-flip",
 434                         BooleanConverter.getInstance(), Boolean.TRUE) {
 435 
 436                     @Override
 437                     public boolean isSettable(ProgressBar n) {
 438                         final ProgressBarSkin skin = (ProgressBarSkin) n.getSkin();
 439                         return skin.indeterminateBarFlip == null ||
 440                                 !skin.indeterminateBarFlip.isBound();
 441                     }
 442 
 443                     @Override
 444                     public StyleableProperty<Boolean> getStyleableProperty(ProgressBar n) {
 445                         final ProgressBarSkin skin = (ProgressBarSkin) n.getSkin();
 446                         return (StyleableProperty<Boolean>) (WritableValue<Boolean>) skin.indeterminateBarFlipProperty();
 447                     }
 448                 };
 449 
 450         private static final CssMetaData<ProgressBar, Number> INDETERMINATE_BAR_ANIMATION_TIME =
 451                 new CssMetaData<ProgressBar, Number>("-fx-indeterminate-bar-animation-time",
 452                         SizeConverter.getInstance(), 2.0) {
 453 
 454                     @Override
 455                     public boolean isSettable(ProgressBar n) {
 456                         final ProgressBarSkin skin = (ProgressBarSkin) n.getSkin();
 457                         return skin.indeterminateBarAnimationTime == null ||
 458                                 !skin.indeterminateBarAnimationTime.isBound();
 459                     }
 460 
 461                     @Override
 462                     public StyleableProperty<Number> getStyleableProperty(ProgressBar n) {
 463                         final ProgressBarSkin skin = (ProgressBarSkin) n.getSkin();
 464                         return (StyleableProperty<Number>) (WritableValue<Number>) skin.indeterminateBarAnimationTimeProperty();
 465                     }
 466                 };
 467 
 468         private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
 469 
 470         static {
 471             final List<CssMetaData<? extends Styleable, ?>> styleables =
 472                     new ArrayList<CssMetaData<? extends Styleable, ?>>(SkinBase.getClassCssMetaData());
 473             styleables.add(INDETERMINATE_BAR_LENGTH);
 474             styleables.add(INDETERMINATE_BAR_ESCAPE);
 475             styleables.add(INDETERMINATE_BAR_FLIP);
 476             styleables.add(INDETERMINATE_BAR_ANIMATION_TIME);
 477             STYLEABLES = Collections.unmodifiableList(styleables);
 478         }
 479     }
 480 
 481     /**
 482      * Returns the CssMetaData associated with this class, which may include the
 483      * CssMetaData of its superclasses.
 484      */
 485     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
 486         return StyleableProperties.STYLEABLES;
 487     }
 488 
 489     /**
 490      * {@inheritDoc}
 491      */
 492     @Override public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
 493         return getClassCssMetaData();
 494     }
 495 
 496 
 497 
 498     /***************************************************************************
 499      *                                                                         *
 500      * Support classes                                                         *
 501      *                                                                         *
 502      **************************************************************************/
 503 
 504     private static class IndeterminateTransition extends Transition {
 505         private final WeakReference<ProgressBarSkin> skin;
 506         private final double startX;
 507         private final double endX;
 508         private final boolean flip;
 509 
 510         public IndeterminateTransition(double startX, double endX, ProgressBarSkin progressBarSkin) {
 511             this.startX = startX;
 512             this.endX = endX;
 513             this.skin = new WeakReference<>(progressBarSkin);
 514             this.flip = progressBarSkin.getIndeterminateBarFlip();
 515             progressBarSkin.getIndeterminateBarEscape();
 516             setCycleDuration(Duration.seconds(progressBarSkin.getIndeterminateBarAnimationTime() * (flip ? 2 : 1)));
 517         }
 518 
 519         @Override
 520         protected void interpolate(double frac) {
 521             ProgressBarSkin s = skin.get();
 522             if (s == null) {
 523                 stop();
 524             } else {
 525                 if (frac <= 0.5 || !flip) {
 526                     s.bar.setScaleX(-1);
 527                     s.bar.setTranslateX(startX + (flip ? 2 : 1) * frac * (endX - startX));
 528                 } else {
 529                     s.bar.setScaleX(1);
 530                     s.bar.setTranslateX(startX + 2 * (1 - frac) * (endX - startX));
 531                 }
 532             }
 533         }
 534     }
 535 }