Skip to content

Commit c4f8f09

Browse files
CopilotbedaHovorka
andcommitted
Add Simulation menu with Start/Stop/Speed control to MenuBar
Agent-Logs-Url: https://github.com/bedaHovorka/interlockSim/sessions/69dde112-6219-41c1-8517-555ceb9dcb6e Co-authored-by: bedaHovorka <5263405+bedaHovorka@users.noreply.github.com>
1 parent 8e25be5 commit c4f8f09

3 files changed

Lines changed: 276 additions & 9 deletions

File tree

desktop-ui/src/main/kotlin/cz/vutbr/fit/interlockSim/gui/MenuBar.kt

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,20 @@ package cz.vutbr.fit.interlockSim.gui
1212

1313
import cz.vutbr.fit.interlockSim.context.EditingContext
1414
import cz.vutbr.fit.interlockSim.context.JvmEditingContextFactory
15+
import cz.vutbr.fit.interlockSim.context.SimulationContext
16+
import cz.vutbr.fit.interlockSim.context.SimulationContextFactory
1517
import cz.vutbr.fit.interlockSim.xml.XMLContextFactory
1618
import org.koin.mp.KoinPlatform.getKoin
1719
import java.awt.event.ActionEvent
20+
import java.awt.event.KeyEvent
1821
import java.io.File
1922
import javax.swing.AbstractAction
2023
import javax.swing.JFileChooser
2124
import javax.swing.JMenu
2225
import javax.swing.JMenuBar
26+
import javax.swing.JMenuItem
2327
import javax.swing.JOptionPane
28+
import javax.swing.KeyStroke
2429

