center gui into screen

This commit is contained in:
VALLONGOL 2025-06-24 14:37:36 +02:00
parent 90d09cd005
commit 1527526188
6 changed files with 307 additions and 111 deletions

View File

@ -7,6 +7,7 @@ import logging
from pathlib import Path from pathlib import Path
from radar_data_reader.gui.main_window import MainWindow from radar_data_reader.gui.main_window import MainWindow
from radar_data_reader.gui.gui_utils import center_window
from radar_data_reader.utils import logger from radar_data_reader.utils import logger
from radar_data_reader.utils.config_manager import ConfigManager from radar_data_reader.utils.config_manager import ConfigManager
from radar_data_reader.core.app_controller import AppController from radar_data_reader.core.app_controller import AppController
@ -41,6 +42,8 @@ def main():
controller = AppController(config_manager) controller = AppController(config_manager)
root = tk.Tk() root = tk.Tk()
# Hide the window initially to prevent flickering while it's being built
root.withdraw()
logger.setup_basic_logging(root, LOGGING_CONFIG) logger.setup_basic_logging(root, LOGGING_CONFIG)
@ -50,18 +53,18 @@ def main():
controller.bind_view(view) controller.bind_view(view)
# Center the main window on the screen before showing it
center_window(root)
view.mainloop() view.mainloop()
except Exception as e: except Exception as e:
log_or_print = ( log_or_print = (
logger.get_logger(__name__) if logger._logging_system_active else print logger.get_logger(__name__) if logger._logging_system_active else print
) )
# --- THIS IS THE CORRECTED LINE ---
if callable(log_or_print): if callable(log_or_print):
# This case is for the fallback 'print' function
log_or_print(f"A critical error occurred: {e}") log_or_print(f"A critical error occurred: {e}")
else: else:
# This case is for the logger object
log_or_print.critical(f"A critical error occurred: {e}", exc_info=True) log_or_print.critical(f"A critical error occurred: {e}", exc_info=True)
if root: if root:

View File

@ -41,14 +41,14 @@ def _get_value_from_path(batch: DataBatch, field: ExportField) -> Any:
return batch.batch_id return batch.batch_id
# Split the path by dots and brackets to handle attributes and indices # Split the path by dots and brackets to handle attributes and indices
parts = re.split(r'\.|\[' , path) parts = re.split(r"\.|\[", path)
current_obj = batch current_obj = batch
for part in parts: for part in parts:
if current_obj is None: if current_obj is None:
return "N/A" return "N/A"
if part.endswith(']'): if part.endswith("]"):
# This is an index access # This is an index access
index_str = part[:-1] index_str = part[:-1]
if not index_str.isdigit(): if not index_str.isdigit():
@ -57,22 +57,24 @@ def _get_value_from_path(batch: DataBatch, field: ExportField) -> Any:
try: try:
current_obj = current_obj[int(index_str)] current_obj = current_obj[int(index_str)]
except (IndexError, TypeError): except (IndexError, TypeError):
log.warning(f"Index out of bounds for '{index_str}' in path: {path}") log.warning(
f"Index out of bounds for '{index_str}' in path: {path}"
)
return "N/A" return "N/A"
else: else:
# This is an attribute access # This is an attribute access
current_obj = getattr(current_obj, part, None) current_obj = getattr(current_obj, part, None)
value = current_obj if current_obj is not None else "N/A" value = current_obj if current_obj is not None else "N/A"
# Handle translation for enums if the final value is an integer # Handle translation for enums if the final value is an integer
if field.translate_with_enum and isinstance(value, int): if field.translate_with_enum and isinstance(value, int):
# For enum translation, we need the path without indices # For enum translation, we need the path without indices
enum_path = re.sub(r'\[\d+\]', '', path) enum_path = re.sub(r"\[\d+\]", "", path)
enum_class = ENUM_REGISTRY.get(enum_path) enum_class = ENUM_REGISTRY.get(enum_path)
if enum_class: if enum_class:
return get_enum_name(enum_class, value) return get_enum_name(enum_class, value)
return value return value
except Exception as e: except Exception as e:
log.warning(f"Could not resolve path '{field.data_path}': {e}") log.warning(f"Could not resolve path '{field.data_path}': {e}")

View File

