# 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 import os from typing import Optional from .ppi_display import PPIDisplay from .settings_window import SettingsWindow from .radar_config_window import RadarConfigWindow from .scenario_controls_frame import ScenarioControlsFrame from .target_list_frame import TargetListFrame from .add_target_window import AddTargetWindow from core.serial_communicator import SerialCommunicator from core.models import Scenario, Target from utils.logger import get_logger, shutdown_logging_system from utils.config_manager import ConfigManager class MainView(tk.Tk): """The main application window.""" def __init__(self): super().__init__() self.logger = get_logger(__name__) self.config_manager = ConfigManager() # -- 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_port = settings.get('connection_port', None) self.communicator = SerialCommunicator() self.scenario = Scenario() self.current_scenario_name: Optional[str] = None self.title("Radar Target Simulator") self.geometry(settings.get('geometry', '1200x900')) self.minsize(900, 700) self._create_menubar() self._create_main_layout() self._create_statusbar() self._load_scenarios_into_ui() last_scenario = settings.get("last_selected_scenario") if last_scenario: self._on_load_scenario(last_scenario) self._update_window_title() self.protocol("WM_DELETE_WINDOW", self._on_closing) self.logger.info("MainView initialized successfully.") def _create_main_layout(self): """Creates the main paned layout based on the new design.""" 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) left_frame = ttk.Frame(self.h_pane) self.h_pane.add(left_frame, weight=1) self.ppi_widget = PPIDisplay(self.h_pane, max_range_nm=self.max_range, scan_limit_deg=self.scan_limit) self.h_pane.add(self.ppi_widget, weight=2) self.scenario_controls = ScenarioControlsFrame( left_frame, load_scenario_command=self._on_load_scenario, save_as_command=self._on_save_scenario_as, update_command=self._on_update_scenario, delete_command=self._on_delete_scenario ) self.scenario_controls.pack(fill=tk.X, expand=False, padx=5, pady=(0, 5)) self.target_list = TargetListFrame(left_frame) self.target_list.pack(fill=tk.BOTH, expand=True, padx=5) self.target_list.add_button.config(command=self._on_add_target) 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_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) def _create_statusbar(self): self.status_var = tk.StringVar(value="Status: Disconnected") status_bar = ttk.Label(self, textvariable=self.status_var, anchor=tk.W, relief=tk.SUNKEN) status_bar.pack(side=tk.BOTTOM, fill=tk.X) def _update_window_title(self): """Updates the window title with the current scenario name.""" title = "Radar Target Simulator" if self.current_scenario_name: title = f"{self.current_scenario_name} - {title}" self.title(title) def _update_all_views(self): """Refreshes all GUI components based on the current scenario.""" self._update_window_title() all_targets = self.scenario.get_all_targets() self.target_list.update_target_list(all_targets) self.ppi_widget.update_targets(all_targets) self.logger.info(f"Views updated for scenario '{self.current_scenario_name}'. It has {len(all_targets)} target(s).") def _load_scenarios_into_ui(self): """Loads scenario names from config and updates the combobox.""" scenario_names = self.config_manager.get_scenario_names() self.scenario_controls.update_scenario_list(scenario_names, self.current_scenario_name) def _on_add_target(self): self.logger.debug("'Add Target' button clicked. Opening dialog.") existing_ids = list(self.scenario.targets.keys()) dialog = AddTargetWindow(self, existing_ids) new_target = dialog.new_target if new_target: self.logger.info(f"New target created with ID {new_target.target_id}.") self.scenario.add_target(new_target) self._update_all_views() else: self.logger.debug("Add target dialog was cancelled.") def _on_load_scenario(self, scenario_name: str): self.logger.info(f"Loading scenario: {scenario_name}") scenario_data = self.config_manager.get_scenario(scenario_name) if scenario_data: try: self.scenario = Scenario.from_dict(scenario_data) self.current_scenario_name = scenario_name self._update_all_views() except Exception as e: self.logger.error(f"Failed to parse scenario data for '{scenario_name}': {e}") messagebox.showerror("Load Error", f"Could not load scenario '{scenario_name}'.\n{e}") else: self.logger.warning(f"Attempted to load a non-existent scenario: {scenario_name}") def _on_save_scenario_as(self, scenario_name: str): self.logger.info(f"Saving current state as new scenario: {scenario_name}") scenario_data = self.scenario.to_dict() self.config_manager.save_scenario(scenario_name, scenario_data) self.current_scenario_name = scenario_name self._load_scenarios_into_ui() # Refresh list self._update_window_title() messagebox.showinfo("Success", f"Scenario '{scenario_name}' saved successfully.", parent=self) def _on_update_scenario(self, scenario_name: str): self.logger.info(f"Updating scenario: {scenario_name}") scenario_data = self.scenario.to_dict() self.config_manager.save_scenario(scenario_name, scenario_data) self._load_scenarios_into_ui() # Refresh list to be safe messagebox.showinfo("Success", f"Scenario '{scenario_name}' updated successfully.", parent=self) def _on_delete_scenario(self, scenario_name: str): self.logger.info(f"Deleting scenario: {scenario_name}") self.config_manager.delete_scenario(scenario_name) self.logger.info(f"Scenario '{scenario_name}' deleted.") # Clear current view if the deleted scenario was active if self.current_scenario_name == scenario_name: self.scenario = Scenario() self.current_scenario_name = None self._update_all_views() self._load_scenarios_into_ui() messagebox.showinfo("Deleted", f"Scenario '{scenario_name}' has been deleted.", parent=self) def _open_settings(self): self.logger.info("Opening settings window.") SettingsWindow(self, self.communicator) def _open_radar_config(self): self.logger.info("Opening radar config window.") dialog = RadarConfigWindow(self) if dialog.scan_limit is not None and dialog.max_range is not None: self.scan_limit = dialog.scan_limit self.max_range = dialog.max_range self.logger.info(f"Scan limit set to: ±{self.scan_limit} degrees") self.logger.info(f"Max range set to: {self.max_range} NM") self.ppi_widget.destroy() self.ppi_widget = PPIDisplay(self.h_pane, max_range_nm=self.max_range, scan_limit_deg=self.scan_limit) self.h_pane.add(self.ppi_widget, weight=2) self._update_all_views() def update_connection_settings(self, port: str): self.connection_port = port self.logger.info(f"Connection port set to: {self.connection_port}") self.status_var.set(f"Status: Configured for {port}. Ready to connect.") def _on_closing(self): self.logger.info("Application shutting down.") # Save general settings settings_to_save = { 'scan_limit': self.scan_limit, 'max_range': self.max_range, 'connection_port': self.connection_port, 'geometry': self.winfo_geometry(), 'last_selected_scenario': self.current_scenario_name } self.config_manager.save_general_settings(settings_to_save) if self.communicator.is_open: self.communicator.disconnect() shutdown_logging_system() self.destroy()