add configuration manager, fix minor display ppi
This commit is contained in:
parent
870d79889d
commit
276cae0692
10
settings.json
Normal file
10
settings.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"general": {
|
||||||
|
"scan_limit": 60,
|
||||||
|
"max_range": 200,
|
||||||
|
"connection_port": null,
|
||||||
|
"geometry": "1200x900+338+338",
|
||||||
|
"last_selected_scenario": null
|
||||||
|
},
|
||||||
|
"scenarios": {}
|
||||||
|
}
|
||||||
@ -4,7 +4,9 @@
|
|||||||
Main view of the application, containing the primary window and widgets.
|
Main view of the application, containing the primary window and widgets.
|
||||||
"""
|
"""
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk, scrolledtext
|
from tkinter import ttk, scrolledtext, messagebox
|
||||||
|
import os
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from .ppi_display import PPIDisplay
|
from .ppi_display import PPIDisplay
|
||||||
from .settings_window import SettingsWindow
|
from .settings_window import SettingsWindow
|
||||||
@ -15,57 +17,70 @@ from .add_target_window import AddTargetWindow
|
|||||||
from core.serial_communicator import SerialCommunicator
|
from core.serial_communicator import SerialCommunicator
|
||||||
from core.models import Scenario, Target
|
from core.models import Scenario, Target
|
||||||
from utils.logger import get_logger, shutdown_logging_system
|
from utils.logger import get_logger, shutdown_logging_system
|
||||||
|
from utils.config_manager import ConfigManager
|
||||||
|
|
||||||
class MainView(tk.Tk):
|
class MainView(tk.Tk):
|
||||||
"""The main application window."""
|
"""The main application window."""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.logger = get_logger(__name__)
|
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.communicator = SerialCommunicator()
|
||||||
self.scenario = Scenario()
|
self.scenario = Scenario()
|
||||||
self.connection_port: str | None = None
|
self.current_scenario_name: Optional[str] = None
|
||||||
self.scan_limit = 60
|
|
||||||
self.max_range = 100
|
|
||||||
|
|
||||||
self.title("Radar Target Simulator")
|
self.title("Radar Target Simulator")
|
||||||
self.geometry("1200x900")
|
self.geometry(settings.get('geometry', '1200x900'))
|
||||||
self.minsize(900, 700)
|
self.minsize(900, 700)
|
||||||
|
|
||||||
self._create_menubar()
|
self._create_menubar()
|
||||||
self._create_main_layout()
|
self._create_main_layout()
|
||||||
self._create_statusbar()
|
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.")
|
self.logger.info("MainView initialized successfully.")
|
||||||
|
|
||||||
def _create_main_layout(self):
|
def _create_main_layout(self):
|
||||||
"""Creates the main paned layout based on the new design."""
|
"""Creates the main paned layout based on the new design."""
|
||||||
# Main vertical pane: Top (Content) / Bottom (Logs)
|
|
||||||
v_pane = ttk.PanedWindow(self, orient=tk.VERTICAL)
|
v_pane = ttk.PanedWindow(self, orient=tk.VERTICAL)
|
||||||
v_pane.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
v_pane.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||||
|
|
||||||
# Top horizontal pane: Left (Controls) / Right (PPI)
|
|
||||||
self.h_pane = ttk.PanedWindow(v_pane, orient=tk.HORIZONTAL)
|
self.h_pane = ttk.PanedWindow(v_pane, orient=tk.HORIZONTAL)
|
||||||
v_pane.add(self.h_pane, weight=4)
|
v_pane.add(self.h_pane, weight=4)
|
||||||
|
|
||||||
# --- Left Side Frame (for controls) ---
|
|
||||||
left_frame = ttk.Frame(self.h_pane)
|
left_frame = ttk.Frame(self.h_pane)
|
||||||
self.h_pane.add(left_frame, weight=1)
|
self.h_pane.add(left_frame, weight=1)
|
||||||
|
|
||||||
# --- Right Side Frame (for PPI) ---
|
|
||||||
self.ppi_widget = PPIDisplay(self.h_pane, max_range_nm=self.max_range, scan_limit_deg=self.scan_limit)
|
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.h_pane.add(self.ppi_widget, weight=2)
|
||||||
|
|
||||||
# --- Populate Left Frame ---
|
self.scenario_controls = ScenarioControlsFrame(
|
||||||
self.scenario_controls = ScenarioControlsFrame(left_frame)
|
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.scenario_controls.pack(fill=tk.X, expand=False, padx=5, pady=(0, 5))
|
||||||
|
|
||||||
self.target_list = TargetListFrame(left_frame)
|
self.target_list = TargetListFrame(left_frame)
|
||||||
self.target_list.pack(fill=tk.BOTH, expand=True, padx=5)
|
self.target_list.pack(fill=tk.BOTH, expand=True, padx=5)
|
||||||
# Connect button command
|
|
||||||
self.target_list.add_button.config(command=self._on_add_target)
|
self.target_list.add_button.config(command=self._on_add_target)
|
||||||
|
|
||||||
# --- Logger (Bottom) ---
|
|
||||||
log_frame_container = ttk.LabelFrame(v_pane, text="Logs")
|
log_frame_container = ttk.LabelFrame(v_pane, text="Logs")
|
||||||
v_pane.add(log_frame_container, weight=1)
|
v_pane.add(log_frame_container, weight=1)
|
||||||
|
|
||||||
@ -75,7 +90,6 @@ class MainView(tk.Tk):
|
|||||||
self.log_text_widget.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
self.log_text_widget.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||||
|
|
||||||
def _create_menubar(self):
|
def _create_menubar(self):
|
||||||
# (This method remains unchanged)
|
|
||||||
menubar = tk.Menu(self)
|
menubar = tk.Menu(self)
|
||||||
self.config(menu=menubar)
|
self.config(menu=menubar)
|
||||||
|
|
||||||
@ -85,24 +99,33 @@ class MainView(tk.Tk):
|
|||||||
settings_menu.add_command(label="Radar Config...", command=self._open_radar_config)
|
settings_menu.add_command(label="Radar Config...", command=self._open_radar_config)
|
||||||
|
|
||||||
def _create_statusbar(self):
|
def _create_statusbar(self):
|
||||||
# (This method remains unchanged)
|
|
||||||
self.status_var = tk.StringVar(value="Status: Disconnected")
|
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 = ttk.Label(self, textvariable=self.status_var, anchor=tk.W, relief=tk.SUNKEN)
|
||||||
status_bar.pack(side=tk.BOTTOM, fill=tk.X)
|
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):
|
def _update_all_views(self):
|
||||||
"""Refreshes all GUI components based on the current scenario."""
|
"""Refreshes all GUI components based on the current scenario."""
|
||||||
|
self._update_window_title()
|
||||||
all_targets = self.scenario.get_all_targets()
|
all_targets = self.scenario.get_all_targets()
|
||||||
self.target_list.update_target_list(all_targets)
|
self.target_list.update_target_list(all_targets)
|
||||||
self.ppi_widget.update_targets(all_targets)
|
self.ppi_widget.update_targets(all_targets)
|
||||||
self.logger.info(f"Views updated. Scenario now has {len(all_targets)} target(s).")
|
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):
|
def _on_add_target(self):
|
||||||
"""Handles the 'Add Target' button click."""
|
|
||||||
self.logger.debug("'Add Target' button clicked. Opening dialog.")
|
self.logger.debug("'Add Target' button clicked. Opening dialog.")
|
||||||
existing_ids = list(self.scenario.targets.keys())
|
existing_ids = list(self.scenario.targets.keys())
|
||||||
|
|
||||||
# This will block until the dialog is closed
|
|
||||||
dialog = AddTargetWindow(self, existing_ids)
|
dialog = AddTargetWindow(self, existing_ids)
|
||||||
|
|
||||||
new_target = dialog.new_target
|
new_target = dialog.new_target
|
||||||
@ -113,6 +136,50 @@ class MainView(tk.Tk):
|
|||||||
else:
|
else:
|
||||||
self.logger.debug("Add target dialog was cancelled.")
|
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):
|
def _open_settings(self):
|
||||||
self.logger.info("Opening settings window.")
|
self.logger.info("Opening settings window.")
|
||||||
SettingsWindow(self, self.communicator)
|
SettingsWindow(self, self.communicator)
|
||||||
@ -135,9 +202,18 @@ class MainView(tk.Tk):
|
|||||||
self.logger.info(f"Connection port set to: {self.connection_port}")
|
self.logger.info(f"Connection port set to: {self.connection_port}")
|
||||||
self.status_var.set(f"Status: Configured for {port}. Ready to connect.")
|
self.status_var.set(f"Status: Configured for {port}. Ready to connect.")
|
||||||
|
|
||||||
|
|
||||||
def _on_closing(self):
|
def _on_closing(self):
|
||||||
self.logger.info("Application shutting down.")
|
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:
|
if self.communicator.is_open:
|
||||||
self.communicator.disconnect()
|
self.communicator.disconnect()
|
||||||
shutdown_logging_system()
|
shutdown_logging_system()
|
||||||
|
|||||||
@ -10,6 +10,8 @@ import math
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from matplotlib.figure import Figure
|
from matplotlib.figure import Figure
|
||||||
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
||||||
|
import matplotlib.transforms as transforms
|
||||||
|
import matplotlib.markers as markers
|
||||||
|
|
||||||
from core.models import Target
|
from core.models import Target
|
||||||
from typing import List
|
from typing import List
|
||||||
@ -22,6 +24,7 @@ class PPIDisplay(ttk.Frame):
|
|||||||
super().__init__(master)
|
super().__init__(master)
|
||||||
self.max_range = max_range_nm
|
self.max_range = max_range_nm
|
||||||
self.scan_limit_deg = scan_limit_deg
|
self.scan_limit_deg = scan_limit_deg
|
||||||
|
self.target_artists = []
|
||||||
|
|
||||||
self._create_controls()
|
self._create_controls()
|
||||||
self._create_plot()
|
self._create_plot()
|
||||||
@ -62,6 +65,20 @@ class PPIDisplay(ttk.Frame):
|
|||||||
self.ax.set_theta_direction(-1) # Clockwise rotation
|
self.ax.set_theta_direction(-1) # Clockwise rotation
|
||||||
self.ax.set_rlabel_position(90)
|
self.ax.set_rlabel_position(90)
|
||||||
self.ax.set_ylim(0, self.max_range)
|
self.ax.set_ylim(0, self.max_range)
|
||||||
|
|
||||||
|
# Set custom theta labels for sector scan view
|
||||||
|
angles = np.arange(0, 360, 30)
|
||||||
|
labels = []
|
||||||
|
for angle in angles:
|
||||||
|
if angle == 0:
|
||||||
|
labels.append("0°")
|
||||||
|
elif angle < 180:
|
||||||
|
labels.append(f"+{angle}°")
|
||||||
|
elif angle == 180:
|
||||||
|
labels.append("±180°")
|
||||||
|
else: # angle > 180
|
||||||
|
labels.append(f"-{360 - angle}°")
|
||||||
|
self.ax.set_thetagrids(angles, labels)
|
||||||
|
|
||||||
# Style the plot
|
# Style the plot
|
||||||
self.ax.tick_params(axis='x', colors='white')
|
self.ax.tick_params(axis='x', colors='white')
|
||||||
@ -76,9 +93,6 @@ class PPIDisplay(ttk.Frame):
|
|||||||
self.ax.plot([limit_rad, limit_rad], [0, self.max_range], color='yellow', linestyle='--', linewidth=1)
|
self.ax.plot([limit_rad, limit_rad], [0, self.max_range], color='yellow', linestyle='--', linewidth=1)
|
||||||
self.ax.plot([-limit_rad, -limit_rad], [0, self.max_range], color='yellow', linestyle='--', linewidth=1)
|
self.ax.plot([-limit_rad, -limit_rad], [0, self.max_range], color='yellow', linestyle='--', linewidth=1)
|
||||||
|
|
||||||
# Initial plot for targets (an empty plot to be updated later)
|
|
||||||
self.target_plot, = self.ax.plot([], [], 'ro', markersize=6) # 'ro' = red circle
|
|
||||||
|
|
||||||
# Embed the plot in a Tkinter canvas
|
# Embed the plot in a Tkinter canvas
|
||||||
self.canvas = FigureCanvasTkAgg(fig, master=self)
|
self.canvas = FigureCanvasTkAgg(fig, master=self)
|
||||||
self.canvas.draw()
|
self.canvas.draw()
|
||||||
@ -88,6 +102,8 @@ class PPIDisplay(ttk.Frame):
|
|||||||
"""Handles the selection of a new range from the combobox."""
|
"""Handles the selection of a new range from the combobox."""
|
||||||
new_range = self.range_var.get()
|
new_range = self.range_var.get()
|
||||||
self.ax.set_ylim(0, new_range)
|
self.ax.set_ylim(0, new_range)
|
||||||
|
# Update heading vector lengths on zoom
|
||||||
|
self.update_targets([]) # Pass empty list to redraw existing ones with new scale
|
||||||
self.canvas.draw()
|
self.canvas.draw()
|
||||||
|
|
||||||
def update_targets(self, targets: List[Target]):
|
def update_targets(self, targets: List[Target]):
|
||||||
@ -97,15 +113,47 @@ class PPIDisplay(ttk.Frame):
|
|||||||
Args:
|
Args:
|
||||||
targets: A list of Target objects to display.
|
targets: A list of Target objects to display.
|
||||||
"""
|
"""
|
||||||
active_targets = [t for t in targets if t.active]
|
# If an empty list is passed, it means we just want to redraw existing targets
|
||||||
|
# (e.g., after a zoom change), not clear them.
|
||||||
if not active_targets:
|
if targets:
|
||||||
self.target_plot.set_data([], [])
|
self.active_targets = [t for t in targets if t.active]
|
||||||
else:
|
|
||||||
# Matplotlib polar plot needs angles in radians
|
# Clear previously drawn targets
|
||||||
theta = [math.radians(t.azimuth_deg) for t in active_targets]
|
for artist in self.target_artists:
|
||||||
r = [t.range_nm for t in active_targets]
|
artist.remove()
|
||||||
self.target_plot.set_data(theta, r)
|
self.target_artists.clear()
|
||||||
|
|
||||||
|
vector_len = self.range_var.get() / 25 # Length of heading vector relative to current view
|
||||||
|
|
||||||
|
for target in getattr(self, 'active_targets', []):
|
||||||
|
# Target position in polar
|
||||||
|
r1 = target.range_nm
|
||||||
|
th1 = np.deg2rad(target.azimuth_deg)
|
||||||
|
|
||||||
|
# Plot the target's center dot
|
||||||
|
dot, = self.ax.plot(th1, r1, 'o', markersize=5, color='red')
|
||||||
|
self.target_artists.append(dot)
|
||||||
|
|
||||||
|
# --- Calculate and plot heading vector ---
|
||||||
|
# Convert target polar to cartesian
|
||||||
|
x1 = r1 * np.sin(th1)
|
||||||
|
y1 = r1 * np.cos(th1)
|
||||||
|
|
||||||
|
# Calculate heading vector components
|
||||||
|
h_rad = np.deg2rad(target.heading_deg)
|
||||||
|
dx = vector_len * np.sin(h_rad)
|
||||||
|
dy = vector_len * np.cos(h_rad)
|
||||||
|
|
||||||
|
# Calculate end point of the vector in cartesian
|
||||||
|
x2, y2 = x1 + dx, y1 + dy
|
||||||
|
|
||||||
|
# Convert end point back to polar
|
||||||
|
r2 = np.sqrt(x2**2 + y2**2)
|
||||||
|
th2 = np.arctan2(x2, y2)
|
||||||
|
|
||||||
|
# Plot the heading line
|
||||||
|
line, = self.ax.plot([th1, th2], [r1, r2], color='red', linewidth=1.2)
|
||||||
|
self.target_artists.append(line)
|
||||||
|
|
||||||
# Redraw the canvas to show the changes
|
# Redraw the canvas to show the changes
|
||||||
self.canvas.draw()
|
self.canvas.draw()
|
||||||
@ -1,32 +1,113 @@
|
|||||||
# target_simulator/gui/scenario_controls_frame.py
|
# target_simulator/gui/scenario_controls_frame.py
|
||||||
|
|
||||||
"""
|
"""
|
||||||
A frame containing widgets for managing scenarios (loading, saving, deleting).
|
A frame containing widgets for managing scenarios.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import ttk, simpledialog, messagebox
|
||||||
|
from typing import Callable, List
|
||||||
|
|
||||||
class ScenarioControlsFrame(ttk.LabelFrame):
|
class ScenarioControlsFrame(ttk.LabelFrame):
|
||||||
"""Frame for scenario management controls."""
|
"""Frame for scenario management controls."""
|
||||||
|
|
||||||
def __init__(self, master):
|
def __init__(self, master, *,
|
||||||
super().__init__(master, text="Scenario Management")
|
load_scenario_command: Callable[[str], None],
|
||||||
|
save_as_command: Callable[[str], None],
|
||||||
|
update_command: Callable[[str], None],
|
||||||
|
delete_command: Callable[[str], None]):
|
||||||
|
super().__init__(master, text="Scenario Controls")
|
||||||
|
|
||||||
self.columnconfigure(1, weight=1)
|
self._load_scenario_command = load_scenario_command
|
||||||
|
self._save_as_command = save_as_command
|
||||||
|
self._update_command = update_command
|
||||||
|
self._delete_command = delete_command
|
||||||
|
|
||||||
|
self.current_scenario = tk.StringVar()
|
||||||
|
|
||||||
# --- Widgets ---
|
# --- Widgets ---
|
||||||
ttk.Label(self, text="Scenario:").grid(row=0, column=0, padx=(5, 0), pady=5, sticky=tk.W)
|
top_frame = ttk.Frame(self)
|
||||||
|
top_frame.pack(fill=tk.X, padx=5, pady=(5, 2))
|
||||||
|
|
||||||
self.scenario_combobox = ttk.Combobox(self, state="readonly")
|
scenario_label = ttk.Label(top_frame, text="Scenario:")
|
||||||
self.scenario_combobox.grid(row=0, column=1, padx=5, pady=5, sticky=tk.EW)
|
scenario_label.pack(side=tk.LEFT, padx=(0, 5))
|
||||||
|
|
||||||
|
self.scenario_combobox = ttk.Combobox(
|
||||||
|
top_frame,
|
||||||
|
textvariable=self.current_scenario,
|
||||||
|
state="readonly"
|
||||||
|
)
|
||||||
|
self.scenario_combobox.pack(side=tk.LEFT, expand=True, fill=tk.X)
|
||||||
|
self.scenario_combobox.bind("<<ComboboxSelected>>", self._on_scenario_select)
|
||||||
|
|
||||||
# --- Buttons ---
|
|
||||||
button_frame = ttk.Frame(self)
|
button_frame = ttk.Frame(self)
|
||||||
button_frame.grid(row=1, column=0, columnspan=2, pady=5, sticky=tk.W)
|
button_frame.pack(fill=tk.X, padx=5, pady=5)
|
||||||
|
|
||||||
self.save_button = ttk.Button(button_frame, text="Save Current...")
|
self.save_as_button = ttk.Button(
|
||||||
self.save_button.pack(side=tk.LEFT, padx=5)
|
button_frame, text="Save As...", command=self._on_save_as
|
||||||
|
)
|
||||||
|
self.save_as_button.pack(side=tk.LEFT, padx=(0, 5))
|
||||||
|
|
||||||
self.delete_button = ttk.Button(button_frame, text="Delete Selected")
|
self.update_button = ttk.Button(
|
||||||
self.delete_button.pack(side=tk.LEFT, padx=5)
|
button_frame, text="Update", command=self._on_update
|
||||||
|
)
|
||||||
|
self.update_button.pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
|
self.delete_button = ttk.Button(
|
||||||
|
button_frame, text="Delete", command=self._on_delete
|
||||||
|
)
|
||||||
|
self.delete_button.pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
|
def _on_scenario_select(self, event):
|
||||||
|
"""Callback for when a scenario is selected from the combobox."""
|
||||||
|
scenario_name = self.current_scenario.get()
|
||||||
|
if scenario_name:
|
||||||
|
self._load_scenario_command(scenario_name)
|
||||||
|
|
||||||
|
def _on_save_as(self):
|
||||||
|
"""Handle the 'Save As' button click."""
|
||||||
|
scenario_name = simpledialog.askstring(
|
||||||
|
"Save Scenario As",
|
||||||
|
"Enter a name for the new scenario:",
|
||||||
|
parent=self
|
||||||
|
)
|
||||||
|
if scenario_name:
|
||||||
|
self._save_as_command(scenario_name)
|
||||||
|
|
||||||
|
def _on_update(self):
|
||||||
|
"""Handle the 'Update' button click."""
|
||||||
|
scenario_name = self.current_scenario.get()
|
||||||
|
if not scenario_name:
|
||||||
|
messagebox.showwarning("No Scenario Selected", "Please select a scenario to update.", parent=self)
|
||||||
|
return
|
||||||
|
self._update_command(scenario_name)
|
||||||
|
|
||||||
|
def _on_delete(self):
|
||||||
|
"""Handle the 'Delete' button click."""
|
||||||
|
scenario_name = self.current_scenario.get()
|
||||||
|
if not scenario_name:
|
||||||
|
messagebox.showwarning("No Scenario Selected", "Please select a scenario to delete.", parent=self)
|
||||||
|
return
|
||||||
|
|
||||||
|
if messagebox.askyesno(
|
||||||
|
"Confirm Delete",
|
||||||
|
f"Are you sure you want to delete the scenario '{scenario_name}'?",
|
||||||
|
parent=self
|
||||||
|
):
|
||||||
|
self._delete_command(scenario_name)
|
||||||
|
|
||||||
|
def update_scenario_list(self, scenarios: List[str], select_scenario: str = None):
|
||||||
|
"""
|
||||||
|
Updates the list of scenarios in the combobox.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
scenarios: A list of scenario names.
|
||||||
|
select_scenario: The name of the scenario to select after updating, if any.
|
||||||
|
"""
|
||||||
|
self.scenario_combobox['values'] = scenarios
|
||||||
|
if select_scenario and select_scenario in scenarios:
|
||||||
|
self.current_scenario.set(select_scenario)
|
||||||
|
elif scenarios:
|
||||||
|
self.current_scenario.set(scenarios[0]) # Select the first one
|
||||||
|
else:
|
||||||
|
self.current_scenario.set("")
|
||||||
|
|||||||
99
target_simulator/utils/config_manager.py
Normal file
99
target_simulator/utils/config_manager.py
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
# target_simulator/utils/config_manager.py
|
||||||
|
|
||||||
|
"""
|
||||||
|
Manages loading and saving of application settings and scenarios to a single JSON file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from typing import Dict, Any, Optional, List
|
||||||
|
|
||||||
|
class ConfigManager:
|
||||||
|
"""Handles reading and writing application settings and scenarios from a JSON file."""
|
||||||
|
|
||||||
|
def __init__(self, filename: str = "settings.json"):
|
||||||
|
"""
|
||||||
|
Initializes the ConfigManager.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename: The name of the settings file. It will be stored in
|
||||||
|
the project root directory.
|
||||||
|
"""
|
||||||
|
if getattr(sys, 'frozen', False):
|
||||||
|
application_path = os.path.dirname(sys.executable)
|
||||||
|
else:
|
||||||
|
application_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||||
|
|
||||||
|
self.filepath = os.path.join(application_path, filename)
|
||||||
|
self._settings = self._load_or_initialize_settings()
|
||||||
|
|
||||||
|
def _load_or_initialize_settings(self) -> Dict[str, Any]:
|
||||||
|
"""Loads settings from the JSON file or initializes with a default structure."""
|
||||||
|
if not os.path.exists(self.filepath):
|
||||||
|
return {"general": {}, "scenarios": {}}
|
||||||
|
try:
|
||||||
|
with open(self.filepath, 'r', encoding='utf-8') as f:
|
||||||
|
settings = json.load(f)
|
||||||
|
if not isinstance(settings, dict) or "general" not in settings or "scenarios" not in settings:
|
||||||
|
return {"general": settings, "scenarios": {}}
|
||||||
|
return settings
|
||||||
|
except (json.JSONDecodeError, IOError):
|
||||||
|
return {"general": {}, "scenarios": {}}
|
||||||
|
|
||||||
|
def _save_settings(self):
|
||||||
|
"""Saves the current settings to the JSON file."""
|
||||||
|
try:
|
||||||
|
with open(self.filepath, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(self._settings, f, indent=4)
|
||||||
|
except IOError as e:
|
||||||
|
print(f"Error saving settings to {self.filepath}: {e}")
|
||||||
|
|
||||||
|
def get_general_settings(self) -> Dict[str, Any]:
|
||||||
|
"""Returns the general settings."""
|
||||||
|
return self._settings.get("general", {})
|
||||||
|
|
||||||
|
def save_general_settings(self, data: Dict[str, Any]):
|
||||||
|
"""Saves the general settings."""
|
||||||
|
self._settings["general"] = data
|
||||||
|
self._save_settings()
|
||||||
|
|
||||||
|
def get_scenario_names(self) -> List[str]:
|
||||||
|
"""Returns a list of all scenario names."""
|
||||||
|
return list(self._settings.get("scenarios", {}).keys())
|
||||||
|
|
||||||
|
def get_scenario(self, name: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Retrieves a specific scenario by name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: The name of the scenario to retrieve.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A dictionary with the scenario data, or None if not found.
|
||||||
|
"""
|
||||||
|
return self._settings.get("scenarios", {}).get(name)
|
||||||
|
|
||||||
|
def save_scenario(self, name: str, data: Dict[str, Any]):
|
||||||
|
"""
|
||||||
|
Saves or updates a scenario.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: The name of the scenario to save.
|
||||||
|
data: The dictionary of scenario data to save.
|
||||||
|
"""
|
||||||
|
if "scenarios" not in self._settings:
|
||||||
|
self._settings["scenarios"] = {}
|
||||||
|
self._settings["scenarios"][name] = data
|
||||||
|
self._save_settings()
|
||||||
|
|
||||||
|
def delete_scenario(self, name: str):
|
||||||
|
"""
|
||||||
|
Deletes a scenario by name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: The name of the scenario to delete.
|
||||||
|
"""
|
||||||
|
if "scenarios" in self._settings and name in self._settings["scenarios"]:
|
||||||
|
del self._settings["scenarios"][name]
|
||||||
|
self._save_settings()
|
||||||
Loading…
Reference in New Issue
Block a user