1111package cz.vutbr.fit.interlockSim.gui
1212
1313import cz.vutbr.fit.interlockSim.context.SimulationContext
14- import cz.vutbr.fit.interlockSim.gui.animation.ControlPanel
1514import io.github.oshai.kotlinlogging.KotlinLogging
1615import java.beans.PropertyChangeListener
17- import javax.swing.SwingUtilities
1816
1917/* *
2018 * Manages the simulation lifecycle on behalf of [Frame] (Issue #189).
@@ -27,30 +25,26 @@ import javax.swing.SwingUtilities
2725 * - Starting the runner synchronously (before the monitor thread) to prevent the
2826 * race condition where [stop] is called before the monitor thread starts the runner
2927 * - Polling for completion on a daemon "SimulationMonitor" thread
30- * - Enabling/disabling the Stop button in [ControlPanel] as the lifecycle changes
31- * - Dispatching [onCompleted] back to EDT when the simulation finishes naturally
28+ * - Reporting lifecycle and speed changes via callbacks
3229 *
3330 * ## Thread Safety
34- * - [start] and [stop] are designed to be called from the same thread (typically EDT
35- * in production, but also from test threads in unit tests). They are NOT thread-safe
36- * for concurrent calls from different threads; external callers are responsible for
37- * serialization. [Frame] enforces EDT-only access via its own `require()` guards.
31+ * - [start] and [stop] are designed to be called from the same thread. They are NOT
32+ * thread-safe for concurrent calls from different threads; external callers are
33+ * responsible for serialization.
3834 * - [runner] is `@Volatile` so the monitor thread reads a fresh value when [stop]
3935 * nulls it.
40- * - [onCompleted] is always dispatched to EDT via [SwingUtilities.invokeLater] .
36+ * - Callbacks are invoked on whichever thread performs the lifecycle change .
4137 *
42- * @param controlPanel ControlPanel whose Stop button and status label are managed here.
43- * @param toolBar Optional [ToolBar] to show/hide simulation controls on lifecycle changes.
44- * @param statusBar Optional [StatusBar] to display speed indicator when speed != 1.0x.
45- * @param onCompleted Callback invoked on EDT when the simulation finishes naturally.
46- * Defaults to a no-op if not provided.
38+ * @param onStateChanged Callback for lifecycle state updates.
39+ * @param onSpeedChanged Callback for speed indicator updates.
40+ * @param onCompleted Callback invoked when the simulation finishes naturally.
41+ * Defaults to a no-op.
4742 * @since 2026-04-20 (extracted from Frame for testability)
4843 * @see Frame
4944 */
5045internal class SimulationController (
51- private val controlPanel : ControlPanel ,
52- private val toolBar : ToolBar ? = null ,
53- private val statusBar : StatusBar ? = null ,
46+ private val onStateChanged : (SimulationStatus ) -> Unit = {},
47+ private val onSpeedChanged : (Double ) -> Unit = {},
5448 private val onCompleted : () -> Unit = {},
5549) {
5650 /* *
@@ -82,9 +76,9 @@ internal class SimulationController(
8276 * 2. Calls [SimulationRunner.start] **synchronously** (before the monitor thread) to
8377 * eliminate the race condition where [stop] could be invoked before the monitor
8478 * thread has a chance to start the runner.
85- * 3. Updates [ControlPanel] status to "Running" and enables the Stop button .
79+ * 3. Emits [SimulationStatus.RUNNING] via [onStateChanged] .
8680 * 4. Launches a daemon "SimulationMonitor" thread that polls [SimulationRunner.isRunning]
87- * and on completion dispatches [onCompleted ] and resets the panel via EDT .
81+ * and on completion emits [SimulationStatus.STOPPED ] and invokes [onCompleted] .
8882 *
8983 * @param context The simulation context to run.
9084 */
@@ -103,35 +97,30 @@ internal class SimulationController(
10397 // thread. This ensures stopSimulation() always has a live thread to interrupt.
10498 newRunner.start()
10599
106- // Wire speed indicator: notify StatusBar whenever SimulationRunner speed changes.
100+ // Wire speed callback for SimulationRunner speed changes.
107101 // The listener is removed when the simulation stops (in stop() or monitor finally).
108102 val listener = PropertyChangeListener { evt ->
109103 val multiplier = evt.newValue as ? Double
110104 if (multiplier == null ) {
111105 logger.debug { " Ignoring unexpected ${SimulationRunner .PROP_SPEED_MULTIPLIER } value: ${evt.newValue} " }
112106 return @PropertyChangeListener
113107 }
114- statusBar?.updateSpeedIndicator (multiplier)
108+ onSpeedChanged (multiplier)
115109 }
116110 speedListener = listener
117111 newRunner.addPropertyChangeListener(SimulationRunner .PROP_SPEED_MULTIPLIER , listener)
118112
119- // Show simulation controls in ToolBar (EDT-safe: start() is called from EDT).
120- toolBar?.showSimulationControls()
121-
122- controlPanel.updateStatus(ControlPanel .SimulationStatus .RUNNING )
123- controlPanel.setStopEnabled(true )
113+ onStateChanged(SimulationStatus .RUNNING )
124114
125115 launchMonitorThread(newRunner)
126116 }
127117
128118 /* *
129119 * Launch a daemon "SimulationMonitor" thread that polls [newRunner] for completion
130- * and dispatches panel reset and [onCompleted] to EDT when done.
120+ * and dispatches callback notifications when done.
131121 *
132- * Guards against stale-monitor: the [SwingUtilities.invokeLater] callback checks
133- * `runner === newRunner` before mutating state so that a stop+start cycle started
134- * before the lambda fires cannot clobber the new run's panel state.
122+ * Guards against stale-monitor by checking `runner === newRunner` before mutating
123+ * state so that a stop+start cycle cannot clobber the new run's state.
135124 */
136125 private fun launchMonitorThread (newRunner : SimulationRunner ) {
137126 val monitorThread =
@@ -144,20 +133,15 @@ internal class SimulationController(
144133 } catch (e: InterruptedException ) {
145134 Thread .currentThread().interrupt()
146135 } finally {
147- SwingUtilities .invokeLater {
148- // Guard against stale-monitor: if stop() + start(ctxB) ran on EDT
149- // before this callback fired, runner has been replaced with a new
150- // instance. Skip the reset to avoid clobbering the new run's panel
151- // state (and avoid firing onCompleted for the old run).
152- if (runner == = newRunner) {
153- cleanupSpeedListener(newRunner)
154- runner = null
155- toolBar?.hideSimulationControls()
156- statusBar?.updateSpeedIndicator(SimulationRunner .DEFAULT_SPEED )
157- controlPanel.updateStatus(ControlPanel .SimulationStatus .STOPPED )
158- controlPanel.setStopEnabled(false )
159- onCompleted()
160- }
136+ // Guard against stale-monitor: if stop() + start(ctxB) ran before
137+ // this callback executes, runner has been replaced with a new instance.
138+ // Skip reset to avoid clobbering the new run's state.
139+ if (runner == = newRunner) {
140+ cleanupSpeedListener(newRunner)
141+ runner = null
142+ onSpeedChanged(SimulationRunner .DEFAULT_SPEED )
143+ onStateChanged(SimulationStatus .STOPPED )
144+ onCompleted()
161145 }
162146 }
163147 },
@@ -168,7 +152,7 @@ internal class SimulationController(
168152 }
169153
170154 /* *
171- * Stop a running simulation and reset the [ControlPanel ].
155+ * Stop a running simulation and emit [SimulationStatus.STOPPED ].
172156 *
173157 * Safe to call when no simulation is running (no-op in that case).
174158 */
@@ -177,10 +161,8 @@ internal class SimulationController(
177161 cleanupSpeedListener(r)
178162 r.stop()
179163 runner = null
180- toolBar?.hideSimulationControls()
181- statusBar?.updateSpeedIndicator(SimulationRunner .DEFAULT_SPEED )
182- controlPanel.setStopEnabled(false )
183- controlPanel.updateStatus(ControlPanel .SimulationStatus .STOPPED )
164+ onSpeedChanged(SimulationRunner .DEFAULT_SPEED )
165+ onStateChanged(SimulationStatus .STOPPED )
184166 }
185167
186168 /* * Removes the speed [PropertyChangeListener] from [r] and clears the reference. */
@@ -215,4 +197,10 @@ internal class SimulationController(
215197 /* * Poll interval (ms) for the monitor thread to detect simulation completion. */
216198 internal const val SIMULATION_POLL_INTERVAL_MS : Long = 500L
217199 }
200+
201+ /* * Simulation lifecycle states emitted via [onStateChanged]. */
202+ enum class SimulationStatus {
203+ RUNNING ,
204+ STOPPED ,
205+ }
218206}
0 commit comments