diff --git a/reactfx/src/main/java/org/reactfx/EventStream.java b/reactfx/src/main/java/org/reactfx/EventStream.java index 7fc9550..ae40c11 100644 --- a/reactfx/src/main/java/org/reactfx/EventStream.java +++ b/reactfx/src/main/java/org/reactfx/EventStream.java @@ -19,6 +19,8 @@ import javafx.concurrent.Task; import org.reactfx.util.Either; +import org.reactfx.util.FxTimer; +import org.reactfx.util.Timer; /** * Stream of values (events). @@ -335,7 +337,7 @@ default AwaitingEventStream reduceSuccessions( } Function timerFactory = - action -> new FxTimer(timeout, action); + action -> FxTimer.create(timeout, action); return new SuccessionReducingStream( this, initialTransformation, reduction, timerFactory); } diff --git a/reactfx/src/main/java/org/reactfx/Timer.java b/reactfx/src/main/java/org/reactfx/ScheduledExecutorServiceTimer.java similarity index 62% rename from reactfx/src/main/java/org/reactfx/Timer.java rename to reactfx/src/main/java/org/reactfx/ScheduledExecutorServiceTimer.java index 3f058c3..bcfbc8b 100644 --- a/reactfx/src/main/java/org/reactfx/Timer.java +++ b/reactfx/src/main/java/org/reactfx/ScheduledExecutorServiceTimer.java @@ -5,46 +5,7 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import javafx.animation.Animation; -import javafx.animation.KeyFrame; -import javafx.animation.Timeline; -import javafx.util.Duration; - -interface Timer { - void restart(); - void stop(); -} - -class FxTimer implements Timer { - private final Animation animation; - private final Runnable action; - - private long seq = 0; - - FxTimer(java.time.Duration timeout, Runnable action) { - Duration fxTimeout = Duration.millis(timeout.toMillis()); - this.animation = new Timeline(new KeyFrame(fxTimeout)); - this.action = action; - } - - @Override - public void restart() { - stop(); - long expected = seq; - animation.setOnFinished(ae -> { - if(seq == expected) { - action.run(); - } - }); - animation.play(); - } - - @Override - public void stop() { - animation.stop(); - ++seq; - } -} +import org.reactfx.util.Timer; class ScheduledExecutorServiceTimer implements Timer { private final long timeout; diff --git a/reactfx/src/main/java/org/reactfx/SuccessionReducingStream.java b/reactfx/src/main/java/org/reactfx/SuccessionReducingStream.java index e19f379..a5e6e4e 100644 --- a/reactfx/src/main/java/org/reactfx/SuccessionReducingStream.java +++ b/reactfx/src/main/java/org/reactfx/SuccessionReducingStream.java @@ -3,6 +3,8 @@ import java.util.function.BiFunction; import java.util.function.Function; +import org.reactfx.util.Timer; + import javafx.beans.binding.BooleanBinding; import javafx.beans.value.ObservableBooleanValue; diff --git a/reactfx/src/main/java/org/reactfx/util/FxTimer.java b/reactfx/src/main/java/org/reactfx/util/FxTimer.java new file mode 100644 index 0000000..29c3042 --- /dev/null +++ b/reactfx/src/main/java/org/reactfx/util/FxTimer.java @@ -0,0 +1,78 @@ +package org.reactfx.util; + +import javafx.animation.Animation; +import javafx.animation.KeyFrame; +import javafx.animation.Timeline; +import javafx.util.Duration; + +/** + * Provides factory methods for timers that are manipulated from and execute + * their action on the JavaFX application thread. + */ +public class FxTimer implements Timer { + + /** + * Prepares a (stopped) timer with the given delay and action. + */ + public static Timer create(java.time.Duration delay, Runnable action) { + return new FxTimer(delay, action, 1); + } + + /** + * Equivalent to {@code create(delay, action).restart()}. + */ + public static Timer runLater(java.time.Duration delay, Runnable action) { + Timer timer = create(delay, action); + timer.restart(); + return timer; + } + + /** + * Prepares a (stopped) timer that executes the given action periodically + * with the given interval. + */ + public static Timer createPeriodic(java.time.Duration interval, Runnable action) { + return new FxTimer(interval, action, Animation.INDEFINITE); + } + + /** + * Equivalent to {@code createPeriodic(interval, action).restart()}. + */ + public static Timer runPeriodically(java.time.Duration interval, Runnable action) { + Timer timer = createPeriodic(interval, action); + timer.restart(); + return timer; + } + + private final Duration timeout; + private final Timeline timeline; + private final Runnable action; + + private long seq = 0; + + private FxTimer(java.time.Duration timeout, Runnable action, int cycles) { + this.timeout = Duration.millis(timeout.toMillis()); + this.timeline = new Timeline(); + this.action = action; + + timeline.setCycleCount(cycles); + } + + @Override + public void restart() { + stop(); + long expected = seq; + timeline.getKeyFrames().setAll(new KeyFrame(timeout, ae -> { + if(seq == expected) { + action.run(); + } + })); + timeline.play(); + } + + @Override + public void stop() { + timeline.stop(); + ++seq; + } +} \ No newline at end of file diff --git a/reactfx/src/main/java/org/reactfx/util/Timer.java b/reactfx/src/main/java/org/reactfx/util/Timer.java new file mode 100644 index 0000000..3f6c07c --- /dev/null +++ b/reactfx/src/main/java/org/reactfx/util/Timer.java @@ -0,0 +1,64 @@ +package org.reactfx.util; + + +/** + * Timer represents a delayed action. This means that every timer has an + * associated action and an associated delay. Action and delay are specified + * on timer creation. + * + *

Every timer also has an associated thread (such as JavaFX application + * thread or a single-thread executor's thread). Timer may only be accessed + * from its associated thread. Timer's action is executed on its associated + * thread, too. This design allows to implement guarantees provided by + * {@link #stop()}. + */ +public interface Timer { + /** + * Schedules the associated action to be executed after the associated + * delay. If the action is already scheduled but hasn't been executed yet, + * the timeout is reset, so that the action won't be executed before the + * full delay from now. + */ + void restart(); + + /** + * If the associated action has been scheduled for execution but not yet + * executed, this method prevents it from being executed at all. This is + * also true in case the timer's timeout has already expired, but the + * associated action hasn't had a chance to be executed on the associated + * thread. Note that this is a stronger guarantee than the one given by + * {@link javafx.animation.Animation#stop()}: + * + *

+     * {@code
+     * Timeline timeline = new Timeline(new KeyFrame(
+     *         Duration.millis(1000),
+     *         ae -> System.out.println("FIRED ANYWAY")));
+     * timeline.play();
+     *
+     * // later on the JavaFX application thread,
+     * // but still before the action has been executed
+     * timeline.stop();
+     *
+     * // later, "FIRED ANYWAY" may still be printed
+     * }
+     * 
+ * + * In contrast, using the {@link FxTimer}, the action is guaranteed not to + * be executed after {@code stop()}: + *
+     * {@code
+     * Timer timer = FxTimer.runLater(
+     *         Duration.ofMillis(1000),
+     *         () -> System.out.println("FIRED"));
+     *
+     * // later on the JavaFX application thread,
+     * // but still before the action has been executed
+     * timer.stop();
+     *
+     * // "FIRED" is guaranteed *not* to be printed
+     * }
+     * 
+ */ + void stop(); +} \ No newline at end of file