summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-27 15:58:28 +0200
committerPaul Buetow <paul@buetow.org>2026-03-27 15:58:28 +0200
commit581ec195b73ce6dc4fb68f8c50bd8b9e3563276e (patch)
tree1998ef945bdab7f170ddc554d0ae0a4e584588cd
parent2caef29dc99d0bb181506bebe7d880ff6e95954c (diff)
Fix CLI replay startup on EDT
-rw-r--r--src/main/java/simulator/VSMain.java67
-rw-r--r--src/test/java/simulator/VSMainTest.java54
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<VSSimulatorFrame> frameRef =
+ new AtomicReference<VSSimulatorFrame>();
+ 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<String> openedFilename = new AtomicReference<String>();
+ 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());
+ }
}