171 lines
7.5 KiB
Python
171 lines
7.5 KiB
Python
import tkinter as tk
|
|
from tkinter import ttk, messagebox
|
|
from typing import Optional
|
|
|
|
|
|
class SimulationControls(ttk.LabelFrame):
|
|
"""Encapsulates the Live Simulation Engine controls.
|
|
|
|
This keeps the UI widgets (Start/Stop, Speed, Update Time, Reset, slider)
|
|
in a focused component. It uses the provided main_view reference for
|
|
callbacks and shared Tk variables to preserve existing behavior while
|
|
keeping the layout code out of `main_view.py`.
|
|
"""
|
|
|
|
def __init__(self, parent, main_view):
|
|
super().__init__(parent, text="Live Simulation Engine")
|
|
|
|
self.main_view = main_view
|
|
|
|
# Use main_view's variables so external code referencing them keeps
|
|
# working unchanged.
|
|
self.time_multiplier_var = getattr(main_view, "time_multiplier_var", tk.StringVar(value="1x"))
|
|
self.update_time = getattr(main_view, "update_time", tk.DoubleVar(value=1.0))
|
|
self.sim_slider_var = getattr(main_view, "sim_slider_var", tk.DoubleVar(value=0.0))
|
|
|
|
# Layout grid setup (mirror MainView original layout)
|
|
for i in range(10):
|
|
self.grid_columnconfigure(i, weight=0)
|
|
self.grid_columnconfigure(0, weight=0)
|
|
self.grid_columnconfigure(3, weight=1)
|
|
|
|
# Buttons
|
|
self.start_button = ttk.Button(self, text="Start Live", command=self._start)
|
|
self.start_button.grid(row=0, column=0, sticky="w", padx=5, pady=5)
|
|
|
|
self.stop_button = ttk.Button(self, text="Stop Live", command=self._stop, state=tk.DISABLED)
|
|
self.stop_button.grid(row=0, column=1, sticky="w", padx=5, pady=5)
|
|
# Guard to prevent re-entrant start attempts while a start is in progress
|
|
self._start_in_progress = False
|
|
|
|
# spacer
|
|
spacer = ttk.Frame(self)
|
|
spacer.grid(row=0, column=3, sticky="ew")
|
|
|
|
# The simulation runs at real-time (1x) for live mode; show a
|
|
# non-editable indicator instead of an interactive combobox so the
|
|
# UI cannot change the runtime multiplier.
|
|
ttk.Label(self, text="Speed:").grid(row=0, column=4, sticky="e", padx=(10, 2), pady=5)
|
|
# Use a simple (non-interactive) label to indicate fixed 1x speed.
|
|
# Keep the attribute name `multiplier_combo` for backward
|
|
# compatibility with code that expects this attribute, but expose a
|
|
# read-only display instead of a Combobox.
|
|
self.multiplier_combo = ttk.Label(self, text="1x")
|
|
self.multiplier_combo.grid(row=0, column=5, sticky="w", padx=(0, 5), pady=5)
|
|
|
|
ttk.Label(self, text="Update Time (s):").grid(row=0, column=6, sticky="e", padx=(10, 2), pady=5)
|
|
self.update_time_entry = ttk.Entry(self, textvariable=self.update_time, width=5)
|
|
self.update_time_entry.grid(row=0, column=7, sticky="w", padx=(0, 5), pady=5)
|
|
|
|
self.reset_button = ttk.Button(self, text="Reset State", command=getattr(main_view, "_on_reset_simulation", lambda: None))
|
|
self.reset_button.grid(row=0, column=8, sticky="e", padx=5, pady=5)
|
|
|
|
self.reset_radar_button = ttk.Button(self, text="Reset Radar", command=getattr(main_view, "_reset_radar_state", lambda: None))
|
|
self.reset_radar_button.grid(row=0, column=9, sticky="e", padx=5, pady=5)
|
|
|
|
# Progress slider row
|
|
progress_frame = ttk.Frame(self)
|
|
progress_frame.grid(row=1, column=0, columnspan=10, sticky="ew", padx=5, pady=(6, 2))
|
|
progress_frame.grid_columnconfigure(0, weight=1)
|
|
progress_frame.grid_columnconfigure(1, weight=0)
|
|
|
|
self.sim_slider = ttk.Scale(
|
|
progress_frame,
|
|
orient=tk.HORIZONTAL,
|
|
variable=self.sim_slider_var,
|
|
from_=0.0,
|
|
to=1.0,
|
|
command=lambda v: None,
|
|
)
|
|
self.sim_slider.grid(row=0, column=0, sticky="ew", padx=(4, 8))
|
|
|
|
# Bind press/release to support seeking (use main_view handler)
|
|
try:
|
|
self.sim_slider.bind("<ButtonPress-1>", lambda e: setattr(main_view, "_slider_is_dragging", True))
|
|
self.sim_slider.bind(
|
|
"<ButtonRelease-1>",
|
|
lambda e: (
|
|
setattr(main_view, "_slider_is_dragging", False),
|
|
getattr(main_view, "_on_seek", lambda: None)(),
|
|
),
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
labels_frame = ttk.Frame(progress_frame)
|
|
labels_frame.grid(row=0, column=1, sticky="e", padx=(4, 4))
|
|
|
|
self.sim_elapsed_label = ttk.Label(labels_frame, text="0.0s", width=8, anchor=tk.E)
|
|
self.sim_elapsed_label.grid(row=0, column=0)
|
|
|
|
slash_label = ttk.Label(labels_frame, text="/")
|
|
slash_label.grid(row=0, column=1, padx=(2, 2))
|
|
|
|
self.sim_total_label = ttk.Label(labels_frame, text="0.0s", width=8, anchor=tk.W)
|
|
self.sim_total_label.grid(row=0, column=2)
|
|
|
|
# Button handlers that delegate to main_view methods
|
|
def _start(self):
|
|
# 1) Verify connection before attempting to start
|
|
try:
|
|
connected = False
|
|
try:
|
|
if hasattr(self.main_view, "target_communicator") and self.main_view.target_communicator:
|
|
connected = bool(getattr(self.main_view.target_communicator, "is_open", False))
|
|
except Exception:
|
|
connected = False
|
|
|
|
if not connected:
|
|
# Inform the user and do NOT disable the Start button
|
|
try:
|
|
messagebox.showinfo(
|
|
"Not connected",
|
|
"Target communicator is not connected. Please connect before starting the simulation.",
|
|
parent=self.main_view,
|
|
)
|
|
except Exception:
|
|
# As a fallback, log to stdout (rare in GUI) and return
|
|
print("Target communicator is not connected. Please connect before starting the simulation.")
|
|
return
|
|
|
|
# 3) If connected, disable Start immediately to prevent duplicates
|
|
try:
|
|
self.start_button.config(state=tk.DISABLED)
|
|
# Force the UI to process this change immediately so the
|
|
# button appears disabled before any potentially blocking
|
|
# start/reset work begins in MainView.
|
|
try:
|
|
if hasattr(self.main_view, "update_idletasks"):
|
|
self.main_view.update_idletasks()
|
|
if hasattr(self.main_view, "update"):
|
|
# update() processes pending events and redraws UI
|
|
# immediately. Use cautiously but helpful here to
|
|
# avoid race where user can click again before the
|
|
# widget state is visually updated.
|
|
self.main_view.update()
|
|
except Exception:
|
|
pass
|
|
except Exception:
|
|
pass
|
|
|
|
# Delegate to MainView to start the simulation. If it fails,
|
|
# re-enable the Start button so the user can retry.
|
|
if hasattr(self.main_view, "_on_start_simulation"):
|
|
try:
|
|
self.main_view._on_start_simulation()
|
|
except Exception:
|
|
try:
|
|
self.start_button.config(state=tk.NORMAL)
|
|
except Exception:
|
|
pass
|
|
raise
|
|
except Exception:
|
|
raise
|
|
|
|
def _stop(self):
|
|
try:
|
|
if hasattr(self.main_view, "_on_stop_simulation"):
|
|
self.main_view._on_stop_simulation()
|
|
except Exception:
|
|
raise
|