# target_simulator/gui/main_view.py """ Main view of the application, containing the primary window and widgets. """ import tkinter as tk from tkinter import ttk, scrolledtext, messagebox from queue import Queue, Empty from typing import Optional, Dict, Any, List import time import math import os import json import sys from datetime import datetime # Use absolute imports for robustness and clarity from target_simulator.gui.ppi_display import PPIDisplay from target_simulator.gui.ppi_adapter import build_display_data from target_simulator.gui.connection_settings_window import ConnectionSettingsWindow from target_simulator.gui.radar_config_window import RadarConfigWindow from target_simulator.gui.scenario_controls_frame import ScenarioControlsFrame from target_simulator.gui.target_list_frame import TargetListFrame from target_simulator.gui.connection_panel import ConnectionPanel from target_simulator.gui.status_bar import StatusBar from target_simulator.gui.simulation_controls import SimulationControls from target_simulator.core.communicator_interface import CommunicatorInterface from target_simulator.core.serial_communicator import SerialCommunicator from target_simulator.core.tftp_communicator import TFTPCommunicator from target_simulator.core.simulation_engine import SimulationEngine from target_simulator.core.models import Scenario, Target from target_simulator.utils.logger import get_logger, shutdown_logging_system from target_simulator.utils.config_manager import ConfigManager from target_simulator.gui.sfp_debug_window import SfpDebugWindow from target_simulator.gui.logger_panel import LoggerPanel from target_simulator.core.sfp_communicator import SFPCommunicator from target_simulator.analysis.simulation_state_hub import SimulationStateHub from target_simulator.analysis.performance_analyzer import PerformanceAnalyzer from target_simulator.gui.analysis_window import AnalysisWindow from target_simulator.core import command_builder from target_simulator.analysis.simulation_archive import SimulationArchive from target_simulator.communication.communicator_manager import CommunicatorManager from target_simulator.simulation.simulation_controller import SimulationController # --- Import Version Info FOR THE WRAPPER ITSELF --- try: from target_simulator import _version as wrapper_version WRAPPER_APP_VERSION_STRING = f"{wrapper_version.__version__} ({wrapper_version.GIT_BRANCH}/{wrapper_version.GIT_COMMIT_HASH[:7]})" except ImportError: WRAPPER_APP_VERSION_STRING = "(Dev Wrapper)" GUI_REFRESH_RATE_MS = 40 class MainView(tk.Tk): """The main application window.""" def __init__(self): super().__init__() self.logger = get_logger(__name__) self.config_manager = ConfigManager() self.current_archive: Optional[SimulationArchive] = None # --- Load Settings --- settings = self.config_manager.get_general_settings() self.scan_limit = settings.get("scan_limit", 60) self.max_range = settings.get("max_range", 100) self.connection_config = self.config_manager.get_connection_settings() # --- Initialize the data hub and controllers --- self.simulation_hub = SimulationStateHub() self.performance_analyzer = PerformanceAnalyzer(self.simulation_hub) self.communicator_manager = CommunicatorManager( simulation_hub=self.simulation_hub, logger=self.logger, defer_sfp_connection=True ) self.communicator_manager.set_config(self.connection_config) self.simulation_controller = SimulationController( communicator_manager=self.communicator_manager, simulation_hub=self.simulation_hub, config_manager=self.config_manager, logger=self.logger, ) # --- Core Logic Handlers --- self.target_communicator: Optional[CommunicatorInterface] = None self.lru_communicator: Optional[CommunicatorInterface] = None self.scenario = Scenario() self.current_scenario_name: Optional[str] = None self.sfp_debug_window: Optional[SfpDebugWindow] = None self.analysis_window: Optional[AnalysisWindow] = None # --- Simulation Engine --- self.simulation_engine: Optional[SimulationEngine] = None self.is_simulation_running = tk.BooleanVar(value=False) self._start_in_progress_main = False # Guard for start button self.time_multiplier = 1.0 self.update_time = tk.DoubleVar(value=1.0) # Simulation progress tracking self.total_sim_time = 0.0 self.sim_elapsed_time = 0.0 self.sim_slider_var = tk.DoubleVar(value=0.0) self._slider_is_dragging = False # --- Window and UI Setup --- self.title(f"Radar Target Simulator") self.geometry(settings.get("geometry", "1200x900")) self.minsize(1024, 768) self._create_menubar() self._create_main_layout() self._create_statusbar() self._status_after_id = None # --- Post-UI Initialization --- self._initialize_communicators() self._load_scenarios_into_ui() last_scenario = settings.get("last_selected_scenario") if last_scenario and last_scenario in self.config_manager.get_scenario_names(): self._on_load_scenario(last_scenario) self._update_window_title() self.protocol("WM_DELETE_WINDOW", self._on_closing) self.logger.info("MainView initialized successfully.") self.after(GUI_REFRESH_RATE_MS, self._gui_refresh_loop) self.after(1000, self._update_rate_status) self.after(1000, self._update_latency_status) def _create_main_layout(self): v_pane = ttk.PanedWindow(self, orient=tk.VERTICAL) v_pane.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) self.h_pane = ttk.PanedWindow(v_pane, orient=tk.HORIZONTAL) v_pane.add(self.h_pane, weight=4) # --- Right Pane (connection panel + PPI) --- right_container = ttk.Frame(self.h_pane) self.h_pane.add(right_container, weight=2) self.connection_panel = ConnectionPanel(right_container, initial_config=self.connection_config) self.connection_panel.pack(side=tk.TOP, fill=tk.X, padx=5, pady=(5, 2)) self.connection_panel.set_connect_handler(self._on_connect_button) self.connection_panel.set_open_settings_handler(self._open_settings) self.ppi_widget = PPIDisplay( right_container, max_range_nm=self.max_range, scan_limit_deg=self.scan_limit ) self.ppi_widget.pack( side=tk.TOP, fill=tk.BOTH, expand=True, padx=2, pady=(2, 5) ) # --- Left Pane --- left_pane_container = ttk.Frame(self.h_pane) self.h_pane.add(left_pane_container, weight=1) left_notebook = ttk.Notebook(left_pane_container) left_notebook.pack(fill=tk.BOTH, expand=True) # --- TAB 1: SCENARIO CONFIG --- scenario_tab = ttk.Frame(left_notebook) left_notebook.add(scenario_tab, text="Scenario Config") self.scenario_controls = ScenarioControlsFrame( scenario_tab, main_view=self, load_scenario_command=self._on_load_scenario, save_as_command=self._on_save_scenario_as, delete_command=self._on_delete_scenario, new_scenario_command=self._on_new_scenario, ) self.scenario_controls.pack(fill=tk.X, expand=False, padx=5, pady=(5, 5)) self.target_list = TargetListFrame( scenario_tab, targets_changed_callback=self._on_targets_changed ) self.target_list.pack(fill=tk.BOTH, expand=True, padx=5) # --- TAB 2: SIMULATION --- simulation_tab = ttk.Frame(left_notebook) left_notebook.add(simulation_tab, text="Simulation") self.simulation_controls = SimulationControls(simulation_tab, self) self.simulation_controls.pack(fill=tk.X, padx=5, pady=10, anchor="n") # Make controls directly accessible for backward compatibility self.start_button = self.simulation_controls.start_button self.stop_button = self.simulation_controls.stop_button self.reset_radar_button = self.simulation_controls.reset_radar_button # --- TAB 3: LRU SIMULATION (unchanged) --- lru_tab = ttk.Frame(left_notebook) left_notebook.add(lru_tab, text="LRU Simulation") # ... widgets for LRU ... # --- TAB 4: Analysis --- analysis_tab = ttk.Frame(left_notebook) left_notebook.add(analysis_tab, text="Analysis") self._create_analysis_tab_widgets(analysis_tab) # --- Bottom Pane (Logs) --- log_frame_container = ttk.LabelFrame(v_pane, text="Logs") v_pane.add(log_frame_container, weight=1) self.log_text_widget = scrolledtext.ScrolledText( log_frame_container, state=tk.DISABLED, wrap=tk.WORD, font=("Consolas", 9) ) self.log_text_widget.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) def _create_analysis_tab_widgets(self, parent): # Place the tree inside a container so we can attach scrollbars neatly tree_container = ttk.Frame(parent) tree_container.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) # Vertical scrollbar vscroll = ttk.Scrollbar(tree_container, orient=tk.VERTICAL) self.analysis_tree = ttk.Treeview( tree_container, columns=("datetime", "scenario", "duration"), show="headings", yscrollcommand=vscroll.set, ) # Configure vertical scrollbar to control the tree vscroll.config(command=self.analysis_tree.yview) self.analysis_tree.heading("datetime", text="Date/Time") self.analysis_tree.heading("scenario", text="Scenario Name") self.analysis_tree.heading("duration", text="Duration (s)") self.analysis_tree.column("datetime", width=150) self.analysis_tree.column("scenario", width=200) self.analysis_tree.column("duration", width=80, anchor=tk.E) # Use grid inside the container to place tree + scrollbars tree_container.grid_rowconfigure(0, weight=1) tree_container.grid_columnconfigure(0, weight=1) self.analysis_tree.grid(row=0, column=0, sticky="nsew") vscroll.grid(row=0, column=1, sticky="ns") btn_frame = ttk.Frame(parent) btn_frame.pack(fill=tk.X, padx=5, pady=5) ttk.Button( btn_frame, text="Refresh List", command=self._refresh_analysis_list ).pack(side=tk.LEFT) ttk.Button( btn_frame, text="Open Archive Folder", command=self._open_archive_folder ).pack(side=tk.LEFT, padx=(6, 0)) ttk.Button( btn_frame, text="Analyze Selected", command=self._on_analyze_run ).pack(side=tk.RIGHT) self.after(100, self._refresh_analysis_list) def _create_menubar(self): menubar = tk.Menu(self) self.config(menu=menubar) settings_menu = tk.Menu(menubar, tearoff=0) menubar.add_cascade(label="Settings", menu=settings_menu) settings_menu.add_command(label="Connection...", command=self._open_settings) settings_menu.add_command( label="Radar Config...", command=self._open_radar_config ) debug_menu = tk.Menu(menubar, tearoff=0) menubar.add_cascade(label="Debug", menu=debug_menu) debug_menu.add_command( label="SFP Packet Inspector...", command=self._open_sfp_debug_window ) debug_menu.add_command( label="Logger Levels...", command=self._open_logger_panel ) def _create_statusbar(self): self.status_bar = StatusBar(self) self.status_bar.place(relx=0.0, rely=1.0, anchor="sw", relwidth=1.0, height=24) self.status_var = self.status_bar.status_var self.rate_status_var = self.status_bar.rate_status_var self.latency_status_var = self.status_bar.latency_status_var self._status_after_id = None def show_status_message(self, text: str, timeout_ms: Optional[int] = 3000): if hasattr(self, 'status_bar') and self.status_bar: self.status_bar.show_status_message(text, timeout_ms=timeout_ms) def clear_status_message(self): if hasattr(self, 'status_bar') and self.status_bar: self.status_bar.show_status_message("Ready", timeout_ms=0) def _update_window_title(self): base_title = (f"Radar Target Simulator- {WRAPPER_APP_VERSION_STRING}") if self.current_scenario_name: self.title(f"{base_title} - {self.current_scenario_name}") else: self.title(base_title) def _on_connection_state_change(self, is_connected: bool): self.logger.info(f"MainView received connection state change: Connected={is_connected}") if hasattr(self, 'status_bar'): self.status_bar.set_target_connected(is_connected) if hasattr(self, 'connection_panel'): self.connection_panel.update_toggle_state(is_connected) if self.sfp_debug_window and self.sfp_debug_window.winfo_exists(): if hasattr(self.sfp_debug_window, "update_toggle_state"): self.sfp_debug_window.update_toggle_state(is_connected) def _initialize_communicators(self): t_comm, t_connected, l_comm, l_connected = self.communicator_manager.initialize_communicators() self.target_communicator = t_comm self.lru_communicator = l_comm if hasattr(self, 'status_bar'): self.status_bar.set_target_connected(t_connected) self.status_bar.set_lru_connected(l_connected) self.communicator_manager.add_connection_state_callback(self._on_connection_state_change) def update_connection_settings(self, new_config: Dict[str, Any]): self.logger.info(f"Updating connection settings: {new_config}") self.connection_config = new_config self.config_manager.save_connection_settings(new_config) if hasattr(self, 'connection_panel'): self.connection_panel.update_summary(self.connection_config) self.communicator_manager.set_config(new_config) self._initialize_communicators() def _open_settings(self): self.logger.info("Opening connection settings window.") ConnectionSettingsWindow(self, self.config_manager, self.connection_config) def _on_connect_button(self): self.logger.info("Connection toggle requested by user.") if self.target_communicator and self.target_communicator.is_open: self.communicator_manager.disconnect_target() else: if not self.communicator_manager.connect_target(): messagebox.showerror( "Connection Failed", "Could not connect. Check settings and logs.", ) def _reset_radar_state(self): self.simulation_controller.reset_radar_state(self) def _on_start_simulation(self): if self._start_in_progress_main: self.logger.info("Start already in progress; ignoring duplicate request.") return self._start_in_progress_main = True self.simulation_controller.start_simulation(self) def _on_stop_simulation(self): self.simulation_controller.stop_simulation(self) def _on_simulation_finished(self): self.simulation_controller.on_simulation_finished(self) def _on_reset_simulation(self): self.logger.info("Resetting scenario to initial state.") self.simulation_controls.hide_notice() if self.is_simulation_running.get(): self._on_stop_simulation() self.simulation_hub.reset() self.ppi_widget.clear_trails() self.scenario.reset_simulation() self._update_all_views() self.show_status_message("Scenario reset to initial state.", timeout_ms=3000) def _update_button_states(self): is_running = self.is_simulation_running.get() start_in_progress = self._start_in_progress_main start_state = tk.DISABLED if (is_running or start_in_progress) else tk.NORMAL stop_state = tk.NORMAL if is_running else tk.DISABLED controls_state = "readonly" if not (is_running or start_in_progress) else tk.DISABLED button_controls_state = tk.NORMAL if not (is_running or start_in_progress) else tk.DISABLED self.start_button.config(state=start_state) self.stop_button.config(state=stop_state) self.reset_radar_button.config(state=button_controls_state) self.scenario_controls.new_button.config(state=button_controls_state) self.scenario_controls.save_button.config(state=button_controls_state) self.scenario_controls.save_as_button.config(state=button_controls_state) self.scenario_controls.delete_button.config(state=button_controls_state) self.scenario_controls.scenario_combobox.config(state=controls_state) self.target_list.add_button.config(state=button_controls_state) self.target_list.remove_button.config(state=button_controls_state) self.target_list.edit_button.config(state=button_controls_state) self.target_list.tree.config(selectmode="browse" if not (is_running or start_in_progress) else "none") def _update_simulation_progress_display(self): try: elapsed = self.sim_elapsed_time total = self.total_sim_time self.simulation_controls.sim_elapsed_label.config(text=f"{elapsed:.1f}s") self.simulation_controls.sim_total_label.config(text=f"{total:.1f}s") except Exception: pass def _update_rate_status(self): try: real_rate = self.simulation_hub.get_real_rate(1.0) if self.simulation_hub else 0.0 packet_rate = self.simulation_hub.get_packet_rate(1.0) if self.simulation_hub else 0.0 ppi_rate = self.ppi_widget.get_real_update_rate(1.0) if hasattr(self, 'ppi_widget') else 0.0 if self.rate_status_var: if packet_rate > 0.0: self.rate_status_var.set( f"pkt in: {packet_rate:.1f} pkt/s | ev in: {real_rate:.1f} ev/s | ppi upd: {ppi_rate:.1f} upd/s" ) else: self.rate_status_var.set( f"real in: {real_rate:.1f} ev/s | ppi upd: {ppi_rate:.1f} upd/s" ) except Exception as e: self.logger.debug(f"Error updating rate status: {e}") finally: self.after(1000, self._update_rate_status) def _update_latency_status(self): """Periodically updates the latency display and prediction horizon.""" try: latency_s = 0.0 if self.target_communicator and hasattr(self.target_communicator, 'router'): router = self.target_communicator.router() if router: latency_s = router.get_estimated_latency_s() # Update the status bar display if self.latency_status_var: if latency_s > 0: self.latency_status_var.set(f"Latency: {latency_s * 1000:.1f} ms") else: self.latency_status_var.set("") # Pulisce se non c'รจ latenza # Update the simulation engine's prediction horizon if it's running if self.simulation_engine and self.simulation_engine.is_running(): self.simulation_engine.set_prediction_horizon(latency_s) except Exception as e: self.logger.debug(f"Error updating latency status: {e}") finally: # Schedule the next update self.after(1000, self._update_latency_status) def _on_seek(self): if not self.simulation_engine or not self.simulation_engine.scenario: return frac = float(self.simulation_controls.sim_slider_var.get()) new_time = max(0.0, min(self.total_sim_time, frac * self.total_sim_time)) self.simulation_engine.set_simulation_time(new_time) self.sim_elapsed_time = new_time self._update_simulation_progress_display() def _on_targets_changed(self, targets: List[Target]): self.scenario.targets = {t.target_id: t for t in targets} self.ppi_widget.update_simulated_targets(targets) if self.current_scenario_name: self.config_manager.save_scenario(self.current_scenario_name, self.scenario.to_dict()) def _update_all_views(self, targets_to_display: Optional[List[Target]] = None): self._update_window_title() if targets_to_display is None: targets_to_display = self.scenario.get_all_targets() self.target_list.update_target_list(targets_to_display) self.ppi_widget.update_simulated_targets(targets_to_display) def _load_scenarios_into_ui(self): scenario_names = self.config_manager.get_scenario_names() self.scenario_controls.update_scenario_list(scenario_names, self.current_scenario_name) if hasattr(self, 'simulation_controls') and hasattr(self.simulation_controls, 'sim_scenario_combobox'): self.simulation_controls.sim_scenario_combobox["values"] = scenario_names def _on_load_scenario(self, scenario_name: str): if self.is_simulation_running.get(): self._on_stop_simulation() self.logger.info(f"Loading scenario: {scenario_name}") scenario_data = self.config_manager.get_scenario(scenario_name) if scenario_data: self.simulation_hub.clear_simulated_data() self.ppi_widget.clear_trails() self.ppi_widget.update_simulated_targets([]) self.scenario = Scenario.from_dict(scenario_data) self.current_scenario_name = scenario_name self.target_list.update_target_list(self.scenario.get_all_targets()) self._update_all_views() if hasattr(self, 'simulation_controls') and hasattr(self.simulation_controls, 'sim_scenario_combobox'): self.simulation_controls.sim_scenario_combobox.set(scenario_name) self.ppi_widget.draw_scenario_preview(self.scenario) else: self.logger.warning(f"Attempted to load a non-existent scenario: {scenario_name}") def _on_save_scenario(self, scenario_name: str): self.logger.info(f"Saving scenario: {scenario_name}") self.scenario.name = scenario_name self.scenario.targets = {t.target_id: t for t in self.target_list.get_targets()} self.config_manager.save_scenario(scenario_name, self.scenario.to_dict()) self.current_scenario_name = scenario_name self._load_scenarios_into_ui() self._update_window_title() messagebox.showinfo("Success", f"Scenario '{scenario_name}' saved.", parent=self) def _on_save_scenario_as(self, scenario_name: str): self._on_save_scenario(scenario_name) def _on_new_scenario(self, scenario_name: str): if scenario_name in self.config_manager.get_scenario_names(): messagebox.showinfo("Duplicate", f"Scenario '{scenario_name}' already exists.", parent=self) self._on_load_scenario(scenario_name) return self.logger.info(f"Creating new scenario: {scenario_name}") self.scenario = Scenario(name=scenario_name) self.current_scenario_name = scenario_name self._update_all_views() names = list(self.config_manager.get_scenario_names()) + [scenario_name] self.scenario_controls.update_scenario_list(names, select_scenario=scenario_name) if hasattr(self, 'simulation_controls') and hasattr(self.simulation_controls, 'sim_scenario_combobox'): self.simulation_controls.sim_scenario_combobox["values"] = names self.simulation_controls.sim_scenario_combobox.set(scenario_name) def _on_delete_scenario(self, scenario_name: str): self.logger.info(f"Deleting scenario: {scenario_name}") self.config_manager.delete_scenario(scenario_name) if self.current_scenario_name == scenario_name: self.current_scenario_name = None self.scenario = Scenario() self._update_all_views() self._load_scenarios_into_ui() def _on_send_lru_status(self): pass def _open_radar_config(self): dialog = RadarConfigWindow( self, current_scan_limit=self.scan_limit, current_max_range=self.max_range ) if dialog.scan_limit is not None and dialog.max_range is not None: if self.scan_limit != dialog.scan_limit or self.max_range != dialog.max_range: self.scan_limit = dialog.scan_limit self.max_range = dialog.max_range self.ppi_widget.reconfigure_radar( max_range_nm=self.max_range, scan_limit_deg=self.scan_limit ) def _on_closing(self): self.logger.info("Application shutting down.") if self.is_simulation_running.get(): self._on_stop_simulation() settings_to_save = { "scan_limit": self.scan_limit, "max_range": self.max_range, "geometry": self.winfo_geometry(), "last_selected_scenario": self.current_scenario_name, } self.config_manager.save_general_settings(settings_to_save) if self.target_communicator: self.communicator_manager.remove_connection_state_callback(self._on_connection_state_change) if self.target_communicator.is_open: self.target_communicator.disconnect() self.status_bar.stop_resource_monitor() shutdown_logging_system() self.destroy() def _open_sfp_debug_window(self): if self.sfp_debug_window and self.sfp_debug_window.winfo_exists(): self.sfp_debug_window.lift() return self.sfp_debug_window = SfpDebugWindow(self) def _open_logger_panel(self): LoggerPanel(self) def _build_display_data_from_hub(self) -> Dict[str, List[Target]]: return build_display_data( self.simulation_hub, scenario=self.scenario, engine=self.simulation_engine, ppi_widget=self.ppi_widget, logger=self.logger, ) def _gui_refresh_loop(self): sim_is_running_now = (self.simulation_engine is not None and self.simulation_engine.is_running()) if self.is_simulation_running.get() and not sim_is_running_now: self._on_simulation_finished() # Update PPI with the latest data from the hub display_data = self._build_display_data_from_hub() self.ppi_widget.update_simulated_targets(display_data.get("simulated", [])) self.ppi_widget.update_real_targets(display_data.get("real", [])) if self.simulation_hub: az_deg, az_ts = self.simulation_hub.get_antenna_azimuth() if az_deg is not None: self.ppi_widget.update_antenna_azimuth(az_deg, timestamp=az_ts) if sim_is_running_now: if self.simulation_engine and self.simulation_engine.scenario: times = [getattr(t, "_sim_time_s", 0.0) for t in self.simulation_engine.scenario.get_all_targets()] self.sim_elapsed_time = max(times) if times else 0.0 if self.total_sim_time > 0 and not self._slider_is_dragging: progress = min(1.0, self.sim_elapsed_time / self.total_sim_time) self.simulation_controls.sim_slider_var.set(progress) self._update_simulation_progress_display() self.after(GUI_REFRESH_RATE_MS, self._gui_refresh_loop) def _refresh_analysis_list(self): self.analysis_tree.delete(*self.analysis_tree.get_children()) archive_folder = SimulationArchive.ARCHIVE_FOLDER if not os.path.exists(archive_folder): return runs = [] for filename in os.listdir(archive_folder): if filename.endswith(".json"): filepath = os.path.join(archive_folder, filename) try: with open(filepath, "r", encoding="utf-8") as f: data = json.load(f) metadata = data.get("metadata", {}) dt_str = filename.split("_")[0] time_str = filename.split("_")[1] run_info = { "datetime_obj": datetime.strptime(f"{dt_str}_{time_str}", "%Y%m%d_%H%M%S"), "scenario": metadata.get("scenario_name", "N/A"), "duration": f"{metadata.get('duration_seconds', 0):.1f}", "filepath": filepath, } runs.append(run_info) except Exception as e: self.logger.warning(f"Could not read archive {filename}: {e}") for run in sorted(runs, key=lambda r: r["datetime_obj"], reverse=True): self.analysis_tree.insert( "", tk.END, values=(run["datetime_obj"].strftime("%Y-%m-%d %H:%M:%S"), run["scenario"], run["duration"]), iid=run["filepath"], ) def _open_archive_folder(self): archive_folder = SimulationArchive.ARCHIVE_FOLDER try: os.makedirs(archive_folder, exist_ok=True) if sys.platform == "win32": os.startfile(os.path.abspath(archive_folder)) elif sys.platform == "darwin": import subprocess subprocess.run(["open", os.path.abspath(archive_folder)]) else: import subprocess subprocess.run(["xdg-open", os.path.abspath(archive_folder)]) except Exception as e: self.logger.exception(f"Failed to open archive folder: {e}") messagebox.showerror("Error", f"Could not open archive folder automatically.") def _on_analyze_run(self): selected_item = self.analysis_tree.focus() if not selected_item: messagebox.showinfo("No Selection", "Please select a simulation run to analyze.") return archive_filepath = selected_item AnalysisWindow(self, archive_filepath=archive_filepath)