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,45 @@ return null; } + private List scenePulseListeners; + + /** + * Adds a new scene pulse listener to this scene. Every time a pulse occurs, + * this listener will be called on the JavaFX Application Thread directly + * following 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 the + * {@link javafx.animation.AnimationTimer} API. + * + * @param r The Runnable to be called when the pulse occurs. + */ + public final void addScenePulseListener(Runnable r) { + if (r == null) { + throw new IllegalArgumentException("Scene pulse listener should not be null"); + } + if (scenePulseListeners == null) { + scenePulseListeners = new CopyOnWriteArrayList<>(); + } + scenePulseListeners.add(r); + } + + /** + * Removes a previously registered scene pulse listener from listening to + * pulses in this scene. + * + * @param r The Runnable that should no longer be called when this scene pulses. + */ + public final void removeScenePulseListener(Runnable r) { + if (scenePulseListeners == null) { + return; + } + scenePulseListeners.remove(r); + } + /** * Return the defined {@code SceneAntialiasing} for this {@code Scene}. *

@@ -2396,6 +2436,14 @@ } Scene.this.doLayoutPass(); + // run any scene pulse listeners immediately after css / layout, + // and before scene synchronization + if (scenePulseListeners != null) { + for (Runnable r : scenePulseListeners) { + r.run(); + } + } + boolean dirty = dirtyNodes == null || dirtyNodesSize != 0 || !isDirtyEmpty(); if (dirty) { if (PULSE_LOGGING_ENABLED) {