add report info into log, change gui
This commit is contained in:
parent
f9383140fb
commit
9161e1f6b5
@ -8,27 +8,38 @@
|
|||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"column_name": "batch_id",
|
"column_name": "batch_id",
|
||||||
"data_path": "batch_id"
|
"data_path": "batch_id",
|
||||||
|
"translate_with_enum": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"column_name": "batch_counter",
|
"column_name": "batch_counter",
|
||||||
"data_path": "main_header.ge_header.signal_descr.batch_counter"
|
"data_path": "main_header.ge_header.signal_descr.batch_counter",
|
||||||
|
"translate_with_enum": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"column_name": "ttag",
|
"column_name": "ttag",
|
||||||
"data_path": "main_header.ge_header.signal_descr.ttag"
|
"data_path": "main_header.ge_header.signal_descr.ttag",
|
||||||
|
"translate_with_enum": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"column_name": "master_mode",
|
"column_name": "master_mode",
|
||||||
"data_path": "main_header.ge_header.mode.master_mode"
|
"data_path": "main_header.ge_header.mode.master_mode",
|
||||||
|
"translate_with_enum": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"column_name": "operation_mode",
|
"column_name": "operation_mode",
|
||||||
"data_path": "main_header.ge_header.mode.operation_mode"
|
"data_path": "main_header.ge_header.mode.operation_mode",
|
||||||
|
"translate_with_enum": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"column_name": "range_scale",
|
"column_name": "range_scale",
|
||||||
"data_path": "main_header.ge_header.mode.range_scale"
|
"data_path": "main_header.ge_header.mode.range_scale",
|
||||||
|
"translate_with_enum": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"column_name": "waveform",
|
||||||
|
"data_path": "main_header.ge_header.mode.waveform",
|
||||||
|
"translate_with_enum": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,17 +15,21 @@ from typing import List, Any, Dict
|
|||||||
from ..utils.config_manager import ConfigManager
|
from ..utils.config_manager import ConfigManager
|
||||||
from ..core.file_reader import run_worker_process
|
from ..core.file_reader import run_worker_process
|
||||||
from ..core.data_structures import DataBatch
|
from ..core.data_structures import DataBatch
|
||||||
|
from ..core.data_enums import ENUM_REGISTRY, get_enum_name
|
||||||
from ..utils import logger
|
from ..utils import logger
|
||||||
from ..gui.profile_editor_window import ProfileEditorWindow
|
from ..gui.profile_editor_window import ProfileEditorWindow
|
||||||
from ..core.export_profiles import ExportProfile
|
from ..core.export_profiles import ExportProfile, ExportField
|
||||||
|
|
||||||
log = logger.get_logger(__name__)
|
log = logger.get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _get_value_from_path(batch: DataBatch, path: str) -> Any:
|
def _get_value_from_path(batch: DataBatch, field: ExportField) -> Any:
|
||||||
"""
|
"""
|
||||||
Safely retrieves a value from a DataBatch object by navigating a dot-separated path.
|
Safely retrieves a value from a DataBatch object using the path from an ExportField.
|
||||||
|
If the field is marked for translation, it attempts to convert the numeric value
|
||||||
|
to its string representation using the corresponding Enum.
|
||||||
"""
|
"""
|
||||||
|
path = field.data_path
|
||||||
try:
|
try:
|
||||||
parts = path.split('.')
|
parts = path.split('.')
|
||||||
if not parts:
|
if not parts:
|
||||||
@ -40,7 +44,15 @@ def _get_value_from_path(batch: DataBatch, path: str) -> Any:
|
|||||||
return "N/A"
|
return "N/A"
|
||||||
current_obj = getattr(current_obj, part, None)
|
current_obj = getattr(current_obj, part, None)
|
||||||
|
|
||||||
return current_obj if current_obj is not None else "N/A"
|
value = current_obj if current_obj is not None else "N/A"
|
||||||
|
|
||||||
|
# --- ENUM TRANSLATION LOGIC ---
|
||||||
|
if field.translate_with_enum and isinstance(value, int):
|
||||||
|
enum_class = ENUM_REGISTRY.get(path)
|
||||||
|
if enum_class:
|
||||||
|
return get_enum_name(enum_class, value)
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
log.warning(f"Could not find attribute for path: {path}")
|
log.warning(f"Could not find attribute for path: {path}")
|
||||||
@ -83,15 +95,14 @@ class AppController:
|
|||||||
self.view.update_export_profiles(profiles, active_profile_name)
|
self.view.update_export_profiles(profiles, active_profile_name)
|
||||||
|
|
||||||
def _propose_output_filepath(self, input_path_str: str):
|
def _propose_output_filepath(self, input_path_str: str):
|
||||||
"""Proposes a default output path based on the input path."""
|
|
||||||
if not input_path_str:
|
if not input_path_str:
|
||||||
return
|
return
|
||||||
proposed_path = Path(input_path_str).with_suffix('.csv')
|
proposed_path = Path(input_path_str).with_suffix('.csv')
|
||||||
self.view.set_output_filepath(str(proposed_path))
|
self.view.set_output_filepath(str(proposed_path))
|
||||||
|
|
||||||
def select_file(self):
|
def select_file(self):
|
||||||
self.current_path = self.view.get_filepath()
|
current_path = self.view.get_filepath()
|
||||||
if filepath := self.view.ask_open_filename(self.current_path):
|
if filepath := self.view.ask_open_filename(current_path):
|
||||||
self.view.set_filepath(filepath)
|
self.view.set_filepath(filepath)
|
||||||
self._propose_output_filepath(filepath)
|
self._propose_output_filepath(filepath)
|
||||||
|
|
||||||
@ -190,14 +201,14 @@ class AppController:
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
row_values = [_get_value_from_path(batch, field.data_path) for field in self.active_export_profile.fields]
|
# Pass the entire ExportField object to the helper function
|
||||||
|
row_values = [_get_value_from_path(batch, field) for field in self.active_export_profile.fields]
|
||||||
self.csv_writer.writerow(row_values)
|
self.csv_writer.writerow(row_values)
|
||||||
self.csv_file_handle.flush()
|
self.csv_file_handle.flush()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f"An unexpected error occurred during CSV row writing: {e}", exc_info=True)
|
log.error(f"An unexpected error occurred during CSV row writing: {e}", exc_info=True)
|
||||||
|
|
||||||
def _log_summary(self, stats: Dict[str, int]):
|
def _log_summary(self, stats: Dict[str, int]):
|
||||||
"""Formats and logs a summary of the processing results."""
|
|
||||||
log.info("--- Processing Summary ---")
|
log.info("--- Processing Summary ---")
|
||||||
|
|
||||||
batches = stats.get("batches_processed", 0)
|
batches = stats.get("batches_processed", 0)
|
||||||
@ -245,7 +256,6 @@ class AppController:
|
|||||||
self.is_processing = False
|
self.is_processing = False
|
||||||
self.worker_process = None
|
self.worker_process = None
|
||||||
|
|
||||||
# Log the final summary
|
|
||||||
if stats := msg.get("stats"):
|
if stats := msg.get("stats"):
|
||||||
self._log_summary(stats)
|
self._log_summary(stats)
|
||||||
|
|
||||||
|
|||||||
69
radar_data_reader/core/data_enums.py
Normal file
69
radar_data_reader/core/data_enums.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
"""
|
||||||
|
This module contains Enum definitions that mirror the C++ enums,
|
||||||
|
used for translating numeric data fields into human-readable strings.
|
||||||
|
"""
|
||||||
|
from enum import Enum, unique
|
||||||
|
|
||||||
|
# Using @unique ensures that no two enum members have the same value.
|
||||||
|
# We use a helper function to handle unknown values gracefully.
|
||||||
|
|
||||||
|
def get_enum_name(enum_class, value):
|
||||||
|
"""Tries to get the name of an enum member, returns the value itself if not found."""
|
||||||
|
try:
|
||||||
|
return enum_class(value).name
|
||||||
|
except ValueError:
|
||||||
|
return value
|
||||||
|
|
||||||
|
@unique
|
||||||
|
class MasterMode(Enum):
|
||||||
|
"""Maps to the m2str function in postprocess.cpp"""
|
||||||
|
IDLE = 0
|
||||||
|
IBIT = 1
|
||||||
|
GM = 2
|
||||||
|
DBS = 3
|
||||||
|
RWS = 4
|
||||||
|
VS = 5
|
||||||
|
ACM = 6
|
||||||
|
TWS = 7
|
||||||
|
SEA1 = 8
|
||||||
|
SEA2 = 9
|
||||||
|
GMTI = 10
|
||||||
|
BCN = 11
|
||||||
|
SAM = 12
|
||||||
|
TA = 13
|
||||||
|
WA = 14
|
||||||
|
STT = 15
|
||||||
|
DTT = 16
|
||||||
|
SSTT = 17
|
||||||
|
ACQ = 18
|
||||||
|
FTT = 19
|
||||||
|
AGR = 20
|
||||||
|
SAR = 21
|
||||||
|
INV = 22
|
||||||
|
DUMMY = 23
|
||||||
|
ETET = 24
|
||||||
|
BOOT = 25
|
||||||
|
|
||||||
|
@unique
|
||||||
|
class Waveform(Enum):
|
||||||
|
"""Maps to the wf2str function in postprocess.cpp"""
|
||||||
|
IDLE = 0
|
||||||
|
LPRF = 1
|
||||||
|
MPRF = 2
|
||||||
|
HPRF = 3
|
||||||
|
LPRFNC = 4
|
||||||
|
MPRF_SHORT = 5
|
||||||
|
AGR_SHORT = 6
|
||||||
|
AGR_LONG = 7
|
||||||
|
MPRF_LOOK_UP = 8
|
||||||
|
MPRF_ADAPTIVE = 9
|
||||||
|
AUTO_WITHOUT_LPRF_NC = 10
|
||||||
|
|
||||||
|
# This dictionary acts as a registry, mapping a data path to the Enum class
|
||||||
|
# that can be used to translate its value. This will be used by the Profile Editor.
|
||||||
|
ENUM_REGISTRY = {
|
||||||
|
"main_header.ge_header.mode.master_mode": MasterMode,
|
||||||
|
"main_header.ge_header.mode.waveform": Waveform,
|
||||||
|
# Add other mappings here as we identify them
|
||||||
|
# "path.to.another.field": AnotherEnum,
|
||||||
|
}
|
||||||
@ -2,7 +2,7 @@
|
|||||||
Defines the data structures for managing export profiles.
|
Defines the data structures for managing export profiles.
|
||||||
"""
|
"""
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import List, Dict, Any, Optional
|
from typing import List, Dict, Any
|
||||||
|
|
||||||
# Using slots for performance, similar to data_structures.py
|
# Using slots for performance, similar to data_structures.py
|
||||||
DC_KWARGS = {'slots': True}
|
DC_KWARGS = {'slots': True}
|
||||||
@ -16,21 +16,20 @@ class ExportField:
|
|||||||
Attributes:
|
Attributes:
|
||||||
column_name (str): The name of the column in the CSV file header.
|
column_name (str): The name of the column in the CSV file header.
|
||||||
data_path (str): A dot-separated path to access the value within
|
data_path (str): A dot-separated path to access the value within
|
||||||
the DataBatch object. E.g., "header.header_data.signal_descr.ttag".
|
the DataBatch object. E.g., "main_header.ge_header.mode.master_mode".
|
||||||
|
translate_with_enum (bool): If True, the numeric value will be translated
|
||||||
|
to its string representation using a corresponding Enum.
|
||||||
|
Defaults to False.
|
||||||
"""
|
"""
|
||||||
column_name: str
|
column_name: str
|
||||||
data_path: str
|
data_path: str
|
||||||
|
translate_with_enum: bool = False
|
||||||
|
|
||||||
|
|
||||||
@dataclass(**DC_KWARGS)
|
@dataclass(**DC_KWARGS)
|
||||||
class ExportProfile:
|
class ExportProfile:
|
||||||
"""
|
"""
|
||||||
Represents a full export profile, including its name and the list of fields.
|
Represents a full export profile, including its name and the list of fields.
|
||||||
|
|
||||||
Attributes:
|
|
||||||
name (str): The unique name of the profile (e.g., "Navigation Data").
|
|
||||||
fields (List[ExportField]): An ordered list of fields to export. The order
|
|
||||||
determines the column order in the CSV.
|
|
||||||
"""
|
"""
|
||||||
name: str
|
name: str
|
||||||
fields: List[ExportField] = field(default_factory=list)
|
fields: List[ExportField] = field(default_factory=list)
|
||||||
@ -40,7 +39,11 @@ class ExportProfile:
|
|||||||
return {
|
return {
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"fields": [
|
"fields": [
|
||||||
{"column_name": f.column_name, "data_path": f.data_path}
|
{
|
||||||
|
"column_name": f.column_name,
|
||||||
|
"data_path": f.data_path,
|
||||||
|
"translate_with_enum": f.translate_with_enum,
|
||||||
|
}
|
||||||
for f in self.fields
|
for f in self.fields
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@ -50,5 +53,16 @@ class ExportProfile:
|
|||||||
"""Creates an ExportProfile instance from a dictionary."""
|
"""Creates an ExportProfile instance from a dictionary."""
|
||||||
name = data.get("name", "Unnamed Profile")
|
name = data.get("name", "Unnamed Profile")
|
||||||
fields_data = data.get("fields", [])
|
fields_data = data.get("fields", [])
|
||||||
fields = [ExportField(**field_data) for field_data in fields_data]
|
|
||||||
|
fields = []
|
||||||
|
for field_data in fields_data:
|
||||||
|
# Ensure backward compatibility with old configs that don't have the flag
|
||||||
|
translate_flag = field_data.get("translate_with_enum", False)
|
||||||
|
fields.append(
|
||||||
|
ExportField(
|
||||||
|
column_name=field_data.get("column_name", ""),
|
||||||
|
data_path=field_data.get("data_path", ""),
|
||||||
|
translate_with_enum=translate_flag,
|
||||||
|
)
|
||||||
|
)
|
||||||
return ExportProfile(name=name, fields=fields)
|
return ExportProfile(name=name, fields=fields)
|
||||||
@ -124,10 +124,10 @@ class MainWindow(tk.Frame):
|
|||||||
ttk.Label(status_frame, text="Progress:", anchor="e").grid(row=1, column=0, padx=(10, 5), pady=2, sticky="e")
|
ttk.Label(status_frame, text="Progress:", anchor="e").grid(row=1, column=0, padx=(10, 5), pady=2, sticky="e")
|
||||||
ttk.Label(status_frame, textvariable=self.progress_text_var, anchor="w").grid(row=1, column=1, padx=5, pady=2, sticky="w")
|
ttk.Label(status_frame, textvariable=self.progress_text_var, anchor="w").grid(row=1, column=1, padx=5, pady=2, sticky="w")
|
||||||
|
|
||||||
ttk.Label(status_frame, text="File Batch Counter:", anchor="e").grid(row=2, column=0, padx=(10, 5), pady=2, sticky="e")
|
ttk.Label(status_frame, text="Batch Counter:", anchor="e").grid(row=2, column=0, padx=(10, 5), pady=2, sticky="e")
|
||||||
ttk.Label(status_frame, textvariable=self.file_batch_counter_var, anchor="w").grid(row=2, column=1, padx=5, pady=2, sticky="w")
|
ttk.Label(status_frame, textvariable=self.file_batch_counter_var, anchor="w").grid(row=2, column=1, padx=5, pady=2, sticky="w")
|
||||||
|
|
||||||
ttk.Label(status_frame, text="File TimeTag:", anchor="e").grid(row=3, column=0, padx=(10, 5), pady=(2, 5), sticky="e")
|
ttk.Label(status_frame, text="TimeTag:", anchor="e").grid(row=3, column=0, padx=(10, 5), pady=(2, 5), sticky="e")
|
||||||
ttk.Label(status_frame, textvariable=self.timetag_var, anchor="w").grid(row=3, column=1, padx=5, pady=(2, 5), sticky="w")
|
ttk.Label(status_frame, textvariable=self.timetag_var, anchor="w").grid(row=3, column=1, padx=5, pady=(2, 5), sticky="w")
|
||||||
|
|
||||||
# --- Log Console Frame ---
|
# --- Log Console Frame ---
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import copy
|
|||||||
from typing import List, Type, Dict, Any, Union, Optional
|
from typing import List, Type, Dict, Any, Union, Optional
|
||||||
|
|
||||||
from ..core import data_structures as ds
|
from ..core import data_structures as ds
|
||||||
|
from ..core.data_enums import ENUM_REGISTRY
|
||||||
from ..core.export_profiles import ExportProfile, ExportField
|
from ..core.export_profiles import ExportProfile, ExportField
|
||||||
from ..utils import logger
|
from ..utils import logger
|
||||||
|
|
||||||
@ -40,20 +41,20 @@ class ProfileEditorWindow(tk.Toplevel):
|
|||||||
def _init_window(self):
|
def _init_window(self):
|
||||||
"""Initializes window properties."""
|
"""Initializes window properties."""
|
||||||
self.title("Export Profile Editor")
|
self.title("Export Profile Editor")
|
||||||
self.geometry("900x600")
|
self.geometry("1000x600") # Increased width for new column
|
||||||
self.transient(self.master)
|
self.transient(self.master)
|
||||||
self.grab_set()
|
self.grab_set()
|
||||||
|
|
||||||
def _init_vars(self):
|
def _init_vars(self):
|
||||||
"""Initializes Tkinter variables."""
|
"""Initializes Tkinter variables."""
|
||||||
self.selected_profile_name = tk.StringVar()
|
self.selected_profile_name = tk.StringVar()
|
||||||
self.selected_fields_var = tk.Variable(value=[])
|
|
||||||
|
|
||||||
def _create_widgets(self):
|
def _create_widgets(self):
|
||||||
"""Creates the main layout and widgets for the editor."""
|
"""Creates the main layout and widgets for the editor."""
|
||||||
main_pane = ttk.PanedWindow(self, orient=tk.HORIZONTAL)
|
main_pane = ttk.PanedWindow(self, orient=tk.HORIZONTAL)
|
||||||
main_pane.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
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")
|
profile_mgmt_frame = ttk.LabelFrame(main_pane, text="Profiles")
|
||||||
main_pane.add(profile_mgmt_frame, weight=1)
|
main_pane.add(profile_mgmt_frame, weight=1)
|
||||||
profile_mgmt_frame.columnconfigure(0, weight=1)
|
profile_mgmt_frame.columnconfigure(0, weight=1)
|
||||||
@ -67,10 +68,11 @@ class ProfileEditorWindow(tk.Toplevel):
|
|||||||
|
|
||||||
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, 2), 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(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)
|
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")
|
fields_frame = ttk.LabelFrame(main_pane, text="Available Fields")
|
||||||
main_pane.add(fields_frame, weight=2)
|
main_pane.add(fields_frame, weight=2)
|
||||||
fields_frame.rowconfigure(0, weight=1)
|
fields_frame.rowconfigure(0, weight=1)
|
||||||
@ -81,8 +83,9 @@ class ProfileEditorWindow(tk.Toplevel):
|
|||||||
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')
|
||||||
|
|
||||||
|
# --- Right Frame: Selected Fields and Actions ---
|
||||||
selected_frame_container = ttk.Frame(main_pane)
|
selected_frame_container = ttk.Frame(main_pane)
|
||||||
main_pane.add(selected_frame_container, weight=2)
|
main_pane.add(selected_frame_container, weight=3)
|
||||||
selected_frame_container.rowconfigure(0, weight=1)
|
selected_frame_container.rowconfigure(0, weight=1)
|
||||||
selected_frame_container.columnconfigure(1, weight=1)
|
selected_frame_container.columnconfigure(1, weight=1)
|
||||||
|
|
||||||
@ -97,34 +100,53 @@ class ProfileEditorWindow(tk.Toplevel):
|
|||||||
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_listbox = tk.Listbox(selected_fields_frame, listvariable=self.selected_fields_var, selectmode=tk.SINGLE)
|
|
||||||
self.selected_listbox.grid(row=0, column=0, sticky="nsew", padx=5, pady=5)
|
self.selected_tree = ttk.Treeview(selected_fields_frame, columns=("display_name", "translate"), show="headings", selectmode="browse")
|
||||||
ysb_list = ttk.Scrollbar(selected_fields_frame, orient='vertical', command=self.selected_listbox.yview)
|
self.selected_tree.heading("display_name", text="Field Name")
|
||||||
self.selected_listbox.config(yscrollcommand=ysb_list.set)
|
self.selected_tree.heading("translate", text="Translate")
|
||||||
ysb_list.grid(row=0, column=1, sticky='ns')
|
self.selected_tree.column("display_name", width=200, stretch=True)
|
||||||
|
self.selected_tree.column("translate", width=100, 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)
|
||||||
|
|
||||||
|
# --- Bottom Frame: Save/Cancel ---
|
||||||
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(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)
|
ttk.Button(bottom_frame, text="Cancel", command=self._on_close).pack(side=tk.RIGHT, padx=5)
|
||||||
|
|
||||||
|
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 != "#2": # Column index for "translate"
|
||||||
|
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):
|
def _populate_available_fields_tree(self):
|
||||||
"""Introspects the data structures and populates the Treeview correctly."""
|
|
||||||
self.fields_tree.delete(*self.fields_tree.get_children())
|
self.fields_tree.delete(*self.fields_tree.get_children())
|
||||||
|
|
||||||
# --- Root 1: Batch-level properties ---
|
|
||||||
batch_root = self.fields_tree.insert("", "end", iid="batch_properties", text="Batch Properties")
|
batch_root = self.fields_tree.insert("", "end", iid="batch_properties", text="Batch Properties")
|
||||||
# Add batch_id directly as it's a top-level property
|
|
||||||
self.fields_tree.insert(batch_root, "end", iid="batch_id", text="batch_id", values=("batch_id", "batch_id"))
|
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")
|
||||||
# --- Root 2: Header data properties ---
|
|
||||||
# The user is interested in the contents of the header block.
|
|
||||||
header_root = self.fields_tree.insert("", "end", iid="header_data", text="Header Data (from DspHeaderIn)")
|
|
||||||
# We start the recursion from GeHeader, with a base path pointing to it.
|
|
||||||
self._recursive_populate_tree(ds.GeHeader, header_root, "main_header.ge_header")
|
self._recursive_populate_tree(ds.GeHeader, header_root, "main_header.ge_header")
|
||||||
|
|
||||||
def _recursive_populate_tree(self, class_obj: Type, parent_id: str, base_path: str):
|
def _recursive_populate_tree(self, class_obj: Type, parent_id: str, base_path: str):
|
||||||
"""Recursively explores dataclasses to build the tree, starting from a base path."""
|
|
||||||
if not dataclasses.is_dataclass(class_obj):
|
if not dataclasses.is_dataclass(class_obj):
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -133,7 +155,6 @@ class ProfileEditorWindow(tk.Toplevel):
|
|||||||
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}"
|
||||||
|
|
||||||
# Determine the type to inspect, handling Optional and List
|
|
||||||
type_to_inspect = field.type
|
type_to_inspect = field.type
|
||||||
if hasattr(type_to_inspect, '__origin__') and type_to_inspect.__origin__ in [Union, list]:
|
if hasattr(type_to_inspect, '__origin__') and type_to_inspect.__origin__ in [Union, list]:
|
||||||
args = getattr(type_to_inspect, '__args__', [])
|
args = getattr(type_to_inspect, '__args__', [])
|
||||||
@ -141,15 +162,13 @@ class ProfileEditorWindow(tk.Toplevel):
|
|||||||
if non_none_args:
|
if non_none_args:
|
||||||
type_to_inspect = non_none_args[0]
|
type_to_inspect = non_none_args[0]
|
||||||
|
|
||||||
# If it's another dataclass, create a branch and recurse
|
|
||||||
if dataclasses.is_dataclass(type_to_inspect):
|
if dataclasses.is_dataclass(type_to_inspect):
|
||||||
child_node = self.fields_tree.insert(parent_id, "end", iid=node_id, text=field_name)
|
child_node = self.fields_tree.insert(parent_id, "end", iid=node_id, text=field_name)
|
||||||
self._recursive_populate_tree(type_to_inspect, node_id, current_path)
|
self._recursive_populate_tree(type_to_inspect, node_id, current_path)
|
||||||
# If it's a simple type, create a leaf
|
|
||||||
else:
|
else:
|
||||||
display_text = f"{field_name}"
|
display_text = f"{field_name}"
|
||||||
if field.type in LEAF_TYPES:
|
if base_path + "." + field_name in ENUM_REGISTRY:
|
||||||
display_text = f"{field_name} [{field.type.__name__}]"
|
display_text += " (Enum)"
|
||||||
self.fields_tree.insert(parent_id, "end", iid=node_id, text=display_text, values=(field_name, current_path))
|
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):
|
||||||
@ -157,7 +176,7 @@ class ProfileEditorWindow(tk.Toplevel):
|
|||||||
self.profile_combobox['values'] = profile_names
|
self.profile_combobox['values'] = profile_names
|
||||||
if profile_names:
|
if profile_names:
|
||||||
self.selected_profile_name.set(profile_names[0])
|
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]:
|
||||||
name = self.selected_profile_name.get()
|
name = self.selected_profile_name.get()
|
||||||
@ -167,41 +186,49 @@ class ProfileEditorWindow(tk.Toplevel):
|
|||||||
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)
|
||||||
|
|
||||||
profile = self._get_current_profile()
|
profile = self._get_current_profile()
|
||||||
if not profile:
|
if not profile:
|
||||||
self.selected_fields_var.set([])
|
|
||||||
return
|
return
|
||||||
|
|
||||||
display_list = [f.column_name for f in profile.fields]
|
for index, field in enumerate(profile.fields):
|
||||||
self.selected_fields_var.set(display_list)
|
is_translatable = field.data_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, checkbox_char)
|
||||||
|
)
|
||||||
|
|
||||||
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:
|
if not selected_item_id: return
|
||||||
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 (a leaf node), not a category.", 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", f"The field '{column_name}' 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_listbox.curselection()
|
selection = self.selected_tree.selection()
|
||||||
if not selection: return
|
if not selection: return
|
||||||
|
|
||||||
index_to_remove = 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
|
||||||
|
|
||||||
@ -209,10 +236,10 @@ class ProfileEditorWindow(tk.Toplevel):
|
|||||||
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_listbox.curselection()
|
selection = self.selected_tree.selection()
|
||||||
if not selection: return
|
if not selection: return
|
||||||
|
|
||||||
index = selection[0]
|
index = int(selection[0])
|
||||||
new_index = index + direction
|
new_index = index + direction
|
||||||
|
|
||||||
profile = self._get_current_profile()
|
profile = self._get_current_profile()
|
||||||
@ -222,12 +249,11 @@ class ProfileEditorWindow(tk.Toplevel):
|
|||||||
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_listbox.selection_set(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("New Profile", "Enter a name for the new profile:", parent=self)
|
||||||
if not name or not name.strip():
|
if not name or not name.strip(): return
|
||||||
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)
|
||||||
@ -258,16 +284,8 @@ 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(
|
response = messagebox.askyesnocancel("Unsaved Changes", "You have unsaved changes. Would you like to save them?", parent=self)
|
||||||
"Unsaved Changes",
|
if response is True: self._on_save_and_close()
|
||||||
"You have unsaved changes. Would you like to save them before closing?",
|
elif response is False: self.destroy()
|
||||||
parent=self
|
|
||||||
)
|
|
||||||
if response is True:
|
|
||||||
self._on_save_and_close()
|
|
||||||
elif response is False:
|
|
||||||
self.destroy()
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
else:
|
else:
|
||||||
self.destroy()
|
self.destroy()
|
||||||
Loading…
Reference in New Issue
Block a user