1 /*
   2  * Copyright (c) 2010, 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javafx.animation;
  27 
  28 import java.util.HashMap;
  29 
  30 import com.sun.javafx.tk.Toolkit;
  31 import javafx.beans.property.BooleanProperty;
  32 import javafx.beans.property.DoubleProperty;
  33 import javafx.beans.property.DoublePropertyBase;
  34 import javafx.beans.property.IntegerProperty;
  35 import javafx.beans.property.IntegerPropertyBase;
  36 import javafx.beans.property.ObjectProperty;
  37 import javafx.beans.property.ObjectPropertyBase;
  38 import javafx.beans.property.ReadOnlyDoubleProperty;
  39 import javafx.beans.property.ReadOnlyDoublePropertyBase;
  40 import javafx.beans.property.ReadOnlyObjectProperty;
  41 import javafx.beans.property.ReadOnlyObjectPropertyBase;
  42 import javafx.beans.property.SimpleBooleanProperty;
  43 import javafx.beans.property.SimpleObjectProperty;
  44 import javafx.collections.FXCollections;
  45 import javafx.collections.ObservableMap;
  46 import javafx.event.ActionEvent;
  47 import javafx.event.EventHandler;
  48 import javafx.util.Duration;
  49 import com.sun.javafx.animation.TickCalculation;
  50 import com.sun.scenario.animation.AbstractMasterTimer;
  51 import com.sun.scenario.animation.shared.ClipEnvelope;
  52 import com.sun.scenario.animation.shared.PulseReceiver;
  53 
  54 import static com.sun.javafx.animation.TickCalculation.*;
  55 import java.security.AccessControlContext;
  56 import java.security.AccessController;
  57 import java.security.PrivilegedAction;
  58 
  59 /**
  60  * The class {@code Animation} provides the core functionality of all animations
  61  * used in the JavaFX runtime.
  62  * <p>
  63  * An animation can run in a loop by setting {@link #cycleCount}. To make an
  64  * animation run back and forth while looping, set the {@link #autoReverse}
  65  * -flag.
  66  * <p>
  67  * Call {@link #play()} or {@link #playFromStart()} to play an {@code Animation}
  68  * . The {@code Animation} progresses in the direction and speed specified by
  69  * {@link #rate}, and stops when its duration is elapsed. An {@code Animation}
  70  * with indefinite duration (a {@link #cycleCount} of {@link #INDEFINITE}) runs
  71  * repeatedly until the {@link #stop()} method is explicitly called, which will
  72  * stop the running {@code Animation} and reset its play head to the initial
  73  * position.
  74  * <p>
  75  * An {@code Animation} can be paused by calling {@link #pause()}, and the next
  76  * {@link #play()} call will resume the {@code Animation} from where it was
  77  * paused.
  78  * <p>
  79  * An {@code Animation}'s play head can be randomly positioned, whether it is
  80  * running or not. If the {@code Animation} is running, the play head jumps to
  81  * the specified position immediately and continues playing from new position.
  82  * If the {@code Animation} is not running, the next {@link #play()} will start
  83  * the {@code Animation} from the specified position.
  84  * <p>
  85  * Inverting the value of {@link #rate} toggles the play direction.
  86  *
  87  * @see Timeline
  88  * @see Transition
  89  *
  90  * @since JavaFX 2.0
  91  */
  92 public abstract class Animation {
  93 
  94     static {
  95         AnimationAccessorImpl.DEFAULT = new AnimationAccessorImpl();
  96     }
  97 
  98     /**
  99      * Used to specify an animation that repeats indefinitely, until the
 100      * {@code stop()} method is called.
 101      */
 102     public static final int INDEFINITE = -1;
 103 
 104     /**
 105      * The possible states for {@link Animation#statusProperty status}.
 106      * @since JavaFX 2.0
 107      */
 108     public static enum Status {
 109         /**
 110          * The paused state.
 111          */
 112         PAUSED,
 113         /**
 114          * The running state.
 115          */
 116         RUNNING,
 117         /**
 118          * The stopped state.
 119          */
 120         STOPPED
 121     }
 122 
 123     private static final double EPSILON = 1e-12;
 124 
 125     /*
 126         These four fields and associated methods were moved here from AnimationPulseReceiver
 127         when that class was removed. They could probably be integrated much cleaner into Animation,
 128         but to make sure the change was made without introducing regressions, this code was
 129         moved pretty much verbatim.
 130      */
 131     private long startTime;
 132     private long pauseTime;
 133     private boolean paused = false;
 134     private final AbstractMasterTimer timer;
 135 
 136     // Access control context, captured whenever we add this pulse reciever to
 137     // the master timer (which is called when an animation is played or resumed)
 138     private AccessControlContext accessCtrlCtx = null;
 139 
 140     private long now() {
 141         return TickCalculation.fromNano(timer.nanos());
 142     }
 143 
 144     private void addPulseReceiver() {
 145         // Capture the Access Control Context to be used during the animation pulse
 146         accessCtrlCtx = AccessController.getContext();
 147 
 148         timer.addPulseReceiver(pulseReceiver);
 149     }
 150 
 151     void startReceiver(long delay) {
 152         paused = false;
 153         startTime = now() + delay;
 154         addPulseReceiver();
 155     }
 156 
 157     void pauseReceiver() {
 158         if (!paused) {
 159             pauseTime = now();
 160             paused = true;
 161             timer.removePulseReceiver(pulseReceiver);
 162         }
 163     }
 164 
 165     void resumeReceiver() {
 166         if (paused) {
 167             final long deltaTime = now() - pauseTime;
 168             startTime += deltaTime;
 169             paused = false;
 170             addPulseReceiver();
 171         }
 172     }
 173 
 174     // package private only for the sake of testing
 175     final PulseReceiver pulseReceiver = new PulseReceiver() {
 176         @Override public void timePulse(long now) {
 177             final long elapsedTime = now - startTime;
 178             if (elapsedTime < 0) {
 179                 return;
 180             }
 181             if (accessCtrlCtx == null) {
 182                 throw new IllegalStateException("Error: AccessControlContext not captured");
 183             }
 184 
 185             AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
 186                 impl_timePulse(elapsedTime);
 187                 return null;
 188             }, accessCtrlCtx);
 189         }
 190     };
 191 
 192     private class CurrentRateProperty extends ReadOnlyDoublePropertyBase {
 193         private double value;
 194 
 195         @Override
 196         public Object getBean() {
 197             return Animation.this;
 198         }
 199 
 200         @Override
 201         public String getName() {
 202             return "currentRate";
 203         }
 204 
 205         @Override
 206         public double get() {
 207             return value;
 208         }
 209 
 210         private void set(double value) {
 211             this.value = value;
 212             fireValueChangedEvent();
 213         }
 214     }
 215 
 216     private class AnimationReadOnlyProperty<T> extends ReadOnlyObjectPropertyBase<T> {
 217 
 218         private final String name;
 219         private T value;
 220 
 221         private AnimationReadOnlyProperty(String name, T value) {
 222             this.name = name;
 223             this.value = value;
 224         }
 225 
 226         @Override
 227         public Object getBean() {
 228             return Animation.this;
 229         }
 230 
 231         @Override
 232         public String getName() {
 233             return name;
 234         }
 235 
 236         @Override
 237         public T get() {
 238             return value;
 239         }
 240 
 241         private void set(T value) {
 242             this.value = value;
 243             fireValueChangedEvent();
 244         }
 245     }
 246 
 247     /**
 248      * The parent of this {@code Animation}. If this animation has not been
 249      * added to another animation, such as {@link ParallelTransition} and
 250      * {@link SequentialTransition}, then parent will be null.
 251      *
 252      * @defaultValue null
 253      */
 254     Animation parent = null;
 255 
 256     /* Package-private for testing purposes */
 257     ClipEnvelope clipEnvelope;
 258 
 259     private boolean lastPlayedFinished = false;
 260 
 261     private boolean lastPlayedForward = true;
 262     /**
 263      * Defines the direction/speed at which the {@code Animation} is expected to
 264      * be played.
 265      * <p>
 266      * The absolute value of {@code rate} indicates the speed which the
 267      * {@code Animation} is to be played, while the sign of {@code rate}
 268      * indicates the direction. A positive value of {@code rate} indicates
 269      * forward play, a negative value indicates backward play and {@code 0.0} to
 270      * stop a running {@code Animation}.
 271      * <p>
 272      * Rate {@code 1.0} is normal play, {@code 2.0} is 2 time normal,
 273      * {@code -1.0} is backwards, etc...
 274      *
 275      * <p>
 276      * Inverting the rate of a running {@code Animation} will cause the
 277      * {@code Animation} to reverse direction in place and play back over the
 278      * portion of the {@code Animation} that has already elapsed.
 279      *
 280      * @defaultValue 1.0
 281      */
 282     private DoubleProperty rate;
 283     private static final double DEFAULT_RATE = 1.0;
 284 
 285     public final void setRate(double value) {
 286         if ((rate != null) || (Math.abs(value - DEFAULT_RATE) > EPSILON)) {
 287             rateProperty().set(value);
 288         }
 289     }
 290 
 291     public final double getRate() {
 292         return (rate == null)? DEFAULT_RATE : rate.get();
 293     }
 294 
 295     public final DoubleProperty rateProperty() {
 296         if (rate == null) {
 297             rate = new DoublePropertyBase(DEFAULT_RATE) {
 298 
 299                 @Override
 300                 public void invalidated() {
 301                     final double newRate = getRate();
 302                     if (isRunningEmbedded()) {
 303                         if (isBound()) {
 304                             unbind();
 305                         }
 306                         set(oldRate);
 307                         throw new IllegalArgumentException("Cannot set rate of embedded animation while running.");
 308                     } else {
 309                         if (Math.abs(newRate) < EPSILON) {
 310                             if (getStatus() == Status.RUNNING) {
 311                                 lastPlayedForward = (Math.abs(getCurrentRate()
 312                                         - oldRate) < EPSILON);
 313                             }
 314                             setCurrentRate(0.0);
 315                             pauseReceiver();
 316                         } else {
 317                             if (getStatus() == Status.RUNNING) {
 318                                 final double currentRate = getCurrentRate();
 319                                 if (Math.abs(currentRate) < EPSILON) {
 320                                     setCurrentRate(lastPlayedForward ? newRate : -newRate);
 321                                     resumeReceiver();
 322                                 } else {
 323                                     final boolean playingForward = Math.abs(currentRate - oldRate) < EPSILON;
 324                                     setCurrentRate(playingForward ? newRate : -newRate);
 325                                 }
 326                             }
 327                             oldRate = newRate;
 328                         }
 329                         clipEnvelope.setRate(newRate);
 330                     }
 331                 }
 332 
 333                 @Override
 334                 public Object getBean() {
 335                     return Animation.this;
 336                 }
 337 
 338                 @Override
 339                 public String getName() {
 340                     return "rate";
 341                 }
 342             };
 343         }
 344         return rate;
 345     }
 346 
 347     private boolean isRunningEmbedded() {
 348         if (parent == null) {
 349             return false;
 350         }
 351         return parent.getStatus() != Status.STOPPED || parent.isRunningEmbedded();
 352     }
 353 
 354     private double oldRate = 1.0;
 355     /**
 356      * Read-only variable to indicate current direction/speed at which the
 357      * {@code Animation} is being played.
 358      * <p>
 359      * {@code currentRate} is not necessary equal to {@code rate}.
 360      * {@code currentRate} is set to {@code 0.0} when animation is paused or
 361      * stopped. {@code currentRate} may also point to different direction during
 362      * reverse cycles when {@code autoReverse} is {@code true}
 363      *
 364      * @defaultValue 0.0
 365      */
 366     private ReadOnlyDoubleProperty currentRate;
 367     private static final double DEFAULT_CURRENT_RATE = 0.0;
 368 
 369     private void setCurrentRate(double value) {
 370         if ((currentRate != null) || (Math.abs(value - DEFAULT_CURRENT_RATE) > EPSILON)) {
 371             ((CurrentRateProperty)currentRateProperty()).set(value);
 372         }
 373     }
 374 
 375     public final double getCurrentRate() {
 376         return (currentRate == null)? DEFAULT_CURRENT_RATE : currentRate.get();
 377     }
 378 
 379     public final ReadOnlyDoubleProperty currentRateProperty() {
 380         if (currentRate == null) {
 381             currentRate = new CurrentRateProperty();
 382         }
 383         return currentRate;
 384     }
 385 
 386     /**
 387      * Read-only variable to indicate the duration of one cycle of this
 388      * {@code Animation}: the time it takes to play from time 0 to the
 389      * end of the Animation (at the default {@code rate} of
 390      * 1.0).
 391      *
 392      * @defaultValue 0ms
 393      */
 394     private ReadOnlyObjectProperty<Duration> cycleDuration;
 395     private static final Duration DEFAULT_CYCLE_DURATION = Duration.ZERO;
 396 
 397     protected final void setCycleDuration(Duration value) {
 398         if ((cycleDuration != null) || (!DEFAULT_CYCLE_DURATION.equals(value))) {
 399             if (value.lessThan(Duration.ZERO)) {
 400                 throw new IllegalArgumentException("Cycle duration cannot be negative");
 401             }
 402             ((AnimationReadOnlyProperty<Duration>)cycleDurationProperty()).set(value);
 403             updateTotalDuration();
 404         }
 405     }
 406 
 407     public final Duration getCycleDuration() {
 408         return (cycleDuration == null)? DEFAULT_CYCLE_DURATION : cycleDuration.get();
 409     }
 410 
 411     public final ReadOnlyObjectProperty<Duration> cycleDurationProperty() {
 412         if (cycleDuration == null) {
 413             cycleDuration = new AnimationReadOnlyProperty<Duration>("cycleDuration", DEFAULT_CYCLE_DURATION);
 414         }
 415         return cycleDuration;
 416     }
 417 
 418     /**
 419      * Read-only variable to indicate the total duration of this
 420      * {@code Animation}, including repeats. A {@code Animation} with a {@code cycleCount}
 421      * of {@code Animation.INDEFINITE} will have a {@code totalDuration} of
 422      * {@code Duration.INDEFINITE}.
 423      *
 424      * <p>
 425      * This is set to cycleDuration * cycleCount.
 426      *
 427      * @defaultValue 0ms
 428      */
 429     private ReadOnlyObjectProperty<Duration> totalDuration;
 430     private static final Duration DEFAULT_TOTAL_DURATION = Duration.ZERO;
 431 
 432     public final Duration getTotalDuration() {
 433         return (totalDuration == null)? DEFAULT_TOTAL_DURATION : totalDuration.get();
 434     }
 435 
 436     public final ReadOnlyObjectProperty<Duration> totalDurationProperty() {
 437         if (totalDuration == null) {
 438             totalDuration = new AnimationReadOnlyProperty<Duration>("totalDuration", DEFAULT_TOTAL_DURATION);
 439         }
 440         return totalDuration;
 441     }
 442 
 443     private void updateTotalDuration() {
 444         // Implementing the bind eagerly, because cycleCount and
 445         // cycleDuration should not change that often
 446         final int cycleCount = getCycleCount();
 447         final Duration cycleDuration = getCycleDuration();
 448         final Duration newTotalDuration = Duration.ZERO.equals(cycleDuration) ? Duration.ZERO
 449                 : (cycleCount == Animation.INDEFINITE) ? Duration.INDEFINITE
 450                         : (cycleCount <= 1) ? cycleDuration : cycleDuration
 451                                 .multiply(cycleCount);
 452         if ((totalDuration != null) || (!DEFAULT_TOTAL_DURATION.equals(newTotalDuration))) {
 453             ((AnimationReadOnlyProperty<Duration>)totalDurationProperty()).set(newTotalDuration);
 454         }
 455         if (getStatus() == Status.STOPPED) {
 456             syncClipEnvelope();
 457             if (newTotalDuration.lessThan(getCurrentTime())) {
 458                 clipEnvelope.jumpTo(fromDuration(newTotalDuration));
 459             }
 460         }
 461     }
 462 
 463     /**
 464      * Defines the {@code Animation}'s play head position.
 465      *
 466      * @defaultValue 0ms
 467      */
 468     private CurrentTimeProperty currentTime;
 469     private long currentTicks;
 470     private class CurrentTimeProperty extends ReadOnlyObjectPropertyBase<Duration> {
 471 
 472         @Override
 473         public Object getBean() {
 474             return Animation.this;
 475         }
 476 
 477         @Override
 478         public String getName() {
 479             return "currentTime";
 480         }
 481 
 482         @Override
 483         public Duration get() {
 484             return getCurrentTime();
 485         }
 486 
 487         @Override
 488         public void fireValueChangedEvent() {
 489             super.fireValueChangedEvent();
 490         }
 491 
 492     }
 493 
 494     public final Duration getCurrentTime() {
 495         return TickCalculation.toDuration(currentTicks);
 496     }
 497 
 498     public final ReadOnlyObjectProperty<Duration> currentTimeProperty() {
 499         if (currentTime == null) {
 500             currentTime = new CurrentTimeProperty();
 501         }
 502         return currentTime;
 503     }
 504 
 505     /**
 506      * Delays the start of an animation.
 507      *
 508      * Cannot be negative. Setting to a negative number will result in {@link IllegalArgumentException}.
 509      *
 510      * @defaultValue 0ms
 511      */
 512     private ObjectProperty<Duration> delay;
 513     private static final Duration DEFAULT_DELAY = Duration.ZERO;
 514 
 515     public final void setDelay(Duration value) {
 516         if ((delay != null) || (!DEFAULT_DELAY.equals(value))) {
 517             delayProperty().set(value);
 518         }
 519     }
 520 
 521     public final Duration getDelay() {
 522         return (delay == null)? DEFAULT_DELAY : delay.get();
 523     }
 524 
 525     public final ObjectProperty<Duration> delayProperty() {
 526         if (delay == null) {
 527             delay = new ObjectPropertyBase<Duration>(DEFAULT_DELAY) {
 528 
 529                 @Override
 530                 public Object getBean() {
 531                     return Animation.this;
 532                 }
 533 
 534                 @Override
 535                 public String getName() {
 536                     return "delay";
 537                 }
 538 
 539                 @Override
 540                 protected void invalidated() {
 541                         final Duration newDuration = get();
 542                         if (newDuration.lessThan(Duration.ZERO)) {
 543                             if (isBound()) {
 544                                 unbind();
 545                             }
 546                             set(Duration.ZERO);
 547                             throw new IllegalArgumentException("Cannot set delay to negative value. Setting to Duration.ZERO");
 548                         }
 549                 }
 550 
 551             };
 552         }
 553         return delay;
 554     }
 555 
 556     /**
 557      * Defines the number of cycles in this animation. The {@code cycleCount}
 558      * may be {@code INDEFINITE} for animations that repeat indefinitely, but
 559      * must otherwise be > 0.
 560      * <p>
 561      * It is not possible to change the {@code cycleCount} of a running
 562      * {@code Animation}. If the value of {@code cycleCount} is changed for a
 563      * running {@code Animation}, the animation has to be stopped and started again to pick
 564      * up the new value.
 565      *
 566      * @defaultValue 1.0
 567      *
 568      */
 569     private IntegerProperty cycleCount;
 570     private static final int DEFAULT_CYCLE_COUNT = 1;
 571 
 572     public final void setCycleCount(int value) {
 573         if ((cycleCount != null) || (value != DEFAULT_CYCLE_COUNT)) {
 574             cycleCountProperty().set(value);
 575         }
 576     }
 577 
 578     public final int getCycleCount() {
 579         return (cycleCount == null)? DEFAULT_CYCLE_COUNT : cycleCount.get();
 580     }
 581 
 582     public final IntegerProperty cycleCountProperty() {
 583         if (cycleCount == null) {
 584             cycleCount = new IntegerPropertyBase(DEFAULT_CYCLE_COUNT) {
 585 
 586                 @Override
 587                 public void invalidated() {
 588                     updateTotalDuration();
 589                 }
 590 
 591                 @Override
 592                 public Object getBean() {
 593                     return Animation.this;
 594                 }
 595 
 596                 @Override
 597                 public String getName() {
 598                     return "cycleCount";
 599                 }
 600             };
 601         }
 602         return cycleCount;
 603     }
 604 
 605     /**
 606      * Defines whether this
 607      * {@code Animation} reverses direction on alternating cycles. If
 608      * {@code true}, the
 609      * {@code Animation} will proceed forward on the first cycle,
 610      * then reverses on the second cycle, and so on. Otherwise, animation will
 611      * loop such that each cycle proceeds forward from the start.
 612      *
 613      * It is not possible to change the {@code autoReverse} flag of a running
 614      * {@code Animation}. If the value of {@code autoReverse} is changed for a
 615      * running {@code Animation}, the animation has to be stopped and started again to pick
 616      * up the new value.
 617      *
 618      * @defaultValue false
 619      */
 620     private BooleanProperty autoReverse;
 621     private static final boolean DEFAULT_AUTO_REVERSE = false;
 622 
 623     public final void setAutoReverse(boolean value) {
 624         if ((autoReverse != null) || (value != DEFAULT_AUTO_REVERSE)) {
 625             autoReverseProperty().set(value);
 626         }
 627     }
 628 
 629     public final boolean isAutoReverse() {
 630         return (autoReverse == null)? DEFAULT_AUTO_REVERSE : autoReverse.get();
 631     }
 632 
 633     public final BooleanProperty autoReverseProperty() {
 634         if (autoReverse == null) {
 635             autoReverse = new SimpleBooleanProperty(this, "autoReverse", DEFAULT_AUTO_REVERSE);
 636         }
 637         return autoReverse;
 638     }
 639 
 640     /**
 641      * The status of the {@code Animation}.
 642      *
 643      * In {@code Animation} can be in one of three states:
 644      * {@link Status#STOPPED}, {@link Status#PAUSED} or {@link Status#RUNNING}.
 645      */
 646     private ReadOnlyObjectProperty<Status> status;
 647     private static final Status DEFAULT_STATUS = Status.STOPPED;
 648 
 649     protected final void setStatus(Status value) {
 650         if ((status != null) || (!DEFAULT_STATUS.equals(value))) {
 651             ((AnimationReadOnlyProperty<Status>)statusProperty()).set(value);
 652         }
 653     }
 654 
 655     public final Status getStatus() {
 656         return (status == null)? DEFAULT_STATUS : status.get();
 657     }
 658 
 659     public final ReadOnlyObjectProperty<Status> statusProperty() {
 660         if (status == null) {
 661             status = new AnimationReadOnlyProperty<Status>("status", Status.STOPPED);
 662         }
 663         return status;
 664     }
 665 
 666     private final double targetFramerate;
 667     private final int resolution;
 668     private long lastPulse;
 669 
 670     /**
 671      * The target framerate is the maximum framerate at which this {@code Animation}
 672      * will run, in frames per second. This can be used, for example, to keep
 673      * particularly complex {@code Animations} from over-consuming system resources.
 674      * By default, an {@code Animation}'s framerate is not explicitly limited, meaning
 675      * the {@code Animation} will run at an optimal framerate for the underlying platform.
 676      *
 677      * @return the target framerate
 678      */
 679     public final double getTargetFramerate() {
 680         return targetFramerate;
 681     }
 682 
 683     /**
 684      * The action to be executed at the conclusion of this {@code Animation}.
 685      *
 686      * @defaultValue null
 687      */
 688     private ObjectProperty<EventHandler<ActionEvent>> onFinished;
 689     private static final EventHandler<ActionEvent> DEFAULT_ON_FINISHED = null;
 690 
 691     public final void setOnFinished(EventHandler<ActionEvent> value) {
 692         if ((onFinished != null) || (value != null /* DEFAULT_ON_FINISHED */)) {
 693             onFinishedProperty().set(value);
 694         }
 695     }
 696 
 697     public final EventHandler<ActionEvent> getOnFinished() {
 698         return (onFinished == null)? DEFAULT_ON_FINISHED : onFinished.get();
 699     }
 700 
 701     public final ObjectProperty<EventHandler<ActionEvent>> onFinishedProperty() {
 702         if (onFinished == null) {
 703             onFinished = new SimpleObjectProperty<EventHandler<ActionEvent>>(this, "onFinished", DEFAULT_ON_FINISHED);
 704         }
 705         return onFinished;
 706     }
 707 
 708     private final ObservableMap<String, Duration> cuePoints = FXCollections
 709             .observableMap(new HashMap<String, Duration>(0));
 710 
 711     /**
 712      * The cue points can be
 713      * used to mark important positions of the {@code Animation}. Once a cue
 714      * point was defined, it can be used as an argument of
 715      * {@link #jumpTo(String) jumpTo()} and {@link #playFrom(String) playFrom()}
 716      * to move to the associated position quickly.
 717      * <p>
 718      * Every {@code Animation} has two predefined cue points {@code "start"} and
 719      * {@code "end"}, which are set at the start respectively the end of the
 720      * {@code Animation}. The predefined cuepoints do not appear in the map,
 721      * attempts to override them have no effect.
 722      * <p>
 723      * Another option to define a cue point in a {@code Animation} is to set the
 724      * {@link KeyFrame#name} property of a {@link KeyFrame}.
 725      *
 726      * @return {@link javafx.collections.ObservableMap} of cue points
 727      */
 728     public final ObservableMap<String, Duration> getCuePoints() {
 729         return cuePoints;
 730     }
 731 
 732     /**
 733      * Jumps to a given position in this {@code Animation}.
 734      *
 735      * If the given time is less than {@link Duration#ZERO}, this method will
 736      * jump to the start of the animation. If the given time is larger than the
 737      * duration of this {@code Animation}, this method will jump to the end.
 738      *
 739      * @param time
 740      *            the new position
 741      * @throws NullPointerException
 742      *             if {@code time} is {@code null}
 743      * @throws IllegalArgumentException
 744      *             if {@code time} is {@link Duration#UNKNOWN}
 745      * @throws IllegalStateException
 746      *             if embedded in another animation,
 747      *                such as {@link SequentialTransition} or {@link ParallelTransition}
 748      */
 749     public void jumpTo(Duration time) {
 750         if (time == null) {
 751             throw new NullPointerException("Time needs to be specified.");
 752         }
 753         if (time.isUnknown()) {
 754             throw new IllegalArgumentException("The time is invalid");
 755         }
 756         if (parent != null) {
 757             throw new IllegalStateException("Cannot jump when embedded in another animation");
 758         }
 759 
 760         lastPlayedFinished = false;
 761 
 762         final Duration totalDuration = getTotalDuration();
 763         time = time.lessThan(Duration.ZERO) ? Duration.ZERO : time
 764                 .greaterThan(totalDuration) ? totalDuration : time;
 765         final long ticks = fromDuration(time);
 766 
 767         if (getStatus() == Status.STOPPED) {
 768             syncClipEnvelope();
 769         }
 770         clipEnvelope.jumpTo(ticks);
 771     }
 772 
 773     /**
 774      * Jumps to a predefined position in this {@code Animation}. This method
 775      * looks for an entry in cue points and jumps to the associated
 776      * position, if it finds one.
 777      * <p>
 778      * If the cue point is behind the end of this {@code Animation}, calling
 779      * {@code jumpTo} will result in a jump to the end. If the cue point has a
 780      * negative {@link javafx.util.Duration} it will result in a jump to the
 781      * beginning. If the cue point has a value of
 782      * {@link javafx.util.Duration#UNKNOWN} calling {@code jumpTo} will have no
 783      * effect for this cue point.
 784      * <p>
 785      * There are two predefined cue points {@code "start"} and {@code "end"}
 786      * which are defined to be at the start respectively the end of this
 787      * {@code Animation}.
 788      *
 789      * @param cuePoint
 790      *            the name of the cue point
 791      * @throws NullPointerException
 792      *             if {@code cuePoint} is {@code null}
 793      * @throws IllegalStateException
 794      *             if embedded in another animation,
 795      *                such as {@link SequentialTransition} or {@link ParallelTransition}
 796      * @see #getCuePoints()
 797      */
 798     public void jumpTo(String cuePoint) {
 799         if (cuePoint == null) {
 800             throw new NullPointerException("CuePoint needs to be specified");
 801         }
 802         if ("start".equalsIgnoreCase(cuePoint)) {
 803             jumpTo(Duration.ZERO);
 804         } else if ("end".equalsIgnoreCase(cuePoint)) {
 805             jumpTo(getTotalDuration());
 806         } else {
 807             final Duration target = getCuePoints().get(cuePoint);
 808             if (target != null) {
 809                 jumpTo(target);
 810             }
 811         }
 812     }
 813 
 814     /**
 815      * A convenience method to play this {@code Animation} from a predefined
 816      * position. The position has to be predefined in cue points.
 817      * Calling this method is equivalent to
 818      *
 819      * <pre>
 820      * <code>
 821      * animation.jumpTo(cuePoint);
 822      * animation.play();
 823      * </code>
 824      * </pre>
 825      *
 826      * Note that unlike {@link #playFromStart()} calling this method will not
 827      * change the playing direction of this {@code Animation}.
 828      *
 829      * @param cuePoint
 830      *            name of the cue point
 831      * @throws NullPointerException
 832      *             if {@code cuePoint} is {@code null}
 833      * @throws IllegalStateException
 834      *             if embedded in another animation,
 835      *                such as {@link SequentialTransition} or {@link ParallelTransition}
 836      * @see #getCuePoints()
 837      */
 838     public void playFrom(String cuePoint) {
 839         jumpTo(cuePoint);
 840         play();
 841     }
 842 
 843     /**
 844      * A convenience method to play this {@code Animation} from a specific
 845      * position. Calling this method is equivalent to
 846      *
 847      * <pre>
 848      * <code>
 849      * animation.jumpTo(time);
 850      * animation.play();
 851      * </code>
 852      * </pre>
 853      *
 854      * Note that unlike {@link #playFromStart()} calling this method will not
 855      * change the playing direction of this {@code Animation}.
 856      *
 857      * @param time
 858      *            position where to play from
 859      * @throws NullPointerException
 860      *             if {@code time} is {@code null}
 861      * @throws IllegalArgumentException
 862      *             if {@code time} is {@link Duration#UNKNOWN}
 863      * @throws IllegalStateException
 864      *             if embedded in another animation,
 865      *                such as {@link SequentialTransition} or {@link ParallelTransition}
 866      */
 867     public void playFrom(Duration time) {
 868         jumpTo(time);
 869         play();
 870     }
 871 
 872     /**
 873      * Plays {@code Animation} from current position in the direction indicated
 874      * by {@code rate}. If the {@code Animation} is running, it has no effect.
 875      * <p>
 876      * When {@code rate} > 0 (forward play), if an {@code Animation} is already
 877      * positioned at the end, the first cycle will not be played, it is
 878      * considered to have already finished. This also applies to a backward (
 879      * {@code rate} < 0) cycle if an {@code Animation} is positioned at the beginning.
 880      * However, if the {@code Animation} has {@code cycleCount} > 1, following
 881      * cycle(s) will be played as usual.
 882      * <p>
 883      * When the {@code Animation} reaches the end, the {@code Animation} is stopped and
 884      * the play head remains at the end.
 885      * <p>
 886      * To play an {@code Animation} backwards from the end:<br>
 887      * <code>
 888      *  animation.setRate(negative rate);<br>
 889      *  animation.jumpTo(overall duration of animation);<br>
 890      *  animation.play();<br>
 891      * </code>
 892      * <p>
 893      * Note: <ul>
 894      * <li>{@code play()} is an asynchronous call, the {@code Animation} may not
 895      * start immediately. </ul>
 896      *
 897      * @throws IllegalStateException
 898      *             if embedded in another animation,
 899      *                such as {@link SequentialTransition} or {@link ParallelTransition}
 900      */
 901     public void play() {
 902         if (parent != null) {
 903             throw new IllegalStateException("Cannot start when embedded in another animation");
 904         }
 905         switch (getStatus()) {
 906             case STOPPED:
 907                 if (impl_startable(true)) {
 908                     final double rate = getRate();
 909                     if (lastPlayedFinished) {
 910                         jumpTo((rate < 0)? getTotalDuration() : Duration.ZERO);
 911                     }
 912                     impl_start(true);
 913                     startReceiver(TickCalculation.fromDuration(getDelay()));
 914                     if (Math.abs(rate) < EPSILON) {
 915                         pauseReceiver();
 916                     } else {
 917 
 918                     }
 919                 } else {
 920                     final EventHandler<ActionEvent> handler = getOnFinished();
 921                     if (handler != null) {
 922                         handler.handle(new ActionEvent(this, null));
 923                     }
 924                 }
 925                 break;
 926             case PAUSED:
 927                 impl_resume();
 928                 if (Math.abs(getRate()) >= EPSILON) {
 929                     resumeReceiver();
 930                 }
 931                 break;
 932         }
 933     }
 934 
 935     /**
 936      * Plays an {@code Animation} from initial position in forward direction.
 937      * <p>
 938      * It is equivalent to
 939      * <p>
 940      * <code>
 941      *      animation.stop();<br>
 942      *      animation.setRate = setRate(Math.abs(animation.getRate())); </br>
 943      *      animation.jumpTo(Duration.ZERO);<br>
 944      *      animation.play();<br>
 945      *  </code>
 946      *
 947      * <p>
 948      * Note: <ul>
 949      * <li>{@code playFromStart()} is an asynchronous call, {@code Animation} may
 950      * not start immediately. </ul>
 951      * <p>
 952      *
 953      * @throws IllegalStateException
 954      *             if embedded in another animation,
 955      *                such as {@link SequentialTransition} or {@link ParallelTransition}
 956      */
 957     public void playFromStart() {
 958         stop();
 959         setRate(Math.abs(getRate()));
 960         jumpTo(Duration.ZERO);
 961         play();
 962     }
 963 
 964     /**
 965      * Stops the animation and resets the play head to its initial position. If
 966      * the animation is not currently running, this method has no effect.
 967      * <p>
 968      * Note: <ul>
 969      * <li>{@code stop()} is an asynchronous call, the {@code Animation} may not stop
 970      * immediately. </ul>
 971      * @throws IllegalStateException
 972      *             if embedded in another animation,
 973      *                such as {@link SequentialTransition} or {@link ParallelTransition}
 974      */
 975     public void stop() {
 976         if (parent != null) {
 977             throw new IllegalStateException("Cannot stop when embedded in another animation");
 978         }
 979         if (getStatus() != Status.STOPPED) {
 980             clipEnvelope.abortCurrentPulse();
 981             impl_stop();
 982             jumpTo(Duration.ZERO);
 983         }
 984     }
 985 
 986     /**
 987      * Pauses the animation. If the animation is not currently running, this
 988      * method has no effect.
 989      * <p>
 990      * Note: <ul>
 991      * <li>{@code pause()} is an asynchronous call, the {@code Animation} may not pause
 992      * immediately. </ul>
 993      * @throws IllegalStateException
 994      *             if embedded in another animation,
 995      *                such as {@link SequentialTransition} or {@link ParallelTransition}
 996      */
 997     public void pause() {
 998         if (parent != null) {
 999             throw new IllegalStateException("Cannot pause when embedded in another animation");
1000         }
1001         if (getStatus() == Status.RUNNING) {
1002             clipEnvelope.abortCurrentPulse();
1003             pauseReceiver();
1004             impl_pause();
1005         }
1006     }
1007 
1008     /**
1009      * The constructor of {@code Animation}.
1010      *
1011      * This constructor allows to define a target framerate.
1012      *
1013      * @param targetFramerate
1014      *            The custom target frame rate for this {@code Animation}
1015      * @see #getTargetFramerate()
1016      */
1017     protected Animation(double targetFramerate) {
1018         this.targetFramerate = targetFramerate;
1019         this.resolution = (int) Math.max(1, Math.round(TickCalculation.TICKS_PER_SECOND / targetFramerate));
1020         this.clipEnvelope = ClipEnvelope.create(this);
1021         this.timer = Toolkit.getToolkit().getMasterTimer();
1022     }
1023 
1024     /**
1025      * The constructor of {@code Animation}.
1026      */
1027     protected Animation() {
1028         this.resolution = 1;
1029         this.targetFramerate = TickCalculation.TICKS_PER_SECOND / Toolkit.getToolkit().getMasterTimer().getDefaultResolution();
1030         this.clipEnvelope = ClipEnvelope.create(this);
1031         this.timer = Toolkit.getToolkit().getMasterTimer();
1032     }
1033 
1034     // These constructors are only for testing purposes
1035     Animation(AbstractMasterTimer timer) {
1036         this.resolution = 1;
1037         this.targetFramerate = TickCalculation.TICKS_PER_SECOND / timer.getDefaultResolution();
1038         this.clipEnvelope = ClipEnvelope.create(this);
1039         this.timer = timer;
1040     }
1041 
1042     // These constructors are only for testing purposes
1043     Animation(AbstractMasterTimer timer, ClipEnvelope clipEnvelope, int resolution) {
1044         this.resolution = resolution;
1045         this.targetFramerate = TickCalculation.TICKS_PER_SECOND / resolution;
1046         this.clipEnvelope = clipEnvelope;
1047         this.timer = timer;
1048     }
1049 
1050     boolean impl_startable(boolean forceSync) {
1051         return (fromDuration(getCycleDuration()) > 0L)
1052                 || (!forceSync && clipEnvelope.wasSynched());
1053     }
1054 
1055     void impl_sync(boolean forceSync) {
1056         if (forceSync || !clipEnvelope.wasSynched()) {
1057             syncClipEnvelope();
1058         }
1059     }
1060 
1061     private void syncClipEnvelope() {
1062         final int publicCycleCount = getCycleCount();
1063         final int internalCycleCount = (publicCycleCount <= 0)
1064                 && (publicCycleCount != INDEFINITE) ? 1 : publicCycleCount;
1065         clipEnvelope = clipEnvelope.setCycleCount(internalCycleCount);
1066         clipEnvelope.setCycleDuration(getCycleDuration());
1067         clipEnvelope.setAutoReverse(isAutoReverse());
1068     }
1069 
1070     void impl_start(boolean forceSync) {
1071         impl_sync(forceSync);
1072         setStatus(Status.RUNNING);
1073         clipEnvelope.start();
1074         setCurrentRate(clipEnvelope.getCurrentRate());
1075         lastPulse = 0;
1076     }
1077 
1078     void impl_pause() {
1079         final double currentRate = getCurrentRate();
1080         if (Math.abs(currentRate) >= EPSILON) {
1081             lastPlayedForward = Math.abs(getCurrentRate() - getRate()) < EPSILON;
1082         }
1083         setCurrentRate(0.0);
1084         setStatus(Status.PAUSED);
1085     }
1086 
1087     void impl_resume() {
1088         setStatus(Status.RUNNING);
1089         setCurrentRate(lastPlayedForward ? getRate() : -getRate());
1090     }
1091 
1092     void impl_stop() {
1093         if (!paused) {
1094             timer.removePulseReceiver(pulseReceiver);
1095         }
1096         setStatus(Status.STOPPED);
1097         setCurrentRate(0.0);
1098     }
1099 
1100     void impl_timePulse(long elapsedTime) {
1101         if (resolution == 1) { // fullspeed
1102             clipEnvelope.timePulse(elapsedTime);
1103         } else if (elapsedTime - lastPulse >= resolution) {
1104             lastPulse = (elapsedTime / resolution) * resolution;
1105             clipEnvelope.timePulse(elapsedTime);
1106         }
1107     }
1108 
1109     abstract void impl_playTo(long currentTicks, long cycleTicks);
1110 
1111     abstract void impl_jumpTo(long currentTicks, long cycleTicks, boolean forceJump);
1112 
1113     void impl_setCurrentTicks(long ticks) {
1114         currentTicks = ticks;
1115         if (currentTime != null) {
1116             currentTime.fireValueChangedEvent();
1117         }
1118     }
1119 
1120     void impl_setCurrentRate(double currentRate) {
1121 //        if (getStatus() == Status.RUNNING) {
1122             setCurrentRate(currentRate);
1123 //        }
1124     }
1125 
1126     final void impl_finished() {
1127         lastPlayedFinished = true;
1128         impl_stop();
1129         final EventHandler<ActionEvent> handler = getOnFinished();
1130         if (handler != null) {
1131             try {
1132                 handler.handle(new ActionEvent(this, null));
1133             } catch (Exception ex) {
1134                 Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), ex);
1135             }
1136         }
1137     }
1138 }