Skip to content

Instantly share code, notes, and snippets.

@tresf
Created January 18, 2020 06:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tresf/35b4db3b75ea8b86fcd9d98d4ae38923 to your computer and use it in GitHub Desktop.
Save tresf/35b4db3b75ea8b86fcd9d98d4ae38923 to your computer and use it in GitHub Desktop.
Trying to fix race conditions in WebView
import com.sun.javafx.tk.TKPulseListener;
import com.sun.javafx.tk.Toolkit;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.Scene;
import javafx.scene.SnapshotParameters;
import javafx.scene.image.WritableImage;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Logger;
public class WebViewResize extends Application {
private static WebViewResize instance = null;
private static AtomicBoolean started = new AtomicBoolean(false);
private static AtomicBoolean finished = new AtomicBoolean(true);
private static AtomicReference<Throwable> thrown = new AtomicReference<>(null);
private static AtomicReference<BufferedImage> capture = new AtomicReference<>(null);
// our main objects
private static WebView webView = null;
private static Stage stage = null;
private static final int STARTUP_TIMEOUT= 10; // seconds
private static final int STARTUP_SLEEP_INTERVAL = 250; // millis
private static final int CAPTURE_SLEEP_INTERVAL = 10; // millis
private static final Logger log = Logger.getLogger(WebViewResize.class.getName());
/** Called by JavaFX thread */
public WebViewResize() {
instance = this;
}
/** Starts JavaFX thread if not already running */
public static synchronized void initialize() throws IOException {
if (instance == null) {
new Thread() {
public void run() {
Application.launch(WebViewResize.class);
}
}.start();
}
for(int i = 0; i < (STARTUP_TIMEOUT * 1000); i += STARTUP_SLEEP_INTERVAL) {
if (started.get()) { break; }
log.fine("Waiting for JavaFX...");
try { Thread.sleep(STARTUP_SLEEP_INTERVAL); } catch(Exception ignore) {}
}
if (!started.get()) {
throw new IOException("JavaFX did not start");
}
}
@Override
public void start(Stage primaryStage) throws Exception {
started.set(true);
log.fine("Started JavaFX, creating WebView...");
stage = primaryStage;
primaryStage.setScene(new Scene(webView = new WebView()));
Worker<Void> worker = webView.getEngine().getLoadWorker();
worker.stateProperty().addListener(stateListener);
//prevents JavaFX from shutting down when hiding window
Platform.setImplicitExit(false);
}
/** Listens for a SUCCEEDED state to activate image capture **/
private static ChangeListener<Worker.State> stateListener = new ChangeListener<Worker.State>() {
@Override
public void changed(ObservableValue<? extends Worker.State> ov, Worker.State oldState, Worker.State newState) {
if (newState == Worker.State.SUCCEEDED) {
/*try {
Method m = stage.getScene().getClass().getDeclaredMethod("setNeedsRepaint");
m.setAccessible(true);
m.invoke(stage.getScene());
} catch(Throwable t) {
t.printStackTrace();
}*/
final AtomicInteger pulseCount = new AtomicInteger(0);
Toolkit.getToolkit().addStageTkPulseListener(new TKPulseListener() {
@Override
public void pulse() {
Toolkit.getToolkit().removeStageTkPulseListener(this);
System.out.println("1. stagePulse");
snapshot(pulseCount.incrementAndGet());
}
});
Toolkit.getToolkit().addSceneTkPulseListener(new TKPulseListener() {
@Override
public void pulse() {
Toolkit.getToolkit().removeSceneTkPulseListener(this);
System.out.println("2. scenePulse");
snapshot(pulseCount.incrementAndGet());
}
});
Toolkit.getToolkit().addPostSceneTkPulseListener(new TKPulseListener() {
@Override
public void pulse() {
System.out.println("3. postScenePulse");
Toolkit.getToolkit().removePostSceneTkPulseListener(this);
snapshot(pulseCount.incrementAndGet());
}
});
}
}
};
private static void snapshot(int pulseCount) {
if(pulseCount < 3) {
return;
}
System.out.println("4. snapshot");
// start image capture
WritableImage snapshot = webView.snapshot(new SnapshotParameters(), null);
capture.set(SwingFXUtils.fromFXImage(snapshot, null));
finished.set(true);
stage.hide();
}
// TODO: Render when invisible
// https://stackoverflow.com/a/52977547/3196753
// SNAPSHOT Example Code:
// https://stackoverflow.com/questions/30121204
/** Loads the specified HTML, waits for all events, returns a snapshot **/
public static synchronized BufferedImage capture(final String html) throws Throwable {
capture.set(null);
thrown.set(null);
finished.set(false);
// run these actions on the JavaFX thread
Platform.runLater(new Thread() {
public void run() {
try {
webView.getEngine().loadContent(html, "text/html");
stage.show(); // will not capture without showing stage
stage.toBack();
}
catch(Throwable t) {
thrown.set(t);
}
}
});
// wait for capture to complete by monitoring finished state
while(!finished.get() && thrown.get() == null) {
log.fine("Waiting on capture...");
try {
Thread.sleep(CAPTURE_SLEEP_INTERVAL);
}
catch(InterruptedException e) {
log.warning(e.getLocalizedMessage());
}
}
if (thrown.get() != null) {
throw thrown.get();
}
return capture.get();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment