diff --git a/modules/graphics/src/main/java/javafx/application/Platform.java b/modules/graphics/src/main/java/javafx/application/Platform.java --- a/modules/graphics/src/main/java/javafx/application/Platform.java +++ b/modules/graphics/src/main/java/javafx/application/Platform.java @@ -25,6 +25,7 @@ package javafx.application; +import com.sun.javafx.tk.Toolkit; import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.ReadOnlyBooleanWrapper; import com.sun.javafx.application.PlatformImpl; @@ -89,6 +90,22 @@ // } /** + * Requests the Java Runtime to perform a pulse. This will run a pulse + * even if there are no animation timers, scene graph modifications, + * or window events that would otherwise cause the pulse to run. + * If no pulse is in progress, then one will be scheduled to + * run the next time the pulse timer fires. + * If there is already a pulse running, then + * at least one more pulse after the current pulse will be scheduled. + * This method may be called on any thread. + * + * @since 9 + */ + public static void requestNextPulse() { + Toolkit.getToolkit().requestNextPulse(); + } + + /** * Returns true if the calling thread is the JavaFX Application Thread. * Use this call the ensure that a given task is being executed * (or not being executed) on the JavaFX Application Thread. diff --git a/modules/graphics/src/main/java/javafx/scene/Scene.java b/modules/graphics/src/main/java/javafx/scene/Scene.java --- a/modules/graphics/src/main/java/javafx/scene/Scene.java +++ b/modules/graphics/src/main/java/javafx/scene/Scene.java @@ -89,6 +89,7 @@ import java.security.AccessController; import java.security.PrivilegedAction; import java.util.*; +import java.util.concurrent.CopyOnWriteArrayList; import com.sun.javafx.logging.PulseLogger; @@ -590,6 +591,126 @@ return null; } + private List preLayoutPulseListeners; + private List postLayoutPulseListeners; + + /** + * Adds a new scene pre layout pulse listener to this scene. Every time a pulse occurs, + * this listener will be called on the JavaFX Application Thread directly + * before the CSS and layout passes, and also before + * any rendering is done for + * this frame. This scene pulse listener is suitable for knowing when a + * scenegraph pulse is happening and also for modifying the scenegraph + * (as it is called before CSS and layout, so any changes made will be properly + * styled and positioned). + * + * This method must be called on the JavaFX Application thread. + * + * @param r The Runnable to be called when the pulse occurs. + * + * @throws IllegalStateException if this method is called on a thread + * other than the JavaFX Application Thread. + * + * @throws NullPointerException if the provided Runnable is null. + * + * @since 9 + */ + public final void addPreLayoutPulseListener(Runnable r) { + Toolkit.getToolkit().checkFxUserThread(); + + if (r == null) { + throw new NullPointerException("Scene pulse listener should not be null"); + } + if (preLayoutPulseListeners == null) { + preLayoutPulseListeners = new CopyOnWriteArrayList<>(); + } + preLayoutPulseListeners.add(r); + } + + /** + * Removes a previously registered scene pre layout pulse listener from listening to + * pulses in this scene. This method does nothing if the specified Runnable is + * not already in the list. + * + * This method must be called on the JavaFX Application thread. + * + * @param r The Runnable that should no longer be called when the pulse + * occurs for this scene. + * + * @throws IllegalStateException if this method is called on a thread + * other than the JavaFX Application Thread. + * + * @since 9 + */ + public final void removePreLayoutPulseListener(Runnable r) { + Toolkit.getToolkit().checkFxUserThread(); + + if (preLayoutPulseListeners == null) { + return; + } + preLayoutPulseListeners.remove(r); + } + + /** + * Adds a new scene post layout pulse listener to this scene. Every time a pulse occurs, + * this listener will be called on the JavaFX Application Thread directly + * after the CSS and layout passes, but before any rendering is done for + * this frame. This scene pulse listener is suitable for knowing when a + * scenegraph pulse is happening, but it is not suited to use cases related + * to modifying the scenegraph (as it is called after CSS and layout, so + * any changes will possibly be incorrect until the next pulse is run). + * An alternative (and better) solution for situations where a scenegraph + * modification is required to happen is to use either the + * {@link #addPreLayoutPulseListener(Runnable)} API or the the + * {@link javafx.animation.AnimationTimer} API. + * + * This method must be called on the JavaFX Application thread. + * + * @param r The Runnable to be called when the pulse occurs. + * + * @throws IllegalStateException if this method is called on a thread + * other than the JavaFX Application Thread. + * + * @throws NullPointerException if the provided Runnable is null. + * + * @since 9 + */ + public final void addPostLayoutPulseListener(Runnable r) { + Toolkit.getToolkit().checkFxUserThread(); + + if (r == null) { + throw new NullPointerException("Scene pulse listener should not be null"); + } + if (postLayoutPulseListeners == null) { + postLayoutPulseListeners = new CopyOnWriteArrayList<>(); + } + postLayoutPulseListeners.add(r); + } + + /** + * Removes a previously registered scene post layout pulse listener from listening to + * pulses in this scene. This method does nothing if the specified Runnable is + * not already in the list. + * + * This method must be called on the JavaFX Application thread. + * + * @param r The Runnable that should no longer be called when the pulse + * occurs for this scene. + * + * @throws IllegalStateException if this method is called on a thread + * other than the JavaFX Application Thread. + * + * @since 9 + */ + public final void removePostLayoutPulseListener(Runnable r) { + Toolkit.getToolkit().checkFxUserThread(); + + if (postLayoutPulseListeners == null) { + return; + } + postLayoutPulseListeners.remove(r); + } + /** * Return the defined {@code SceneAntialiasing} for this {@code Scene}. *