@ -499,10 +499,11 @@ class DelayWidth(CtypesStructureBase):
class TimerRawIf(CtypesStructureBase): class TimerRawIf(CtypesStructureBase):
""" """
Mirrors the C++ `timer_raw_if_t` struct (Reduced initial version). Mirrors the C++ `timer_raw_if_t` struct (Reduced initial version).
This maps only the first, most stable part of the structure. This maps only the first, most stable part of the structure.
""" """
_fields_ = [ _fields_ = [
("tcr", ctypes.c_uint32), ("tcr", ctypes.c_uint32),
("tpr", ctypes.c_uint32), ("tpr", ctypes.c_uint32),
@ -512,19 +513,21 @@ class TimerRawIf(CtypesStructureBase):
("diff_prt_num", ctypes.c_uint16), ("diff_prt_num", ctypes.c_uint16),
("spares__", ctypes.c_uint32 * 3), ("spares__", ctypes.c_uint32 * 3),
("shift", ShiftRegisters), ("shift", ShiftRegisters),
("spare_after_shift", ctypes.c_byte * (12)), # Padding fino a 0x50 ("spare_after_shift", ctypes.c_byte * (12)), # Padding fino a 0x50
# Offset in C++: 0x50 # Offset in C++: 0x50
("aesa_delay", UniquePrtFifo * 2), # Dimensione: 64 bytes * 2 = 128 bytes. Va da 0x50 a 0xD0 (
("spare0__", ctypes.c_byte * 48), # Padding per raggiungere 0x100 (da 0xD0 a 0x100) "aesa_delay",
UniquePrtFifo * 2,
), # Dimensione: 64 bytes * 2 = 128 bytes. Va da 0x50 a 0xD0
(
"spare0__",
ctypes.c_byte * 48,
), # Padding per raggiungere 0x100 (da 0xD0 a 0x100)
# Offset in C++: 0x100 # Offset in C++: 0x100
("exp_pulse1_delay", UniquePrtFifo * 2), # da 0x100 a 0x180 ("exp_pulse1_delay", UniquePrtFifo * 2), # da 0x100 a 0x180
("exp_pulse2_delay", UniquePrtFifo * 2), # da 0x180 a 0x200 ("exp_pulse2_delay", UniquePrtFifo * 2), # da 0x180 a 0x200
# Offset in C++: 0x200 # Offset in C++: 0x200
("pretrigger_det_delay", UniquePrtFifo * 2), # da 0x200 a 0x280 ("pretrigger_det_delay", UniquePrtFifo * 2), # da 0x200 a 0x280
# Il resto è omesso per ora # Il resto è omesso per ora
] ]

View File