2530
/**
2631
* Application menu bar with File and Help menus
@@ -265,6 +270,57 @@ class MenuBar : JMenuBar() {
265270
}
266271
}
267272

273+
/**
274+
* Shows a file chooser, loads the selected XML as a [SimulationContext], sets it on the
275+
* [Frame] and immediately starts the simulation.
276+
*/
277+
private inner class StartSimulationAction : AbstractAction("Start...") {
278+
override fun actionPerformed(e: ActionEvent) {
279+
val fileChooser = JFileChooser(System.getProperty("user.dir"))
280+
fileChooser.dialogTitle = "Start Simulation"
281+
282+
val returnValue = fileChooser.showOpenDialog(this@MenuBar)
283+
if (returnValue != JFileChooser.APPROVE_OPTION) return
284+
285+
val selectedFile: File = fileChooser.selectedFile
286+
287+
try {
288+
val simulationContextFactory = getKoin().get<SimulationContextFactory>()
289+
val simContext = simulationContextFactory.createContext(selectedFile) as SimulationContext
290+
val frame = getKoin().get<Frame>()
291+
frame.setContext(simContext)
292+
frame.startSimulation()
293+
} catch (exception: Exception) {
294+
JOptionPane.showMessageDialog(
295+
this@MenuBar,
296+
"Failed to start simulation: ${exception.message}\n\n" +
297+
"Ensure the file is a valid railway network XML.",
298+
"Cannot Start Simulation",
299+
JOptionPane.ERROR_MESSAGE
300+
)
301+
}
302+
}
303+
}
304+
305+
/** Terminates the currently running simulation via [Frame.stopSimulation]. */
306+
private inner class StopSimulationAction : AbstractAction("Stop") {
307+
override fun actionPerformed(e: ActionEvent) {
308+
val frame = getKoin().get<Frame>()
309+
frame.stopSimulation()
310+
}
311+
}
312+
313+
/** Sets the simulation speed multiplier via [SimulationController.setSpeed]. */
314+
private inner class SetSpeedAction(
315+
private val label: String,
316+
private val multiplier: Double,
317+
) : AbstractAction(label) {
318+
override fun actionPerformed(e: ActionEvent) {
319+
val frame = getKoin().get<Frame>()
320+
frame.simulationController.setSpeed(multiplier)
321+
}
322+
}
323+
268324
private inner class InfoAction(
269325
private val infoName: String,
270326
private val text: String
@@ -276,6 +332,7 @@ class MenuBar : JMenuBar() {
276332

277333
init {
278334
add(fileMenu())
335+
add(simulationMenu())
279336
add(helpMenu())
280337
}
281338

@@ -289,6 +346,37 @@ class MenuBar : JMenuBar() {
289346
return menu
290347
}
291348

349+
/**
350+
* Builds the "Simulation" menu with Start/Stop actions and a Speed submenu.
351+
*
352+
* Speed presets (0.1x, 0.5x, 1x, 2x, 10x) have keyboard accelerators (keys 1–5)
353+
* so that the user can change the speed without reaching for the mouse during a run.
354+
*/
355+
private fun simulationMenu(): JMenu {
356+
val menu = JMenu("Simulation")
357+
menu.add(StartSimulationAction())
358+
menu.add(StopSimulationAction())
359+
menu.addSeparator()
360+
361+
val speedMenu = JMenu("Speed")
362+
val speedPresets =
363+
listOf(
364+
Triple("0.1x", 0.1, KeyEvent.VK_1),
365+
Triple("0.5x", 0.5, KeyEvent.VK_2),
366+
Triple("1x", 1.0, KeyEvent.VK_3),
367+
Triple("2x", 2.0, KeyEvent.VK_4),
368+
Triple("10x", 10.0, KeyEvent.VK_5),
369+
)
370+
for ((label, multiplier, keyCode) in speedPresets) {
371+
val item = JMenuItem(SetSpeedAction(label, multiplier))
372+
item.accelerator = KeyStroke.getKeyStroke(keyCode, 0)
373+
speedMenu.add(item)
374+
}
375+
menu.add(speedMenu)
376+
377+
return menu
378+
}
379+
292380
private fun helpMenu(): JMenu {
293381
val menu = JMenu("Help")
294382
menu.add(
@@ -300,7 +388,16 @@ class MenuBar : JMenuBar() {
300388
"<br><b>Editing:</b><br>" +
301389
"- Left mouse: Insert nodes and join them<br>" +
302390
"- Middle mouse: Delete nodes<br>" +
303-
"- Right mouse: Popup menu</html>"
391+
"- Right mouse: Popup menu<br>" +
392+
"<br><b>Simulation:</b><br>" +
393+
"- Simulation &gt; Start...: Load XML and start simulation<br>" +
394+
"- Simulation &gt; Stop: Terminate running simulation<br>" +
395+
"<br><b>Simulation Speed (keyboard shortcuts):</b><br>" +
396+
"- Key 1: 0.1x speed<br>" +
397+
"- Key 2: 0.5x speed<br>" +
398+
"- Key 3: 1x speed (real-time)<br>" +
399+
"- Key 4: 2x speed<br>" +
400+
"- Key 5: 10x speed</html>"
304401
)
305402
)
306403
menu.add(

desktop-ui/src/main/kotlin/cz/vutbr/fit/interlockSim/gui/SimulationController.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@ internal class SimulationController(
5757
var runner: SimulationRunner? = null
5858
private set
5959

60+
/**
61+
* Desired speed multiplier applied to new and currently running simulations.
62+
*
63+
* Stored so that a speed selection before [start] is honoured once the runner is created.
64+
*/
65+
private var desiredSpeed: Double = SimulationRunner.DEFAULT_SPEED
66+
6067
/**
6168
* Start the simulation for [context].
6269
*
@@ -81,6 +88,7 @@ internal class SimulationController(
8188
}
8289

8390
val newRunner = SimulationRunner(context)
91+
newRunner.speedMultiplier = desiredSpeed
8492
runner = newRunner
8593

8694
// Start synchronously BEFORE enabling the Stop button or launching the monitor
@@ -148,6 +156,23 @@ internal class SimulationController(
148156
/** Returns `true` while the underlying [SimulationRunner] reports running. */
149157
fun isRunning(): Boolean = runner?.isRunning() ?: false
150158

159+
/**
160+
* Set the simulation speed multiplier.
161+
*
162+
* Applied immediately to the currently running simulation (if any) and stored
163+
* so it is also honoured by the next [start] call.
164+
*
165+
* @param multiplier Speed factor in [SimulationRunner.MIN_SPEED]..[SimulationRunner.MAX_SPEED].
166+
* @throws IllegalArgumentException if [multiplier] is outside the valid range.
167+
*/
168+
fun setSpeed(multiplier: Double) {
169+
require(multiplier in SimulationRunner.MIN_SPEED..SimulationRunner.MAX_SPEED) {
170+
"speedMultiplier must be in [${SimulationRunner.MIN_SPEED}..${SimulationRunner.MAX_SPEED}], got: $multiplier"
171+
}
172+
desiredSpeed = multiplier
173+
runner?.speedMultiplier = multiplier
174+
}
175+
151176
companion object {
152177
private val logger = KotlinLogging.logger {}
153178

0 commit comments

Comments
 (0)