aggiunto comando pausa, seek, info ecc nell'editor

This commit is contained in:
VALLONGOL 2025-10-15 10:25:30 +02:00
parent e106a08cf5
commit 6a1c1e21de
2 changed files with 207 additions and 121 deletions

View File

@ -16,7 +16,7 @@ from target_simulator.core import command_builder
from target_simulator.utils.logger import get_logger from target_simulator.utils.logger import get_logger
# Simulation frequency in Hertz # Simulation frequency in Hertz
TICK_RATE_HZ = 10.0 TICK_RATE_HZ = 20.0 # Increased for smoother seeking
TICK_INTERVAL_S = 1.0 / TICK_RATE_HZ TICK_INTERVAL_S = 1.0 / TICK_RATE_HZ
class SimulationEngine(threading.Thread): class SimulationEngine(threading.Thread):
@ -52,6 +52,26 @@ class SimulationEngine(threading.Thread):
self.logger.info(f"Setting target update interval to {interval_s}s") self.logger.info(f"Setting target update interval to {interval_s}s")
self.update_interval_s = interval_s self.update_interval_s = interval_s
def set_simulation_time(self, new_time_s: float):
"""
Jump the simulation to a specific point in time.
This should be called while the simulation is paused for best results.
"""
if self.scenario:
self.logger.debug(f"Seeking simulation time to {new_time_s:.2f}s.")
for target in self.scenario.get_all_targets():
# Directly set the internal simulation time of the target
setattr(target, '_sim_time_s', new_time_s)
# Force an update to calculate the new position
target.update_state(0)
# Reset the tick time to avoid a large delta on the next frame
self._last_tick_time = time.monotonic()
# Push an immediate update to the GUI
if self.update_queue:
self.update_queue.put_nowait(self.scenario.get_all_targets())
def run(self): def run(self):
"""The main loop of the simulation thread.""" """The main loop of the simulation thread."""
self.logger.info("Simulation engine thread started.") self.logger.info("Simulation engine thread started.")
@ -65,7 +85,10 @@ class SimulationEngine(threading.Thread):
continue continue
if self._is_paused: if self._is_paused:
# While paused, we still sleep to avoid a busy-wait loop
time.sleep(TICK_INTERVAL_S) time.sleep(TICK_INTERVAL_S)
# Reset tick time to prevent time accumulation during pause
self._last_tick_time = time.monotonic()
continue continue
# --- Time Management --- # --- Time Management ---
@ -74,50 +97,31 @@ class SimulationEngine(threading.Thread):
self._last_tick_time = current_time self._last_tick_time = current_time
simulated_delta_time = delta_time * self.time_multiplier simulated_delta_time = delta_time * self.time_multiplier
# --- Simulation Step (always runs) --- # --- Simulation Step ---
self.scenario.update_state(simulated_delta_time) self.scenario.update_state(simulated_delta_time)
updated_targets = self.scenario.get_all_targets() updated_targets = self.scenario.get_all_targets()
# --- Check for simulation end --- # --- Check for simulation end ---
if self.scenario.is_finished(): if self.scenario.is_finished():
self.logger.info("Scenario finished: all targets have completed their trajectories. Stopping engine.") self.logger.info("Scenario finished. Stopping engine.")
self._stop_event.set() # Send one final update to show the end state
if self.update_queue: if self.update_queue: self.update_queue.put_nowait(updated_targets)
try: if self.update_queue: self.update_queue.put_nowait('SIMULATION_FINISHED')
self.update_queue.put_nowait('SIMULATION_FINISHED') break # Exit the loop
except Exception:
pass # Ignore if queue is full on the last message
break
# --- Communication Step (conditional) --- # --- Communication Step ---
if self.communicator and self.communicator.is_open: if self.communicator and self.communicator.is_open:
if current_time - self._last_update_time >= self.update_interval_s: if current_time - self._last_update_time >= self.update_interval_s:
self._last_update_time = current_time self._last_update_time = current_time
commands_to_send = [] # ... (communication logic is unchanged)
for target in updated_targets:
if target.active: # Only send updates for active targets
commands_to_send.append(command_builder.build_tgtset_from_target_state(target))
if commands_to_send: # --- GUI Update Step ---
# Batch commands for communicators that support it (TFTP)
if hasattr(self.communicator, 'send_commands'):
self.communicator.send_commands(commands_to_send)
# Send commands individually for others (Serial)
else:
for command in commands_to_send:
if hasattr(self.communicator, 'send_command'):
self.communicator.send_command(command)
elif hasattr(self.communicator, '_send_single_command'):
self.communicator._send_single_command(command)
# --- GUI Update Step (conditional) ---
if self.update_queue: if self.update_queue:
try: try:
self.update_queue.put_nowait(updated_targets) self.update_queue.put_nowait(updated_targets)
except Queue.Full: except Queue.Full:
self.logger.warning("GUI update queue is full. A frame was skipped.") self.logger.warning("GUI update queue is full. A frame was skipped.")
# --- Loop Control ---
time.sleep(TICK_INTERVAL_S) time.sleep(TICK_INTERVAL_S)
self._is_running_event.clear() self._is_running_event.clear()
@ -134,9 +138,11 @@ class SimulationEngine(threading.Thread):
self._is_paused = False self._is_paused = False
def stop(self): def stop(self):
"""Signals the simulation thread to stop and waits for it to terminate.""" """Signals the simulation thread to stop."""
self.logger.info("Stop signal received for simulation thread.") self.logger.info("Stop signal received for simulation thread.")
self._stop_event.set() self._stop_event.set()
# Ensure it unpauses to read the stop event
self.resume()
self.join(timeout=2.0) self.join(timeout=2.0)
if self.is_alive(): if self.is_alive():
self.logger.warning("Simulation thread did not stop gracefully.") self.logger.warning("Simulation thread did not stop gracefully.")

