SXXXXXXX_RadarDataReader/radar_data_reader/gui/profile_editor_window.py
VALLONGOL 90d09cd005 add new filed for timer
add edit path in profile window
add array function  to export
2025-06-24 14:30:13 +02:00

346 lines
16 KiB
Python

# radar_data_reader/gui/profile_editor_window.py
"""
GUI Window for creating, editing, and deleting export profiles.
Includes a custom dialog for editing complex data paths.
"""
import tkinter as tk
from tkinter import ttk, simpledialog, messagebox
import ctypes
import copy
import re
from typing import List, Type, Dict, Any, 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 EditPathDialog(tk.Toplevel):
"""A custom dialog window to edit a data path string."""
def __init__(self, parent, initial_value=""):
super().__init__(parent)
self.transient(parent)
self.grab_set()
self.title("Edit Data Path")
self.geometry("800x150") # Wide dialog for long paths
self.result = None
main_frame = ttk.Frame(self, padding="10")
main_frame.pack(fill=tk.BOTH, expand=True)
main_frame.columnconfigure(0, weight=1)
# Instructions Label
instructions = (
"Enter the full data path. Use '.' for attributes and '[index]' for arrays.\n"
"Example 1: main_header.ge_header.mode.master_mode\n"
"Example 2: timer_data.blob.payload.aesa_delay[0].fifo[3]"
)
ttk.Label(main_frame, text=instructions, justify=tk.LEFT).grid(row=0, column=0, sticky="w", pady=(0, 10))
# Entry Widget
self.path_var = tk.StringVar(value=initial_value)
self.path_entry = ttk.Entry(main_frame, textvariable=self.path_var, font=("Courier", 10))
self.path_entry.grid(row=1, column=0, sticky="ew")
self.path_entry.focus_set()
self.path_entry.selection_range(0, tk.END)
# Buttons Frame
button_frame = ttk.Frame(main_frame)
button_frame.grid(row=2, column=0, sticky="e", pady=(10, 0))
ttk.Button(button_frame, text="OK", command=self._on_ok, default=tk.ACTIVE).pack(side=tk.LEFT, padx=5)
ttk.Button(button_frame, text="Cancel", command=self._on_cancel).pack(side=tk.LEFT)
self.protocol("WM_DELETE_WINDOW", self._on_cancel)
self.bind("<Return>", self._on_ok)
self.bind("<Escape>", self._on_cancel)
def _on_ok(self, event=None):
self.result = self.path_var.get().strip()
self.destroy()
def _on_cancel(self, event=None):
self.result = None
self.destroy()
def wait_for_input(self):
"""Waits for the dialog to close and returns the result."""
self.wait_window()
return self.result
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):
self.title("Export Profile Editor")
self.geometry("1200x700")
self.transient(self.master)
self.grab_set()
def _init_vars(self):
self.selected_profile_name = tk.StringVar()
def _create_widgets(self):
main_pane = ttk.PanedWindow(self, orient=tk.HORIZONTAL)
main_pane.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
profile_mgmt_frame = ttk.LabelFrame(main_pane, text="Profiles")
main_pane.add(profile_mgmt_frame, weight=2)
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("<<ComboboxSelected>>", 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)
fields_frame = ttk.LabelFrame(main_pane, text="Available Fields")
main_pane.add(fields_frame, weight=3)
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")
selected_frame_container = ttk.Frame(main_pane)
main_pane.add(selected_frame_container, weight=5)
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=10)
ttk.Button(action_btn_frame, text="Down", command=lambda: self._move_field(1)).grid(pady=5)
ttk.Button(action_btn_frame, text="Edit Path", command=self._edit_selected_field_path).grid(pady=10)
ttk.Button(action_btn_frame, text="Reset", command=self._clear_selected_fields).grid(pady=5)
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"), show="headings", selectmode="browse")
self.selected_tree.heading("display_name", text="Field Name")
self.selected_tree.heading("data_path", text="Source Path")
self.selected_tree.heading("translate", text="Translate")
self.selected_tree.column("display_name", width=150, stretch=True)
self.selected_tree.column("data_path", width=300, stretch=True) # Widened column
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("<Button-1>", self._on_selected_tree_click)
self.selected_tree.bind("<Double-1>", self._on_selected_tree_double_click)
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 _on_selected_tree_double_click(self, event):
"""Handle double-click on the selected fields tree to edit path."""
self._edit_selected_field_path()
def _edit_selected_field_path(self):
"""Allows manual editing of the data_path for the selected field."""
selection = self.selected_tree.selection()
if not selection:
messagebox.showinfo("No Selection", "Please select a field to edit.", parent=self)
return
index = int(selection[0])
profile = self._get_current_profile()
if not profile: return
field = profile.fields[index]
dialog = EditPathDialog(self, initial_value=field.data_path)
new_path = dialog.wait_for_input()
if new_path is not None and new_path != field.data_path:
# Basic validation for the path format
if not re.match(r'^[\w\.\[\]]+$', new_path):
messagebox.showerror("Invalid Path", "The path contains invalid characters.", parent=self)
return
field.data_path = new_path
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": 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 or re.sub(r'\[\d+\]', '', field.data_path) in ENUM_REGISTRY:
field.translate_with_enum = not field.translate_with_enum
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):
base_path = re.sub(r'\[\d+\]', '', field.data_path)
is_translatable = base_path in ENUM_REGISTRY
checkbox_char = ""
if is_translatable: checkbox_char = "" if field.translate_with_enum else ""
self.selected_tree.insert("", "end", iid=str(index), values=(field.column_name, field.data_path, checkbox_char))
def _clear_selected_fields(self):
"""Clears all fields from the currently selected profile."""
profile = self._get_current_profile()
if not profile or not profile.fields: 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()
# --- Other methods remain unchanged ---
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 _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()