# radar_data_reader/gui/profile_editor_window.py """ GUI Window for creating, editing, and deleting export profiles. """ import tkinter as tk from tkinter import ttk, simpledialog, messagebox import ctypes import copy from typing import List, Type, Dict, Any, Union, Optional from ..core import data_structures as ds from ..core.data_enums import ENUM_REGISTRY from ..core.export_profiles import ExportProfile, ExportField from ..utils import logger log = logger.get_logger(__name__) class ProfileEditorWindow(tk.Toplevel): """A Toplevel window for managing export profiles.""" def __init__(self, master, controller, profiles: List[ExportProfile]): super().__init__(master) self.master = master self.controller = controller self.profiles = copy.deepcopy(profiles) self._original_profiles_dict = {p.name: p.to_dict() for p in self.profiles} self._init_window() self._init_vars() self._create_widgets() self._populate_available_fields_tree() self._load_profiles_to_combobox() self.protocol("WM_DELETE_WINDOW", self._on_close) def _init_window(self): """Initializes window properties.""" self.title("Export Profile Editor") self.geometry("1200x700") # Increased width for new column self.transient(self.master) self.grab_set() def _init_vars(self): """Initializes Tkinter variables.""" self.selected_profile_name = tk.StringVar() def _create_widgets(self): """Creates the main layout and widgets for the editor.""" main_pane = ttk.PanedWindow(self, orient=tk.HORIZONTAL) main_pane.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # --- Left Frame: Profile Management --- profile_mgmt_frame = ttk.LabelFrame(main_pane, text="Profiles") main_pane.add(profile_mgmt_frame, weight=2) # Adjusted weight profile_mgmt_frame.columnconfigure(0, weight=1) cb_frame = ttk.Frame(profile_mgmt_frame) cb_frame.grid(row=0, column=0, sticky="ew", padx=5, pady=5) cb_frame.columnconfigure(0, weight=1) self.profile_combobox = ttk.Combobox( cb_frame, textvariable=self.selected_profile_name, state="readonly" ) self.profile_combobox.grid(row=0, column=0, sticky="ew") self.profile_combobox.bind("<>", self._on_profile_selected) btn_frame = ttk.Frame(profile_mgmt_frame) btn_frame.grid(row=1, column=0, sticky="ew", padx=5) btn_frame.columnconfigure((0, 1), weight=1) ttk.Button(btn_frame, text="New", command=self._on_new_profile).grid(row=0, column=0, sticky="ew", padx=2) ttk.Button(btn_frame, text="Delete", command=self._on_delete_profile).grid(row=0, column=1, sticky="ew", padx=2) # --- Middle Frame: Available Fields --- fields_frame = ttk.LabelFrame(main_pane, text="Available Fields") main_pane.add(fields_frame, weight=3) # Adjusted weight fields_frame.rowconfigure(0, weight=1) fields_frame.columnconfigure(0, weight=1) self.fields_tree = ttk.Treeview(fields_frame, selectmode="browse") self.fields_tree.grid(row=0, column=0, sticky="nsew", padx=5, pady=5) ysb = ttk.Scrollbar(fields_frame, orient="vertical", command=self.fields_tree.yview) self.fields_tree.configure(yscrollcommand=ysb.set) ysb.grid(row=0, column=1, sticky="ns") # --- Right Frame: Selected Fields and Actions --- selected_frame_container = ttk.Frame(main_pane) main_pane.add(selected_frame_container, weight=5) # Adjusted weight selected_frame_container.rowconfigure(0, weight=1) selected_frame_container.columnconfigure(1, weight=1) action_btn_frame = ttk.Frame(selected_frame_container) action_btn_frame.grid(row=0, column=0, sticky="ns", padx=5, pady=5) ttk.Button(action_btn_frame, text=">>", command=self._add_field).grid(pady=5) ttk.Button(action_btn_frame, text="<<", command=self._remove_field).grid(pady=5) ttk.Button(action_btn_frame, text="Up", command=lambda: self._move_field(-1)).grid(pady=20) ttk.Button(action_btn_frame, text="Down", command=lambda: self._move_field(1)).grid(pady=5) ttk.Button( action_btn_frame, text="Reset", command=self._clear_selected_fields ).grid(pady=20) selected_fields_frame = ttk.LabelFrame(selected_frame_container, text="Selected Fields for Profile") selected_fields_frame.grid(row=0, column=1, sticky="nsew") selected_fields_frame.rowconfigure(0, weight=1) selected_fields_frame.columnconfigure(0, weight=1) self.selected_tree = ttk.Treeview( selected_fields_frame, columns=("display_name", "data_path", "translate"), # Added data_path column show="headings", selectmode="browse", ) self.selected_tree.heading("display_name", text="Field Name") self.selected_tree.heading("data_path", text="Source Path") # New header self.selected_tree.heading("translate", text="Translate") self.selected_tree.column("display_name", width=150, stretch=True) self.selected_tree.column("data_path", width=250, stretch=True) # New column config self.selected_tree.column("translate", width=80, anchor="center", stretch=False) self.selected_tree.grid(row=0, column=0, sticky="nsew") self.selected_tree.bind("", self._on_selected_tree_click) # --- Bottom Frame: Save/Cancel --- bottom_frame = ttk.Frame(self) bottom_frame.pack(fill=tk.X, padx=10, pady=(0, 10)) ttk.Button(bottom_frame, text="Save & Close", command=self._on_save_and_close).pack(side=tk.RIGHT) ttk.Button(bottom_frame, text="Cancel", command=self._on_close).pack(side=tk.RIGHT, padx=5) def _clear_selected_fields(self): """Clears all fields from the currently selected profile.""" profile = self._get_current_profile() if not profile: return if not profile.fields: # Do nothing if already empty return if messagebox.askyesno( "Confirm Clear", f"Are you sure you want to remove all fields from the profile '{profile.name}'?", parent=self ): profile.fields.clear() self._load_profile_into_ui() def _on_selected_tree_click(self, event): region = self.selected_tree.identify_region(event.x, event.y) if region != "cell": return column_id = self.selected_tree.identify_column(event.x) if column_id != "#3": # Column is now the 3rd one return item_id = self.selected_tree.identify_row(event.y) if not item_id: return profile = self._get_current_profile() if not profile: return field_index = int(item_id) field = profile.fields[field_index] if field.data_path in ENUM_REGISTRY: field.translate_with_enum = not field.translate_with_enum self._load_profile_into_ui() def _populate_available_fields_tree(self): self.fields_tree.delete(*self.fields_tree.get_children()) batch_root = self.fields_tree.insert("", "end", iid="batch_properties", text="Batch Properties") self.fields_tree.insert(batch_root, "end", iid="batch_id", text="batch_id", values=("batch_id", "batch_id")) header_root = self.fields_tree.insert("", "end", iid="header_data", text="Header Data (from DSPHDRIN)") self._recursive_populate_tree_ctypes(ds.GeHeader, header_root, "main_header.ge_header") cdpsts_root = self.fields_tree.insert("", "end", iid="cdpsts_data", text="CDP/STS Block Data") self._recursive_populate_tree_ctypes(ds.CdpDataLayout, cdpsts_root, "cdp_sts_results.payload.data") timer_root = self.fields_tree.insert("", "end", iid="timer_data", text="Timer Block Data") self._recursive_populate_tree_ctypes(ds.GrifoTimerBlob, timer_root, "timer_data.blob") def _recursive_populate_tree_ctypes(self, class_obj: Type[ctypes.Structure], parent_id: str, base_path: str): if not hasattr(class_obj, '_fields_'): return for field_name, field_type in class_obj._fields_: current_path = f"{base_path}.{field_name}" node_id = f"{parent_id}_{field_name}" if hasattr(field_type, '_fields_'): child_node = self.fields_tree.insert(parent_id, "end", iid=node_id, text=field_name) self._recursive_populate_tree_ctypes(field_type, child_node, current_path) elif hasattr(field_type, '_length_'): self.fields_tree.insert(parent_id, "end", iid=node_id, text=f"{field_name} [Array]", values=(field_name, current_path)) else: display_text = f"{field_name}" if current_path in ENUM_REGISTRY: display_text += " (Enum)" self.fields_tree.insert(parent_id, "end", iid=node_id, text=display_text, values=(field_name, current_path)) def _load_profiles_to_combobox(self): profile_names = [p.name for p in self.profiles] self.profile_combobox["values"] = profile_names if profile_names: self.selected_profile_name.set(profile_names[0]) self._load_profile_into_ui() def _get_current_profile(self) -> Optional[ExportProfile]: name = self.selected_profile_name.get() return next((p for p in self.profiles if p.name == name), None) def _on_profile_selected(self, event=None): self._load_profile_into_ui() def _load_profile_into_ui(self): for i in self.selected_tree.get_children(): self.selected_tree.delete(i) profile = self._get_current_profile() if not profile: return for index, field in enumerate(profile.fields): is_translatable = field.data_path in ENUM_REGISTRY checkbox_char = "☐" if is_translatable: checkbox_char = "☑" if field.translate_with_enum else "☐" # Show a more readable source path source_display = '.'.join(field.data_path.split('.')[:2]) self.selected_tree.insert( "", "end", iid=str(index), values=(field.column_name, source_display, checkbox_char) ) def _add_field(self): selected_item_id = self.fields_tree.focus() if not selected_item_id: return item_values = self.fields_tree.item(selected_item_id, "values") if not item_values or len(item_values) < 2: messagebox.showinfo("Cannot Add Field", "Please select a specific data field.", parent=self) return column_name, data_path = item_values profile = self._get_current_profile() if not profile: return if any(f.data_path == data_path for f in profile.fields): messagebox.showinfo("Duplicate Field", "This field is already in the profile.", parent=self) return profile.fields.append(ExportField(column_name=column_name, data_path=data_path)) self._load_profile_into_ui() def _remove_field(self): selection = self.selected_tree.selection() if not selection: return index_to_remove = int(selection[0]) profile = self._get_current_profile() if not profile: return del profile.fields[index_to_remove] self._load_profile_into_ui() def _move_field(self, direction: int): selection = self.selected_tree.selection() if not selection: return index = int(selection[0]) new_index = index + direction profile = self._get_current_profile() if not profile or not (0 <= new_index < len(profile.fields)): return fields = profile.fields fields.insert(new_index, fields.pop(index)) self._load_profile_into_ui() self.selected_tree.selection_set(str(new_index)) def _on_new_profile(self): name = simpledialog.askstring("New Profile", "Enter a name for the new profile:", parent=self) if not name or not name.strip(): return if any(p.name == name for p in self.profiles): messagebox.showerror("Error", f"A profile with the name '{name}' already exists.", parent=self) return new_profile = ExportProfile(name=name.strip()) self.profiles.append(new_profile) self._load_profiles_to_combobox() self.selected_profile_name.set(name) self._load_profile_into_ui() def _on_delete_profile(self): profile = self._get_current_profile() if not profile: return if messagebox.askyesno("Confirm Delete", f"Are you sure you want to delete the profile '{profile.name}'?", parent=self): self.profiles.remove(profile) self._load_profiles_to_combobox() def _check_unsaved_changes(self) -> bool: current_profiles_dict = {p.name: p.to_dict() for p in self.profiles} return current_profiles_dict != self._original_profiles_dict def _on_save_and_close(self): log.info("Saving export profiles and closing editor.") self.controller.save_export_profiles(self.profiles) self.destroy() def _on_close(self): if self._check_unsaved_changes(): response = messagebox.askyesnocancel("Unsaved Changes", "You have unsaved changes. Would you like to save them?", parent=self) if response is True: self._on_save_and_close() elif response is False: self.destroy() else: self.destroy()