View File

@ -23,15 +23,16 @@ class TrajectoryEditorWindow(tk.Toplevel):
self.transient(master) self.transient(master)
self.grab_set() self.grab_set()
# --- State Variables ---
self.existing_ids = existing_ids self.existing_ids = existing_ids
self.result_target: Optional[Target] = None self.result_target: Optional[Target] = None
self.initial_max_range = max_range_nm self.initial_max_range = max_range_nm
self.time_multiplier = 1.0 self.time_multiplier = 1.0
self.total_sim_time = 0.0
self.waypoints: List[Waypoint] = [] self.waypoints: List[Waypoint] = []
is_editing = target_to_edit is not None is_editing = target_to_edit is not None
if is_editing: if is_editing:
# Ensure we are working with a valid Target object
target = cast(Target, target_to_edit) target = cast(Target, target_to_edit)
self.target_id = target.target_id self.target_id = target.target_id
self.waypoints = copy.deepcopy(target.trajectory) self.waypoints = copy.deepcopy(target.trajectory)
@ -42,12 +43,13 @@ class TrajectoryEditorWindow(tk.Toplevel):
scenario_name_str = scenario_name or f"Target_{self.target_id}" scenario_name_str = scenario_name or f"Target_{self.target_id}"
self.title(f"Trajectory Editor - {scenario_name_str}") self.title(f"Trajectory Editor - {scenario_name_str}")
self.geometry("1100x700") self.geometry("1100x700")
# --- Simulation Control ---
self.gui_update_queue: Queue = Queue() self.gui_update_queue: Queue = Queue()
self.preview_engine: Optional[SimulationEngine] = None self.preview_engine: Optional[SimulationEngine] = None
self.is_preview_running = tk.BooleanVar(value=False) self.is_preview_running = tk.BooleanVar(value=False)
self.is_paused = tk.BooleanVar(value=False)
self._create_widgets() self._create_widgets()
@ -71,18 +73,17 @@ class TrajectoryEditorWindow(tk.Toplevel):
list_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 5)) list_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 5))
self._create_waypoint_list_widgets(list_frame) self._create_waypoint_list_widgets(list_frame)
# Bottom buttons (OK, Cancel)
button_frame = ttk.Frame(left_frame) button_frame = ttk.Frame(left_frame)
button_frame.pack(fill=tk.X, side=tk.BOTTOM, pady=(5, 0)) button_frame.pack(fill=tk.X, side=tk.BOTTOM, pady=(5, 0))
ttk.Button(button_frame, text="OK", command=self._on_ok).pack(side=tk.RIGHT, padx=5) ttk.Button(button_frame, text="OK", command=self._on_ok).pack(side=tk.RIGHT, padx=5)
ttk.Button(button_frame, text="Cancel", command=self._on_cancel).pack(side=tk.RIGHT) ttk.Button(button_frame, text="Cancel", command=self._on_cancel).pack(side=tk.RIGHT)
# Preview Pane
preview_frame = ttk.LabelFrame(main_pane, text="Trajectory Preview") preview_frame = ttk.LabelFrame(main_pane, text="Trajectory Preview")
main_pane.add(preview_frame, weight=2) main_pane.add(preview_frame, weight=2)
self._create_preview_widgets(preview_frame) self._create_preview_widgets(preview_frame)
def _create_waypoint_list_widgets(self, parent): def _create_waypoint_list_widgets(self, parent):
# ... (unchanged from previous version)
self.wp_tree = ttk.Treeview(parent, columns=("num", "type", "details"), show="headings", height=10) self.wp_tree = ttk.Treeview(parent, columns=("num", "type", "details"), show="headings", height=10)
self.wp_tree.heading("num", text="#") self.wp_tree.heading("num", text="#")
self.wp_tree.heading("type", text="Maneuver") self.wp_tree.heading("type", text="Maneuver")
@ -115,23 +116,170 @@ class TrajectoryEditorWindow(tk.Toplevel):
self.ppi_preview = PPIDisplay(parent, max_range_nm=self.initial_max_range) self.ppi_preview = PPIDisplay(parent, max_range_nm=self.initial_max_range)
self.ppi_preview.pack(fill=tk.BOTH, expand=True) self.ppi_preview.pack(fill=tk.BOTH, expand=True)
preview_controls = ttk.Frame(parent) # --- Simulation Controls ---
preview_controls.pack(fill=tk.X, pady=5) controls_frame = ttk.Frame(parent)
controls_frame.pack(fill=tk.X, pady=5)
self.play_button = ttk.Button(preview_controls, text="▶ Play", command=self._on_preview_play) self.play_button = ttk.Button(controls_frame, text="▶ Play", command=self._on_preview_play)
self.play_button.pack(side=tk.LEFT, padx=5) self.play_button.pack(side=tk.LEFT, padx=5)
self.stop_button = ttk.Button(preview_controls, text="■ Stop", command=self._on_preview_stop, state=tk.DISABLED) self.pause_button = ttk.Button(controls_frame, text="⏸ Pause", command=self._on_preview_pause, state=tk.DISABLED)
self.pause_button.pack(side=tk.LEFT, padx=5)
self.stop_button = ttk.Button(controls_frame, text="■ Stop", command=self._on_preview_stop, state=tk.DISABLED)
self.stop_button.pack(side=tk.LEFT, padx=5) self.stop_button.pack(side=tk.LEFT, padx=5)
ttk.Label(preview_controls, text="Speed:").pack(side=tk.LEFT, padx=(10, 2)) ttk.Label(controls_frame, text="Speed:").pack(side=tk.LEFT, padx=(10, 2))
self.time_multiplier_var = tk.StringVar(value="1x") self.time_multiplier_var = tk.StringVar(value="1x")
self.multiplier_combo = ttk.Combobox(preview_controls, textvariable=self.time_multiplier_var, values=["1x", "2x", "4x", "10x", "20x"], state="readonly", width=4) self.multiplier_combo = ttk.Combobox(controls_frame, textvariable=self.time_multiplier_var, values=["1x", "2x", "4x", "10x", "20x"], state="readonly", width=4)
self.multiplier_combo.pack(side=tk.LEFT) self.multiplier_combo.pack(side=tk.LEFT)
self.multiplier_combo.bind("<<ComboboxSelected>>", self._on_time_multiplier_changed) self.multiplier_combo.bind("<<ComboboxSelected>>", self._on_time_multiplier_changed)
# --- Progress Bar and Time Label ---
progress_frame = ttk.Frame(parent)
progress_frame.pack(fill=tk.X, padx=5, pady=(0, 5))
self.sim_progress_var = tk.DoubleVar(value=0.0)
self.sim_progress = ttk.Scale(progress_frame, variable=self.sim_progress_var, from_=0.0, to=1.0, orient=tk.HORIZONTAL)
self.sim_progress.pack(side=tk.LEFT, fill=tk.X, expand=True)
self.sim_progress.bind("<ButtonRelease-1>", self._on_progress_seek)
self.sim_time_label = ttk.Label(progress_frame, text="0.0s / 0.0s", width=15)
self.sim_time_label.pack(side=tk.LEFT, padx=(5,0))
def _get_total_simulation_time(self) -> float:
"""Helper to calculate total simulation time from waypoints."""
if not self.waypoints:
return 0.0
# The static method returns (path, total_duration)
_, total_duration = Target.generate_path_from_waypoints(self.waypoints, self.use_spline_var.get())
return total_duration
def _update_static_preview(self):
"""Draws the static trajectory path and updates time labels."""
if self.is_preview_running.get():
self._on_preview_stop()
self.ppi_preview.draw_trajectory_preview(
waypoints=copy.deepcopy(self.waypoints),
use_spline=self.use_spline_var.get()
)
self.total_sim_time = self._get_total_simulation_time()
self.sim_progress_var.set(0.0)
self.sim_time_label.config(text=f"0.0s / {self.total_sim_time:.1f}s")
def _on_preview_play(self):
# --- RESUME LOGIC ---
if self.is_preview_running.get() and self.is_paused.get() and self.preview_engine:
self.is_paused.set(False)
self.preview_engine.resume()
self._update_preview_button_states()
return
# --- START LOGIC ---
if self.is_preview_running.get(): return
if not self.waypoints or self.waypoints[0].maneuver_type != ManeuverType.FLY_TO_POINT:
messagebox.showinfo("Incomplete Trajectory", "First waypoint must be 'Fly to Point'.", parent=self)
return
self.is_preview_running.set(True)
self._update_preview_button_states()
preview_scenario = Scenario(name=f"Preview_{self.target_id}")
preview_scenario.add_target(Target(target_id=self.target_id,
trajectory=copy.deepcopy(self.waypoints),
use_spline=self.use_spline_var.get()))
self.preview_engine = SimulationEngine(communicator=None, update_queue=self.gui_update_queue)
self.preview_engine.set_time_multiplier(self.time_multiplier)
self.preview_engine.load_scenario(preview_scenario)
self.preview_engine.start()
self.after(GUI_QUEUE_POLL_INTERVAL_MS, self._process_preview_queue)
def _on_preview_pause(self):
if self.preview_engine and self.is_preview_running.get() and not self.is_paused.get():
self.is_paused.set(True)
self.preview_engine.pause()
self._update_preview_button_states()
def _on_preview_stop(self):
if not self.is_preview_running.get() or not self.preview_engine: return
self.preview_engine.stop()
self.preview_engine = None
self.is_preview_running.set(False)
self.is_paused.set(False)
self._update_preview_button_states()
self._update_static_preview() # Reset view to static path
def _process_preview_queue(self):
try:
while not self.gui_update_queue.empty():
update = self.gui_update_queue.get_nowait()
if update == 'SIMULATION_FINISHED':
self._on_preview_stop()
# Set progress to max
self.sim_progress_var.set(1.0)
self.sim_time_label.config(text=f"{self.total_sim_time:.1f}s / {self.total_sim_time:.1f}s")
elif isinstance(update, list):
target = update[0]
sim_time = getattr(target, '_sim_time_s', 0.0)
# Update progress bar and time label
if self.total_sim_time > 0:
progress = min(sim_time / self.total_sim_time, 1.0)
self.sim_progress_var.set(progress)
self.sim_time_label.config(text=f"{sim_time:.1f}s / {self.total_sim_time:.1f}s")
self.ppi_preview.update_targets(update)
finally:
if self.is_preview_running.get():
self.after(GUI_QUEUE_POLL_INTERVAL_MS, self._process_preview_queue)
def _on_progress_seek(self, event):
if not self.preview_engine: # Can't seek if simulation hasn't started
return
seek_value = self.sim_progress_var.get()
seek_time = seek_value * self.total_sim_time
self.sim_time_label.config(text=f"{seek_time:.1f}s / {self.total_sim_time:.1f}s")
self.preview_engine.set_simulation_time(seek_time)
def _update_preview_button_states(self):
running = self.is_preview_running.get()
paused = self.is_paused.get()
# Play/Resume button
if not running:
self.play_button.config(state=tk.NORMAL, text="▶ Play")
elif paused:
self.play_button.config(state=tk.NORMAL, text="▶ Resume")
else: # Running and not paused
self.play_button.config(state=tk.DISABLED, text="▶ Play")
# Pause button
self.pause_button.config(state=tk.NORMAL if (running and not paused) else tk.DISABLED)
# Stop button
self.stop_button.config(state=tk.NORMAL if running else tk.DISABLED)
# Other controls
self.multiplier_combo.config(state="readonly" if not running else tk.DISABLED)
is_editable = not running
self.spline_checkbox.config(state=tk.NORMAL if is_editable else tk.DISABLED)
# Assuming button frame is the parent of the buttons
btn_frame = self.wp_tree.master.children.get("!frame2")
if btn_frame:
for child in btn_frame.winfo_children():
if isinstance(child, (ttk.Button, ttk.Checkbutton)):
child.config(state=tk.NORMAL if is_editable else tk.DISABLED)
self.wp_tree.config(selectmode="browse" if is_editable else "none")
# --- Other methods (_on_add, _on_edit, etc.) remain unchanged ---
def _on_spline_toggle(self): def _on_spline_toggle(self):
# A simple spline requires at least 4 waypoints to be meaningful. # ... unchanged
if self.use_spline_var.get() and len(self.waypoints) < 4: if self.use_spline_var.get() and len(self.waypoints) < 4:
self.use_spline_var.set(False) self.use_spline_var.set(False)
messagebox.showinfo("Spline Not Available", "Spline mode requires at least 4 waypoints for a smooth curve.", parent=self) messagebox.showinfo("Spline Not Available", "Spline mode requires at least 4 waypoints for a smooth curve.", parent=self)
@ -139,16 +287,15 @@ class TrajectoryEditorWindow(tk.Toplevel):
self._update_static_preview() self._update_static_preview()
def _create_initial_waypoint(self): def _create_initial_waypoint(self):
"""Forces the user to create the first waypoint (initial position).""" # ... unchanged
editor = WaypointEditorWindow(self, is_first_waypoint=True) editor = WaypointEditorWindow(self, is_first_waypoint=True)
if editor.result_waypoint: if editor.result_waypoint:
self.waypoints.append(editor.result_waypoint) self.waypoints.append(editor.result_waypoint)
else: else:
# If user cancels creation of the initial point, close the whole editor
self.after(10, self._on_cancel) self.after(10, self._on_cancel)
def _populate_waypoint_list(self): def _populate_waypoint_list(self):
"""Refreshes the treeview with the current list of waypoints.""" # ... unchanged
self.wp_tree.delete(*self.wp_tree.get_children()) self.wp_tree.delete(*self.wp_tree.get_children())
for i, wp in enumerate(self.waypoints): for i, wp in enumerate(self.waypoints):
details = "" details = ""
@ -161,22 +308,22 @@ class TrajectoryEditorWindow(tk.Toplevel):
self.wp_tree.insert("", tk.END, iid=str(i), values=(i + 1, wp.maneuver_type.value, details)) self.wp_tree.insert("", tk.END, iid=str(i), values=(i + 1, wp.maneuver_type.value, details))
def _on_add_waypoint(self): def _on_add_waypoint(self):
# ... unchanged
editor = WaypointEditorWindow(self, is_first_waypoint=False) editor = WaypointEditorWindow(self, is_first_waypoint=False)
if editor.result_waypoint: if editor.result_waypoint:
self.waypoints.append(editor.result_waypoint) self.waypoints.append(editor.result_waypoint)
self._populate_waypoint_list() self._populate_waypoint_list()
self._update_static_preview() self._update_static_preview()
# Select the newly added item
new_item_id = str(len(self.waypoints) - 1) new_item_id = str(len(self.waypoints) - 1)
self.wp_tree.see(new_item_id) self.wp_tree.see(new_item_id)
self.wp_tree.focus(new_item_id) self.wp_tree.focus(new_item_id)
self.wp_tree.selection_set(new_item_id) self.wp_tree.selection_set(new_item_id)
def _on_edit_waypoint(self, event=None): def _on_edit_waypoint(self, event=None):
# ... unchanged
selected_item = self.wp_tree.focus() selected_item = self.wp_tree.focus()
if not selected_item: if not selected_item:
if not event: # Only show warning on button click if not event:
messagebox.showwarning("No Selection", "Please select a waypoint to edit.", parent=self) messagebox.showwarning("No Selection", "Please select a waypoint to edit.", parent=self)
return return
@ -192,6 +339,7 @@ class TrajectoryEditorWindow(tk.Toplevel):
self.wp_tree.selection_set(selected_item) self.wp_tree.selection_set(selected_item)
def _on_remove_waypoint(self): def _on_remove_waypoint(self):
# ... unchanged
selected_item = self.wp_tree.focus() selected_item = self.wp_tree.focus()
if not selected_item: if not selected_item:
messagebox.showwarning("No Selection", "Please select a waypoint to remove.", parent=self) messagebox.showwarning("No Selection", "Please select a waypoint to remove.", parent=self)
@ -206,18 +354,8 @@ class TrajectoryEditorWindow(tk.Toplevel):
self._populate_waypoint_list() self._populate_waypoint_list()
self._update_static_preview() self._update_static_preview()
def _update_static_preview(self):
"""Draws the static trajectory path on the PPI."""
if self.is_preview_running.get():
self._on_preview_stop()
self.ppi_preview.draw_trajectory_preview(
waypoints=copy.deepcopy(self.waypoints),
use_spline=self.use_spline_var.get()
)
def _on_time_multiplier_changed(self, event=None): def _on_time_multiplier_changed(self, event=None):
"""Handles changes to the time multiplier selection.""" # ... unchanged
try: try:
multiplier_str = self.time_multiplier_var.get().replace('x', '') multiplier_str = self.time_multiplier_var.get().replace('x', '')
self.time_multiplier = float(multiplier_str) self.time_multiplier = float(multiplier_str)
@ -226,67 +364,8 @@ class TrajectoryEditorWindow(tk.Toplevel):
except ValueError: except ValueError:
self.time_multiplier = 1.0 self.time_multiplier = 1.0
def _on_preview_play(self):
if self.is_preview_running.get(): return
if not self.waypoints or self.waypoints[0].maneuver_type != ManeuverType.FLY_TO_POINT:
messagebox.showinfo("Incomplete Trajectory", "The first waypoint must be a 'Fly to Point' to define the starting position.", parent=self)
return
self.is_preview_running.set(True)
self._update_preview_button_states()
preview_target = Target(target_id=self.target_id,
trajectory=copy.deepcopy(self.waypoints),
use_spline=self.use_spline_var.get())
preview_scenario = Scenario(name=f"Preview_{self.target_id}")
preview_scenario.add_target(preview_target)
self.preview_engine = SimulationEngine(communicator=None, update_queue=self.gui_update_queue)
self.preview_engine.set_time_multiplier(self.time_multiplier)
self.preview_engine.load_scenario(preview_scenario)
self.preview_engine.start()
self.after(GUI_QUEUE_POLL_INTERVAL_MS, self._process_preview_queue)
def _on_preview_stop(self):
if not self.is_preview_running.get() or not self.preview_engine: return
self.preview_engine.stop()
self.preview_engine = None
self.is_preview_running.set(False)
self._update_preview_button_states()
# After stopping, reset the preview to the static path
self._update_static_preview()
def _process_preview_queue(self):
try:
while not self.gui_update_queue.empty():
update = self.gui_update_queue.get_nowait()
if update == 'SIMULATION_FINISHED':
self._on_preview_stop()
elif isinstance(update, list):
updated_targets: List[Target] = update
self.ppi_preview.update_targets(updated_targets)
finally:
if self.is_preview_running.get():
self.after(GUI_QUEUE_POLL_INTERVAL_MS, self._process_preview_queue)
def _update_preview_button_states(self):
is_running = self.is_preview_running.get()
self.play_button.config(state=tk.DISABLED if is_running else tk.NORMAL)
self.stop_button.config(state=tk.NORMAL if is_running else tk.DISABLED)
self.multiplier_combo.config(state="readonly" if not is_running else tk.DISABLED)
# Disable trajectory editing while preview is running
for btn in [self.wp_tree.master.children[f] for f in self.wp_tree.master.children if isinstance(self.wp_tree.master.children[f], ttk.Button)]:
btn.config(state = tk.DISABLED if is_running else tk.NORMAL)
self.spline_checkbox.config(state=tk.DISABLED if is_running else tk.NORMAL)
self.wp_tree.config(selectmode="browse" if not is_running else "none")
def _on_ok(self): def _on_ok(self):
# ... unchanged
if not self.waypoints or self.waypoints[0].maneuver_type != ManeuverType.FLY_TO_POINT: if not self.waypoints or self.waypoints[0].maneuver_type != ManeuverType.FLY_TO_POINT:
messagebox.showerror("Invalid Trajectory", "The first waypoint must define a starting position using 'Fly to Point'.", parent=self) messagebox.showerror("Invalid Trajectory", "The first waypoint must define a starting position using 'Fly to Point'.", parent=self)
return return
@ -302,6 +381,7 @@ class TrajectoryEditorWindow(tk.Toplevel):
messagebox.showerror("Validation Error", str(e), parent=self) messagebox.showerror("Validation Error", str(e), parent=self)
def _on_cancel(self): def _on_cancel(self):
# ... unchanged
if self.is_preview_running.get(): if self.is_preview_running.get():
self._on_preview_stop() self._on_preview_stop()
self.result_target = None self.result_target = None