@ -0,0 +1,55 @@
# radar_data_reader/gui/gui_utils.py
"""
GUI utility functions, such as window centering logic, that can be reused
across different parts of the application or other projects.
"""
import tkinter as tk
from typing import Optional
def center_window(
window: tk.Tk | tk.Toplevel, parent: Optional[tk.Tk | tk.Toplevel] = None
):
"""
Centers a Tkinter window on the screen or relative to a parent window.
This function should be called after the window's initial size has been
set or determined, typically after creating all its widgets. Using
`window.update_idletasks()` right before this call ensures that the
window's dimensions are up-to-date.
Args:
window: The Tkinter window (tk.Tk or tk.Toplevel) to be centered.
parent: The optional parent window. If provided, `window` will be
centered relative to the parent. If None, `window` will be
centered on the screen.
"""
window.update_idletasks() # Ensure window dimensions are calculated
# Get window's dimensions
win_width = window.winfo_width()
win_height = window.winfo_height()
if parent:
# Center relative to the parent window
parent.update_idletasks()
parent_x = parent.winfo_x()
parent_y = parent.winfo_y()
parent_width = parent.winfo_width()
parent_height = parent.winfo_height()
pos_x = parent_x + (parent_width // 2) - (win_width // 2)
pos_y = parent_y + (parent_height // 2) - (win_height // 2)
else:
# Center on the screen
screen_width = window.winfo_screenwidth()
screen_height = window.winfo_screenheight()
pos_x = (screen_width // 2) - (win_width // 2)
pos_y = (screen_height // 2) - (win_height // 2)
# Set the window's position
window.geometry(f"{win_width}x{win_height}+{pos_x}+{pos_y}")
window.deiconify() # Ensure window is visible

View File

@ -6,12 +6,13 @@ Includes a custom dialog for editing complex data paths.
""" """
import tkinter as tk import tkinter as tk
from tkinter import ttk, simpledialog, messagebox from tkinter import ttk, messagebox
import ctypes import ctypes
import copy import copy
import re import re
from typing import List, Type, Dict, Any, Optional from typing import List, Type, Dict, Any, Optional
from .gui_utils import center_window
from ..core import data_structures as ds from ..core import data_structures as ds
from ..core.data_enums import ENUM_REGISTRY from ..core.data_enums import ENUM_REGISTRY
from ..core.export_profiles import ExportProfile, ExportField from ..core.export_profiles import ExportProfile, ExportField
@ -22,45 +23,55 @@ log = logger.get_logger(__name__)
class EditPathDialog(tk.Toplevel): class EditPathDialog(tk.Toplevel):
"""A custom dialog window to edit a data path string.""" """A custom dialog window to edit a data path string."""
def __init__(self, parent, initial_value=""): def __init__(self, parent, initial_value=""):
super().__init__(parent) super().__init__(parent)
self.transient(parent) self.transient(parent)
self.grab_set() self.grab_set()
self.title("Edit Data Path") self.title("Edit Data Path")
self.geometry("800x150") # Wide dialog for long paths
self.withdraw() # Hide until ready
self.result = None self.result = None
main_frame = ttk.Frame(self, padding="10") main_frame = ttk.Frame(self, padding="10")
main_frame.pack(fill=tk.BOTH, expand=True) main_frame.pack(fill=tk.BOTH, expand=True)
main_frame.columnconfigure(0, weight=1) main_frame.columnconfigure(0, weight=1)
# Instructions Label
instructions = ( instructions = (
"Enter the full data path. Use '.' for attributes and '[index]' for arrays.\n" "Enter the full data path. Use '.' for attributes and '[index]' for arrays.\n"
"Example 1: main_header.ge_header.mode.master_mode\n" "Example 1: main_header.ge_header.mode.master_mode\n"
"Example 2: timer_data.blob.payload.aesa_delay[0].fifo[3]" "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)) 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_var = tk.StringVar(value=initial_value)
self.path_entry = ttk.Entry(main_frame, textvariable=self.path_var, font=("Courier", 10)) self.path_entry = ttk.Entry(
main_frame, textvariable=self.path_var, font=("Courier", 10), width=90
)
self.path_entry.grid(row=1, column=0, sticky="ew") self.path_entry.grid(row=1, column=0, sticky="ew")
self.path_entry.focus_set() self.path_entry.focus_set()
self.path_entry.selection_range(0, tk.END) self.path_entry.selection_range(0, tk.END)
# Buttons Frame
button_frame = ttk.Frame(main_frame) button_frame = ttk.Frame(main_frame)
button_frame.grid(row=2, column=0, sticky="e", pady=(10, 0)) 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(
ttk.Button(button_frame, text="Cancel", command=self._on_cancel).pack(side=tk.LEFT) 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.protocol("WM_DELETE_WINDOW", self._on_cancel)
self.bind("<Return>", self._on_ok) self.bind("<Return>", self._on_ok)
self.bind("<Escape>", self._on_cancel) self.bind("<Escape>", self._on_cancel)
# Center the dialog relative to its parent
center_window(self, parent)
def _on_ok(self, event=None): def _on_ok(self, event=None):
self.result = self.path_var.get().strip() self.result = self.path_var.get().strip()
self.destroy() self.destroy()
@ -86,19 +97,24 @@ class ProfileEditorWindow(tk.Toplevel):
self.profiles = copy.deepcopy(profiles) self.profiles = copy.deepcopy(profiles)
self._original_profiles_dict = {p.name: p.to_dict() for p in self.profiles} self._original_profiles_dict = {p.name: p.to_dict() for p in self.profiles}
self.withdraw() # Hide until ready
self._init_window() self._init_window()
self._init_vars() self._init_vars()
self._create_widgets() self._create_widgets()
self._populate_available_fields_tree() self._populate_available_fields_tree()
self._load_profiles_to_combobox() self._load_profiles_to_combobox()
# Center the window relative to its parent
center_window(self, self.master)
self.protocol("WM_DELETE_WINDOW", self._on_close) self.protocol("WM_DELETE_WINDOW", self._on_close)
def _init_window(self): def _init_window(self):
self.title("Export Profile Editor") self.title("Export Profile Editor")
self.geometry("1200x700") self.geometry("1200x700")
self.transient(self.master) self.transient(self.master)
self.grab_set()
def _init_vars(self): def _init_vars(self):
self.selected_profile_name = tk.StringVar() self.selected_profile_name = tk.StringVar()
@ -114,15 +130,21 @@ class ProfileEditorWindow(tk.Toplevel):
cb_frame = ttk.Frame(profile_mgmt_frame) cb_frame = ttk.Frame(profile_mgmt_frame)
cb_frame.grid(row=0, column=0, sticky="ew", padx=5, pady=5) cb_frame.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
cb_frame.columnconfigure(0, weight=1) cb_frame.columnconfigure(0, weight=1)
self.profile_combobox = ttk.Combobox(cb_frame, textvariable=self.selected_profile_name, state="readonly") 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.grid(row=0, column=0, sticky="ew")
self.profile_combobox.bind("<<ComboboxSelected>>", self._on_profile_selected) self.profile_combobox.bind("<<ComboboxSelected>>", self._on_profile_selected)
btn_frame = ttk.Frame(profile_mgmt_frame) btn_frame = ttk.Frame(profile_mgmt_frame)
btn_frame.grid(row=1, column=0, sticky="ew", padx=5) btn_frame.grid(row=1, column=0, sticky="ew", padx=5)
btn_frame.columnconfigure((0, 1), weight=1) 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="New", command=self._on_new_profile).grid(
ttk.Button(btn_frame, text="Delete", command=self._on_delete_profile).grid(row=0, column=1, sticky="ew", padx=2) 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") fields_frame = ttk.LabelFrame(main_pane, text="Available Fields")
main_pane.add(fields_frame, weight=3) main_pane.add(fields_frame, weight=3)
@ -130,7 +152,9 @@ class ProfileEditorWindow(tk.Toplevel):
fields_frame.columnconfigure(0, weight=1) fields_frame.columnconfigure(0, weight=1)
self.fields_tree = ttk.Treeview(fields_frame, selectmode="browse") self.fields_tree = ttk.Treeview(fields_frame, selectmode="browse")
self.fields_tree.grid(row=0, column=0, sticky="nsew", padx=5, pady=5) 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) ysb = ttk.Scrollbar(
fields_frame, orient="vertical", command=self.fields_tree.yview
)
self.fields_tree.configure(yscrollcommand=ysb.set) self.fields_tree.configure(yscrollcommand=ysb.set)
ysb.grid(row=0, column=1, sticky="ns") ysb.grid(row=0, column=1, sticky="ns")
@ -143,22 +167,37 @@ class ProfileEditorWindow(tk.Toplevel):
action_btn_frame.grid(row=0, column=0, sticky="ns", padx=5, pady=5) 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._add_field).grid(pady=5)
ttk.Button(action_btn_frame, text="<<", command=self._remove_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(
ttk.Button(action_btn_frame, text="Down", command=lambda: self._move_field(1)).grid(pady=5) action_btn_frame, text="Up", command=lambda: self._move_field(-1)
ttk.Button(action_btn_frame, text="Edit Path", command=self._edit_selected_field_path).grid(pady=10) ).grid(pady=10)
ttk.Button(action_btn_frame, text="Reset", command=self._clear_selected_fields).grid(pady=5) 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 = ttk.LabelFrame(
selected_frame_container, text="Selected Fields for Profile"
)
selected_fields_frame.grid(row=0, column=1, sticky="nsew") selected_fields_frame.grid(row=0, column=1, sticky="nsew")
selected_fields_frame.rowconfigure(0, weight=1) selected_fields_frame.rowconfigure(0, weight=1)
selected_fields_frame.columnconfigure(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 = 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("display_name", text="Field Name")
self.selected_tree.heading("data_path", text="Source Path") self.selected_tree.heading("data_path", text="Source Path")
self.selected_tree.heading("translate", text="Translate") self.selected_tree.heading("translate", text="Translate")
self.selected_tree.column("display_name", width=150, stretch=True) 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("data_path", width=300, stretch=True)
self.selected_tree.column("translate", width=80, anchor="center", stretch=False) self.selected_tree.column("translate", width=80, anchor="center", stretch=False)
self.selected_tree.grid(row=0, column=0, sticky="nsew") 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("<Button-1>", self._on_selected_tree_click)
@ -166,103 +205,163 @@ class ProfileEditorWindow(tk.Toplevel):
bottom_frame = ttk.Frame(self) bottom_frame = ttk.Frame(self)
bottom_frame.pack(fill=tk.X, padx=10, pady=(0, 10)) 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(
ttk.Button(bottom_frame, text="Cancel", command=self._on_close).pack(side=tk.RIGHT, padx=5) 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): def _on_selected_tree_double_click(self, event):
"""Handle double-click on the selected fields tree to edit path.""" item = self.selected_tree.identify_row(event.y)
self._edit_selected_field_path() if item:
self._edit_selected_field_path()
def _edit_selected_field_path(self): def _edit_selected_field_path(self):
"""Allows manual editing of the data_path for the selected field."""
selection = self.selected_tree.selection() selection = self.selected_tree.selection()
if not selection: if not selection:
messagebox.showinfo("No Selection", "Please select a field to edit.", parent=self) messagebox.showinfo(
"No Selection", "Please select a field to edit.", parent=self
)
return return
index = int(selection[0]) index = int(selection[0])
profile = self._get_current_profile() profile = self._get_current_profile()
if not profile: return if not profile:
return
field = profile.fields[index] field = profile.fields[index]
dialog = EditPathDialog(self, initial_value=field.data_path) dialog = EditPathDialog(self, initial_value=field.data_path)
new_path = dialog.wait_for_input() new_path = dialog.wait_for_input()
if new_path is not None and new_path != field.data_path: 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):
if not re.match(r'^[\w\.\[\]]+$', new_path): messagebox.showerror(
messagebox.showerror("Invalid Path", "The path contains invalid characters.", parent=self) "Invalid Path", "The path contains invalid characters.", parent=self
)
return return
field.data_path = new_path field.data_path = new_path
self._load_profile_into_ui() self._load_profile_into_ui()
def _on_selected_tree_click(self, event): def _on_selected_tree_click(self, event):
region = self.selected_tree.identify_region(event.x, event.y) region = self.selected_tree.identify_region(event.x, event.y)
if region != "cell": return if region != "cell":
return
column_id = self.selected_tree.identify_column(event.x) column_id = self.selected_tree.identify_column(event.x)
if column_id != "#3": return if column_id != "#3":
return
item_id = self.selected_tree.identify_row(event.y) item_id = self.selected_tree.identify_row(event.y)
if not item_id: return if not item_id:
return
profile = self._get_current_profile() profile = self._get_current_profile()
if not profile: return if not profile:
return
field_index = int(item_id) field_index = int(item_id)
field = profile.fields[field_index] field = profile.fields[field_index]
if field.data_path in ENUM_REGISTRY or re.sub(r'\[\d+\]', '', field.data_path) in ENUM_REGISTRY: base_path = re.sub(r"\[\d+\]", "", field.data_path)
if base_path in ENUM_REGISTRY:
field.translate_with_enum = not field.translate_with_enum field.translate_with_enum = not field.translate_with_enum
self._load_profile_into_ui() self._load_profile_into_ui()
def _load_profile_into_ui(self): def _load_profile_into_ui(self):
for i in self.selected_tree.get_children(): self.selected_tree.delete(i) for i in self.selected_tree.get_children():
self.selected_tree.delete(i)
profile = self._get_current_profile() profile = self._get_current_profile()
if not profile: return if not profile:
return
for index, field in enumerate(profile.fields): for index, field in enumerate(profile.fields):
base_path = re.sub(r'\[\d+\]', '', field.data_path) base_path = re.sub(r"\[\d+\]", "", field.data_path)
is_translatable = base_path in ENUM_REGISTRY is_translatable = base_path in ENUM_REGISTRY
checkbox_char = "" checkbox_char = ""
if is_translatable: checkbox_char = "" if field.translate_with_enum else "" if is_translatable:
self.selected_tree.insert("", "end", iid=str(index), values=(field.column_name, field.data_path, checkbox_char)) 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): def _clear_selected_fields(self):
"""Clears all fields from the currently selected profile."""
profile = self._get_current_profile() profile = self._get_current_profile()
if not profile or not profile.fields: return if not profile or not profile.fields:
if messagebox.askyesno("Confirm Clear", f"Are you sure you want to remove all fields from the profile '{profile.name}'?", parent=self): 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() profile.fields.clear()
self._load_profile_into_ui() self._load_profile_into_ui()
# --- Other methods remain unchanged ---
def _populate_available_fields_tree(self): def _populate_available_fields_tree(self):
self.fields_tree.delete(*self.fields_tree.get_children()) self.fields_tree.delete(*self.fields_tree.get_children())
batch_root = self.fields_tree.insert("", "end", iid="batch_properties", text="Batch Properties") batch_root = self.fields_tree.insert(
self.fields_tree.insert(batch_root, "end", iid="batch_id", text="batch_id", values=("batch_id", "batch_id")) "", "end", iid="batch_properties", text="Batch Properties"
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") self.fields_tree.insert(
cdpsts_root = self.fields_tree.insert("", "end", iid="cdpsts_data", text="CDP/STS Block Data") batch_root,
self._recursive_populate_tree_ctypes(ds.CdpDataLayout, cdpsts_root, "cdp_sts_results.payload.data") "end",
timer_root = self.fields_tree.insert("", "end", iid="timer_data", text="Timer Block Data") iid="batch_id",
self._recursive_populate_tree_ctypes(ds.GrifoTimerBlob, timer_root, "timer_data.blob") 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): def _recursive_populate_tree_ctypes(
if not hasattr(class_obj, '_fields_'): return 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_: for field_name, field_type in class_obj._fields_:
current_path = f"{base_path}.{field_name}" current_path = f"{base_path}.{field_name}"
node_id = f"{parent_id}_{field_name}" node_id = f"{parent_id}_{field_name}"
if hasattr(field_type, '_fields_'): if hasattr(field_type, "_fields_"):
child_node = self.fields_tree.insert(parent_id, "end", iid=node_id, text=field_name) child_node = self.fields_tree.insert(
self._recursive_populate_tree_ctypes(field_type, child_node, current_path) parent_id, "end", iid=node_id, text=field_name
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)) 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: else:
display_text = f"{field_name}" display_text = f"{field_name}"
if current_path in ENUM_REGISTRY: display_text += " (Enum)" if current_path in ENUM_REGISTRY:
self.fields_tree.insert(parent_id, "end", iid=node_id, text=display_text, values=(field_name, current_path)) 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): def _load_profiles_to_combobox(self):
profile_names = [p.name for p in self.profiles] profile_names = [p.name for p in self.profiles]
self.profile_combobox["values"] = profile_names self.profile_combobox["values"] = profile_names
if profile_names: self.selected_profile_name.set(profile_names[0]) if profile_names:
self.selected_profile_name.set(profile_names[0])
self._load_profile_into_ui() self._load_profile_into_ui()
def _get_current_profile(self) -> Optional[ExportProfile]: def _get_current_profile(self) -> Optional[ExportProfile]:
@ -274,46 +373,63 @@ class ProfileEditorWindow(tk.Toplevel):
def _add_field(self): def _add_field(self):
selected_item_id = self.fields_tree.focus() selected_item_id = self.fields_tree.focus()
if not selected_item_id: return if not selected_item_id:
return
item_values = self.fields_tree.item(selected_item_id, "values") item_values = self.fields_tree.item(selected_item_id, "values")
if not item_values or len(item_values) < 2: if not item_values or len(item_values) < 2:
messagebox.showinfo("Cannot Add Field", "Please select a specific data field.", parent=self) messagebox.showinfo(
"Cannot Add Field", "Please select a specific data field.", parent=self
)
return return
column_name, data_path = item_values column_name, data_path = item_values
profile = self._get_current_profile() profile = self._get_current_profile()
if not profile: return if not profile:
return
if any(f.data_path == data_path for f in profile.fields): 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) messagebox.showinfo(
"Duplicate Field", "This field is already in the profile.", parent=self
)
return return
profile.fields.append(ExportField(column_name=column_name, data_path=data_path)) profile.fields.append(ExportField(column_name=column_name, data_path=data_path))
self._load_profile_into_ui() self._load_profile_into_ui()
def _remove_field(self): def _remove_field(self):
selection = self.selected_tree.selection() selection = self.selected_tree.selection()
if not selection: return if not selection:
return
index_to_remove = int(selection[0]) index_to_remove = int(selection[0])
profile = self._get_current_profile() profile = self._get_current_profile()
if not profile: return if not profile:
return
del profile.fields[index_to_remove] del profile.fields[index_to_remove]
self._load_profile_into_ui() self._load_profile_into_ui()
def _move_field(self, direction: int): def _move_field(self, direction: int):
selection = self.selected_tree.selection() selection = self.selected_tree.selection()
if not selection: return if not selection:
return
index = int(selection[0]) index = int(selection[0])
new_index = index + direction new_index = index + direction
profile = self._get_current_profile() profile = self._get_current_profile()
if not profile or not (0 <= new_index < len(profile.fields)): return if not profile or not (0 <= new_index < len(profile.fields)):
return
fields = profile.fields fields = profile.fields
fields.insert(new_index, fields.pop(index)) fields.insert(new_index, fields.pop(index))
self._load_profile_into_ui() self._load_profile_into_ui()
self.selected_tree.selection_set(str(new_index)) self.selected_tree.selection_set(str(new_index))
def _on_new_profile(self): def _on_new_profile(self):
name = simpledialog.askstring("New Profile", "Enter a name for the new profile:", parent=self) name = simpledialog.askstring(
if not name or not name.strip(): return "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): if any(p.name == name for p in self.profiles):
messagebox.showerror("Error", f"A profile with the name '{name}' already exists.", parent=self) messagebox.showerror(
"Error",
f"A profile with the name '{name}' already exists.",
parent=self,
)
return return
new_profile = ExportProfile(name=name.strip()) new_profile = ExportProfile(name=name.strip())
self.profiles.append(new_profile) self.profiles.append(new_profile)
@ -323,8 +439,13 @@ class ProfileEditorWindow(tk.Toplevel):
def _on_delete_profile(self): def _on_delete_profile(self):
profile = self._get_current_profile() profile = self._get_current_profile()
if not profile: return if not profile:
if messagebox.askyesno("Confirm Delete", f"Are you sure you want to delete the profile '{profile.name}'?", parent=self): 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.profiles.remove(profile)
self._load_profiles_to_combobox() self._load_profiles_to_combobox()
@ -339,8 +460,14 @@ class ProfileEditorWindow(tk.Toplevel):
def _on_close(self): def _on_close(self):
if self._check_unsaved_changes(): if self._check_unsaved_changes():
response = messagebox.askyesnocancel("Unsaved Changes", "You have unsaved changes. Would you like to save them?", parent=self) response = messagebox.askyesnocancel(
if response is True: self._on_save_and_close() "Unsaved Changes",
elif response is False: self.destroy() "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: else:
self.destroy() self.destroy()

View File

@ -9,6 +9,8 @@ from tkinter import ttk, filedialog
from typing import Dict, Any from typing import Dict, Any
import os import os
from .gui_utils import center_window
class RecConfigWindow(tk.Toplevel): class RecConfigWindow(tk.Toplevel):
"""A Toplevel window for managing g_reconvert.exe parameters.""" """A Toplevel window for managing g_reconvert.exe parameters."""
@ -18,18 +20,22 @@ class RecConfigWindow(tk.Toplevel):
self.controller = controller self.controller = controller
self.config_data = current_config.copy() self.config_data = current_config.copy()
self.withdraw() # Hide window until it's ready to be shown
self._init_window() self._init_window()
self._init_vars() self._init_vars()
self._populate_vars_from_config() self._populate_vars_from_config()
self._create_widgets() self._create_widgets()
# Center the window relative to its parent
center_window(self, self.master)
self.protocol("WM_DELETE_WINDOW", self._on_cancel) self.protocol("WM_DELETE_WINDOW", self._on_cancel)
def _init_window(self): def _init_window(self):
self.title("g_reconverter Advanced Configuration") self.title("g_reconverter Advanced Configuration")
self.geometry("800x650") # Increased height slightly self.geometry("800x650")
self.transient(self.master) self.transient(self.master)
self.grab_set() # The centering is now handled by the utility function
def _init_vars(self): def _init_vars(self):
"""Initializes all Tkinter variables for the parameters.""" """Initializes all Tkinter variables for the parameters."""