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("", lambda e: setattr(main_view, "_slider_is_dragging", True)) self.sim_slider.bind( "", 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