From 581ec195b73ce6dc4fb68f8c50bd8b9e3563276e Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Fri, 27 Mar 2026 15:58:28 +0200 Subject: Fix CLI replay startup on EDT --- src/main/java/simulator/VSMain.java | 67 +++++++++++++++++++++++++++++++-- src/test/java/simulator/VSMainTest.java | 54 ++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 3 deletions(-) diff --git a/src/main/java/simulator/VSMain.java b/src/main/java/simulator/VSMain.java index 5718b72..3332e85 100644 --- a/src/main/java/simulator/VSMain.java +++ b/src/main/java/simulator/VSMain.java @@ -1,9 +1,13 @@ package simulator; import java.awt.Component; +import java.lang.reflect.InvocationTargetException; import java.util.Locale; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; import javax.swing.UIManager; +import javax.swing.SwingUtilities; import events.VSRegisteredEvents; import prefs.VSDefaultPrefs; @@ -16,6 +20,10 @@ import prefs.VSPrefs; * @author Paul C. Buetow */ public class VSMain { + interface SimulatorFrameFactory { + VSSimulatorFrame create(VSPrefs prefs, Component relativeTo); + } + /** The global preferences */ public static VSPrefs prefs; @@ -65,6 +73,61 @@ public class VSMain { return filename.isEmpty() ? null : filename; } + static VSSimulatorFrame launchSimulatorFrame(VSPrefs prefs, + Component relativeTo, + String startupSimulationFile) { + return launchSimulatorFrame(prefs, relativeTo, startupSimulationFile, + new SimulatorFrameFactory() { + public VSSimulatorFrame create(VSPrefs framePrefs, + Component frameRelativeTo) { + return new VSSimulatorFrame(framePrefs, frameRelativeTo); + } + }); + } + + static VSSimulatorFrame launchSimulatorFrame(VSPrefs prefs, + Component relativeTo, + String startupSimulationFile, + SimulatorFrameFactory factory) { + Objects.requireNonNull(prefs, "prefs"); + Objects.requireNonNull(factory, "factory"); + + AtomicReference frameRef = + new AtomicReference(); + Runnable openWindow = new Runnable() { + public void run() { + VSSimulatorFrame simulatorFrame = + factory.create(prefs, relativeTo); + frameRef.set(simulatorFrame); + if (startupSimulationFile != null) + simulatorFrame.openAndStartSimulator(startupSimulationFile); + } + }; + + runOnEventDispatchThread(openWindow); + return frameRef.get(); + } + + static void runOnEventDispatchThread(Runnable action) { + Objects.requireNonNull(action, "action"); + + if (SwingUtilities.isEventDispatchThread()) { + action.run(); + return; + } + + try { + SwingUtilities.invokeAndWait(action); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Interrupted while launching UI", + e); + } catch (InvocationTargetException e) { + throw new IllegalStateException("Failed to launch UI", + e.getCause()); + } + } + /** * The main method. * @@ -93,9 +156,7 @@ public class VSMain { Thread.currentThread().interrupt(); } - VSSimulatorFrame simulatorFrame = new VSSimulatorFrame(prefs, null); String startupSimulationFile = resolveStartupSimulationFile(args); - if (startupSimulationFile != null) - simulatorFrame.openAndStartSimulator(startupSimulationFile); + launchSimulatorFrame(prefs, null, startupSimulationFile); } } diff --git a/src/test/java/simulator/VSMainTest.java b/src/test/java/simulator/VSMainTest.java index 5d4fb8b..8f4bffc 100644 --- a/src/test/java/simulator/VSMainTest.java +++ b/src/test/java/simulator/VSMainTest.java @@ -1,10 +1,22 @@ package simulator; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; + +import java.awt.Component; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.Test; +import prefs.VSDefaultPrefs; +import prefs.VSPrefs; + public class VSMainTest { @Test void resolveStartupSimulationFileReturnsNullForMissingArgs() { @@ -24,4 +36,46 @@ public class VSMainTest { new String[] {" saved-simulations/raft.dat ", "ignored"})); } + + @Test + void runOnEventDispatchThreadExecutesOnSwingEdt() { + AtomicBoolean ranOnEdt = new AtomicBoolean(false); + + VSMain.runOnEventDispatchThread(new Runnable() { + public void run() { + ranOnEdt.set(javax.swing.SwingUtilities + .isEventDispatchThread()); + } + }); + + assertTrue(ranOnEdt.get()); + } + + @Test + void launchSimulatorFrameCreatesAndStartsOnSwingEdt() { + VSPrefs prefs = VSDefaultPrefs.init(); + AtomicBoolean createdOnEdt = new AtomicBoolean(false); + AtomicReference openedFilename = new AtomicReference(); + VSSimulatorFrame frame = mock(VSSimulatorFrame.class); + doAnswer(invocation -> { + openedFilename.set(invocation.getArgument(0, String.class)); + return null; + }).when(frame).openAndStartSimulator("saved-simulations/raft.dat"); + + VSSimulatorFrame launchedFrame = VSMain.launchSimulatorFrame( + prefs, null, "saved-simulations/raft.dat", + new VSMain.SimulatorFrameFactory() { + public VSSimulatorFrame create(VSPrefs framePrefs, + Component relativeTo) { + createdOnEdt.set(javax.swing.SwingUtilities + .isEventDispatchThread()); + return frame; + } + }); + + assertTrue(createdOnEdt.get()); + assertNotNull(launchedFrame); + assertSame(frame, launchedFrame); + assertEquals("saved-simulations/raft.dat", openedFilename.get()); + } } -- cgit v1.2.3