@@ -2386,6 +2507,14 @@ disposeAccessibles(); + // run any scene pre pulse listeners immediately _before_ css / layout, + // and before scene synchronization + if (preLayoutPulseListeners != null) { + for (Runnable r : preLayoutPulseListeners) { + r.run(); + } + } + if (PULSE_LOGGING_ENABLED) { PulseLogger.newPhase("CSS Pass"); } @@ -2396,6 +2525,14 @@ } Scene.this.doLayoutPass(); + // run any scene post pulse listeners immediately _after_ css / layout, + // and before scene synchronization + if (postLayoutPulseListeners != null) { + for (Runnable r : postLayoutPulseListeners) { + r.run(); + } + } + boolean dirty = dirtyNodes == null || dirtyNodesSize != 0 || !isDirtyEmpty(); if (dirty) { if (PULSE_LOGGING_ENABLED) { diff --git a/modules/graphics/src/test/java/test/javafx/scene/SceneTest.java b/modules/graphics/src/test/java/test/javafx/scene/SceneTest.java --- a/modules/graphics/src/test/java/test/javafx/scene/SceneTest.java +++ b/modules/graphics/src/test/java/test/javafx/scene/SceneTest.java @@ -25,6 +25,7 @@ package test.javafx.scene; +import com.sun.javafx.runtime.SystemProperties; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.concurrent.Task; @@ -51,7 +52,10 @@ import org.junit.Before; import org.junit.Test; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicInteger; + import javafx.scene.Camera; import javafx.scene.Cursor; import javafx.scene.CursorShim; @@ -856,4 +860,134 @@ */ assertEquals("MyValue", properties2.get("MyKey")); } + + /*************************************************************************** + * * + * Scene Pulse Listener Tests * + * * + **************************************************************************/ + + @Test(expected = NullPointerException.class) + public void testAddNullPreLayoutPulseListener() { + Scene scene = new Scene(new Group(), 300, 300); + scene.addPreLayoutPulseListener(null); + } + + @Test(expected = NullPointerException.class) + public void testAddNullPostLayoutPulseListener() { + Scene scene = new Scene(new Group(), 300, 300); + scene.addPostLayoutPulseListener(null); + } + + // FIXME the thread for some reason is still returning that it is the FX app thread! (Also, duplicate for post-layout) +// @Test(expected = IllegalStateException.class) +// public void testAddPreLayoutPulseListenerOffThread() { +// Scene scene = new Scene(new Group(), 300, 300); +// assertTrue(Platform.isFxApplicationThread()); +// +// final CountDownLatch latch = new CountDownLatch(1); +// new Thread(() -> { +// try { +// assertFalse(Platform.isFxApplicationThread()); +// scene.addPreLayoutPulseListener(() -> { +// }); +// } finally { +// latch.countDown(); +// } +// }).start(); +// try { latch.await(); } catch(Exception e) { } +// } + + + @Test public void testRemoveNullPreLayoutPulseListener_nullListenersList() { + Scene scene = new Scene(new Group(), 300, 300); + scene.removePreLayoutPulseListener(null); + // no failure expected + } + + @Test public void testRemoveNullPostLayoutPulseListener_nullListenersList() { + Scene scene = new Scene(new Group(), 300, 300); + scene.removePostLayoutPulseListener(null); + // no failure expected + } + + @Test public void testRemoveNullPreLayoutPulseListener_nonNullListenersList() { + Scene scene = new Scene(new Group(), 300, 300); + scene.addPreLayoutPulseListener(() -> { }); + scene.removePreLayoutPulseListener(null); + // no failure expected + } + + @Test public void testRemoveNullPostLayoutPulseListener_nonNullListenersList() { + Scene scene = new Scene(new Group(), 300, 300); + scene.addPostLayoutPulseListener(() -> { }); + scene.removePostLayoutPulseListener(null); + // no failure expected + } + + @Test public void testPreLayoutPulseListenerIsFired() { + Scene scene = new Scene(new Group(), 300, 300); + final AtomicInteger counter = new AtomicInteger(0); + + assertEquals(0, counter.get()); + scene.addPreLayoutPulseListener(() -> counter.incrementAndGet()); + assertEquals(0, counter.get()); + + SceneShim.scenePulseListener_pulse(scene); + assertEquals(1, counter.get()); + + SceneShim.scenePulseListener_pulse(scene); + assertEquals(2, counter.get()); + } + + @Test public void testPostLayoutPulseListenerIsFired() { + Scene scene = new Scene(new Group(), 300, 300); + final AtomicInteger counter = new AtomicInteger(0); + + assertEquals(0, counter.get()); + scene.addPostLayoutPulseListener(() -> counter.incrementAndGet()); + assertEquals(0, counter.get()); + + SceneShim.scenePulseListener_pulse(scene); + assertEquals(1, counter.get()); + + SceneShim.scenePulseListener_pulse(scene); + assertEquals(2, counter.get()); + } + + @Test public void testPreLayoutPulseListenerIsFired_untilRemoved() { + Scene scene = new Scene(new Group(), 300, 300); + final AtomicInteger counter = new AtomicInteger(0); + + Runnable r = () -> counter.incrementAndGet(); + + assertEquals(0, counter.get()); + scene.addPreLayoutPulseListener(r); + assertEquals(0, counter.get()); + + SceneShim.scenePulseListener_pulse(scene); + assertEquals(1, counter.get()); + + scene.removePreLayoutPulseListener(r); + SceneShim.scenePulseListener_pulse(scene); + assertEquals(1, counter.get()); + } + + @Test public void testPostLayoutPulseListenerIsFired_untilRemoved() { + Scene scene = new Scene(new Group(), 300, 300); + final AtomicInteger counter = new AtomicInteger(0); + + Runnable r = () -> counter.incrementAndGet(); + + assertEquals(0, counter.get()); + scene.addPostLayoutPulseListener(r); + assertEquals(0, counter.get()); + + SceneShim.scenePulseListener_pulse(scene); + assertEquals(1, counter.get()); + + scene.removePostLayoutPulseListener(r); + SceneShim.scenePulseListener_pulse(scene); + assertEquals(1, counter.get()); + } }