220 lines
9.4 KiB
Python
220 lines
9.4 KiB
Python
# 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() |