add report info into log, change gui

This commit is contained in:
VALLONGOL 2025-06-20 12:23:22 +02:00
parent f9383140fb
commit 9161e1f6b5
6 changed files with 202 additions and 80 deletions

View File

@ -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
} }
] ]
} }

View File

@ -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)

View 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,
}

View File

@ -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)

View File

@ -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 ---

View File

@ -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)
ysb_list = ttk.Scrollbar(selected_fields_frame, orient='vertical', command=self.selected_listbox.yview)
self.selected_listbox.config(yscrollcommand=ysb_list.set)
ysb_list.grid(row=0, column=1, sticky='ns')
self.selected_tree = ttk.Treeview(selected_fields_frame, columns=("display_name", "translate"), show="headings", selectmode="browse")
self.selected_tree.heading("display_name", text="Field Name")
self.selected_tree.heading("translate", text="Translate")
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()