center gui into screen
This commit is contained in:
parent
90d09cd005
commit
1527526188
@ -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:
|
||||||
|
|||||||
@ -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,7 +57,9 @@ 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
|
||||||
@ -68,7 +70,7 @@ def _get_value_from_path(batch: DataBatch, field: ExportField) -> Any:
|
|||||||
# 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)
|
||||||
|
|||||||
@ -503,6 +503,7 @@ 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
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
@ -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,12 +23,14 @@ 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
|
||||||
|
|
||||||
@ -35,32 +38,40 @@ class EditPathDialog(tk.Toplevel):
|
|||||||
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()
|
||||||
@ -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."""
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user