diff --git a/radar_data_reader/__main__.py b/radar_data_reader/__main__.py index d5a7f4c..c73635c 100644 --- a/radar_data_reader/__main__.py +++ b/radar_data_reader/__main__.py @@ -7,6 +7,7 @@ import logging from pathlib import Path 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.config_manager import ConfigManager from radar_data_reader.core.app_controller import AppController @@ -41,6 +42,8 @@ def main(): controller = AppController(config_manager) root = tk.Tk() + # Hide the window initially to prevent flickering while it's being built + root.withdraw() logger.setup_basic_logging(root, LOGGING_CONFIG) @@ -50,18 +53,18 @@ def main(): controller.bind_view(view) + # Center the main window on the screen before showing it + center_window(root) + view.mainloop() except Exception as e: log_or_print = ( logger.get_logger(__name__) if logger._logging_system_active else print ) - # --- THIS IS THE CORRECTED LINE --- if callable(log_or_print): - # This case is for the fallback 'print' function log_or_print(f"A critical error occurred: {e}") else: - # This case is for the logger object log_or_print.critical(f"A critical error occurred: {e}", exc_info=True) if root: diff --git a/radar_data_reader/core/app_controller.py b/radar_data_reader/core/app_controller.py index a399cb2..59f66c7 100644 --- a/radar_data_reader/core/app_controller.py +++ b/radar_data_reader/core/app_controller.py @@ -41,14 +41,14 @@ def _get_value_from_path(batch: DataBatch, field: ExportField) -> Any: return batch.batch_id # 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 for part in parts: if current_obj is None: return "N/A" - if part.endswith(']'): + if part.endswith("]"): # This is an index access index_str = part[:-1] if not index_str.isdigit(): @@ -57,22 +57,24 @@ def _get_value_from_path(batch: DataBatch, field: ExportField) -> Any: try: current_obj = current_obj[int(index_str)] 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" else: # This is an attribute access current_obj = getattr(current_obj, part, None) - + value = current_obj if current_obj is not None else "N/A" - + # Handle translation for enums if the final value is an integer if field.translate_with_enum and isinstance(value, int): # 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) if enum_class: return get_enum_name(enum_class, value) - + return value except Exception as e: log.warning(f"Could not resolve path '{field.data_path}': {e}") diff --git a/radar_data_reader/core/data_structures.py b/radar_data_reader/core/data_structures.py index 842ee6a..d3395b4 100644 --- a/radar_data_reader/core/data_structures.py +++ b/radar_data_reader/core/data_structures.py @@ -499,10 +499,11 @@ class DelayWidth(CtypesStructureBase): class TimerRawIf(CtypesStructureBase): - """ + """ Mirrors the C++ `timer_raw_if_t` struct (Reduced initial version). This maps only the first, most stable part of the structure. """ + _fields_ = [ ("tcr", ctypes.c_uint32), ("tpr", ctypes.c_uint32), @@ -512,19 +513,21 @@ class TimerRawIf(CtypesStructureBase): ("diff_prt_num", ctypes.c_uint16), ("spares__", ctypes.c_uint32 * 3), ("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 - ("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 - ("exp_pulse1_delay", UniquePrtFifo * 2), # da 0x100 a 0x180 - ("exp_pulse2_delay", UniquePrtFifo * 2), # da 0x180 a 0x200 - + ("exp_pulse1_delay", UniquePrtFifo * 2), # da 0x100 a 0x180 + ("exp_pulse2_delay", UniquePrtFifo * 2), # da 0x180 a 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 ] diff --git a/radar_data_reader/gui/gui_utils.py b/radar_data_reader/gui/gui_utils.py index e69de29..5319843 100644 --- a/radar_data_reader/gui/gui_utils.py +++ b/radar_data_reader/gui/gui_utils.py @@ -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 diff --git a/radar_data_reader/gui/profile_editor_window.py b/radar_data_reader/gui/profile_editor_window.py index df4d180..0ce7641 100644 --- a/radar_data_reader/gui/profile_editor_window.py +++ b/radar_data_reader/gui/profile_editor_window.py @@ -6,12 +6,13 @@ Includes a custom dialog for editing complex data paths. """ import tkinter as tk -from tkinter import ttk, simpledialog, messagebox +from tkinter import ttk, messagebox import ctypes import copy import re from typing import List, Type, Dict, Any, Optional +from .gui_utils import center_window from ..core import data_structures as ds from ..core.data_enums import ENUM_REGISTRY from ..core.export_profiles import ExportProfile, ExportField @@ -22,45 +23,55 @@ log = logger.get_logger(__name__) class EditPathDialog(tk.Toplevel): """A custom dialog window to edit a data path string.""" + def __init__(self, parent, initial_value=""): super().__init__(parent) self.transient(parent) self.grab_set() self.title("Edit Data Path") - self.geometry("800x150") # Wide dialog for long paths + + self.withdraw() # Hide until ready self.result = None - + main_frame = ttk.Frame(self, padding="10") main_frame.pack(fill=tk.BOTH, expand=True) main_frame.columnconfigure(0, weight=1) - # Instructions Label instructions = ( "Enter the full data path. Use '.' for attributes and '[index]' for arrays.\n" "Example 1: main_header.ge_header.mode.master_mode\n" "Example 2: timer_data.blob.payload.aesa_delay[0].fifo[3]" ) - ttk.Label(main_frame, text=instructions, justify=tk.LEFT).grid(row=0, column=0, sticky="w", pady=(0, 10)) + ttk.Label(main_frame, text=instructions, justify=tk.LEFT).grid( + row=0, column=0, sticky="w", pady=(0, 10) + ) - # Entry Widget self.path_var = tk.StringVar(value=initial_value) - self.path_entry = ttk.Entry(main_frame, textvariable=self.path_var, font=("Courier", 10)) + self.path_entry = 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.focus_set() self.path_entry.selection_range(0, tk.END) - # Buttons Frame button_frame = ttk.Frame(main_frame) button_frame.grid(row=2, column=0, sticky="e", pady=(10, 0)) - - ttk.Button(button_frame, text="OK", command=self._on_ok, default=tk.ACTIVE).pack(side=tk.LEFT, padx=5) - ttk.Button(button_frame, text="Cancel", command=self._on_cancel).pack(side=tk.LEFT) + + ttk.Button( + button_frame, text="OK", command=self._on_ok, default=tk.ACTIVE + ).pack(side=tk.LEFT, padx=5) + ttk.Button(button_frame, text="Cancel", command=self._on_cancel).pack( + side=tk.LEFT + ) self.protocol("WM_DELETE_WINDOW", self._on_cancel) self.bind("", self._on_ok) self.bind("", self._on_cancel) + # Center the dialog relative to its parent + center_window(self, parent) + def _on_ok(self, event=None): self.result = self.path_var.get().strip() self.destroy() @@ -86,19 +97,24 @@ class ProfileEditorWindow(tk.Toplevel): self.profiles = copy.deepcopy(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_vars() self._create_widgets() self._populate_available_fields_tree() 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) def _init_window(self): self.title("Export Profile Editor") self.geometry("1200x700") self.transient(self.master) - self.grab_set() def _init_vars(self): self.selected_profile_name = tk.StringVar() @@ -114,15 +130,21 @@ class ProfileEditorWindow(tk.Toplevel): cb_frame = ttk.Frame(profile_mgmt_frame) cb_frame.grid(row=0, column=0, sticky="ew", padx=5, pady=5) cb_frame.columnconfigure(0, weight=1) - self.profile_combobox = ttk.Combobox(cb_frame, textvariable=self.selected_profile_name, state="readonly") + self.profile_combobox = ttk.Combobox( + cb_frame, textvariable=self.selected_profile_name, state="readonly" + ) self.profile_combobox.grid(row=0, column=0, sticky="ew") self.profile_combobox.bind("<>", self._on_profile_selected) btn_frame = ttk.Frame(profile_mgmt_frame) btn_frame.grid(row=1, column=0, sticky="ew", padx=5) btn_frame.columnconfigure((0, 1), weight=1) - ttk.Button(btn_frame, text="New", command=self._on_new_profile).grid(row=0, column=0, sticky="ew", padx=2) - ttk.Button(btn_frame, text="Delete", command=self._on_delete_profile).grid(row=0, column=1, sticky="ew", padx=2) + ttk.Button(btn_frame, text="New", command=self._on_new_profile).grid( + row=0, column=0, sticky="ew", padx=2 + ) + ttk.Button(btn_frame, text="Delete", command=self._on_delete_profile).grid( + row=0, column=1, sticky="ew", padx=2 + ) fields_frame = ttk.LabelFrame(main_pane, text="Available Fields") main_pane.add(fields_frame, weight=3) @@ -130,7 +152,9 @@ class ProfileEditorWindow(tk.Toplevel): fields_frame.columnconfigure(0, weight=1) self.fields_tree = ttk.Treeview(fields_frame, selectmode="browse") self.fields_tree.grid(row=0, column=0, sticky="nsew", padx=5, pady=5) - ysb = ttk.Scrollbar(fields_frame, orient="vertical", command=self.fields_tree.yview) + ysb = ttk.Scrollbar( + fields_frame, orient="vertical", command=self.fields_tree.yview + ) self.fields_tree.configure(yscrollcommand=ysb.set) ysb.grid(row=0, column=1, sticky="ns") @@ -143,22 +167,37 @@ class ProfileEditorWindow(tk.Toplevel): action_btn_frame.grid(row=0, column=0, sticky="ns", padx=5, pady=5) ttk.Button(action_btn_frame, text=">>", command=self._add_field).grid(pady=5) ttk.Button(action_btn_frame, text="<<", command=self._remove_field).grid(pady=5) - ttk.Button(action_btn_frame, text="Up", command=lambda: self._move_field(-1)).grid(pady=10) - ttk.Button(action_btn_frame, text="Down", command=lambda: self._move_field(1)).grid(pady=5) - ttk.Button(action_btn_frame, text="Edit Path", command=self._edit_selected_field_path).grid(pady=10) - ttk.Button(action_btn_frame, text="Reset", command=self._clear_selected_fields).grid(pady=5) + ttk.Button( + action_btn_frame, text="Up", command=lambda: self._move_field(-1) + ).grid(pady=10) + ttk.Button( + action_btn_frame, text="Down", command=lambda: self._move_field(1) + ).grid(pady=5) + ttk.Button( + action_btn_frame, text="Edit Path", command=self._edit_selected_field_path + ).grid(pady=10) + ttk.Button( + action_btn_frame, text="Reset", command=self._clear_selected_fields + ).grid(pady=5) - selected_fields_frame = ttk.LabelFrame(selected_frame_container, text="Selected Fields for Profile") + selected_fields_frame = ttk.LabelFrame( + selected_frame_container, text="Selected Fields for Profile" + ) selected_fields_frame.grid(row=0, column=1, sticky="nsew") selected_fields_frame.rowconfigure(0, weight=1) selected_fields_frame.columnconfigure(0, weight=1) - self.selected_tree = ttk.Treeview(selected_fields_frame, columns=("display_name", "data_path", "translate"), show="headings", selectmode="browse") + self.selected_tree = ttk.Treeview( + selected_fields_frame, + columns=("display_name", "data_path", "translate"), + show="headings", + selectmode="browse", + ) self.selected_tree.heading("display_name", text="Field Name") self.selected_tree.heading("data_path", text="Source Path") self.selected_tree.heading("translate", text="Translate") self.selected_tree.column("display_name", width=150, stretch=True) - self.selected_tree.column("data_path", width=300, stretch=True) # Widened column + self.selected_tree.column("data_path", width=300, stretch=True) self.selected_tree.column("translate", width=80, anchor="center", stretch=False) self.selected_tree.grid(row=0, column=0, sticky="nsew") self.selected_tree.bind("", self._on_selected_tree_click) @@ -166,103 +205,163 @@ class ProfileEditorWindow(tk.Toplevel): bottom_frame = ttk.Frame(self) bottom_frame.pack(fill=tk.X, padx=10, pady=(0, 10)) - ttk.Button(bottom_frame, text="Save & Close", command=self._on_save_and_close).pack(side=tk.RIGHT) - ttk.Button(bottom_frame, text="Cancel", command=self._on_close).pack(side=tk.RIGHT, padx=5) + ttk.Button( + bottom_frame, text="Save & Close", command=self._on_save_and_close + ).pack(side=tk.RIGHT) + ttk.Button(bottom_frame, text="Cancel", command=self._on_close).pack( + side=tk.RIGHT, padx=5 + ) def _on_selected_tree_double_click(self, event): - """Handle double-click on the selected fields tree to edit path.""" - self._edit_selected_field_path() + item = self.selected_tree.identify_row(event.y) + if item: + self._edit_selected_field_path() def _edit_selected_field_path(self): - """Allows manual editing of the data_path for the selected field.""" selection = self.selected_tree.selection() if not selection: - messagebox.showinfo("No Selection", "Please select a field to edit.", parent=self) + messagebox.showinfo( + "No Selection", "Please select a field to edit.", parent=self + ) return - index = int(selection[0]) profile = self._get_current_profile() - if not profile: return - + if not profile: + return field = profile.fields[index] - dialog = EditPathDialog(self, initial_value=field.data_path) new_path = dialog.wait_for_input() - if new_path is not None and new_path != field.data_path: - # Basic validation for the path format - if not re.match(r'^[\w\.\[\]]+$', new_path): - messagebox.showerror("Invalid Path", "The path contains invalid characters.", parent=self) + if not re.match(r"^[\w\.\[\]]+$", new_path): + messagebox.showerror( + "Invalid Path", "The path contains invalid characters.", parent=self + ) return field.data_path = new_path self._load_profile_into_ui() def _on_selected_tree_click(self, event): region = self.selected_tree.identify_region(event.x, event.y) - if region != "cell": return + if region != "cell": + return 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) - if not item_id: return + if not item_id: + return profile = self._get_current_profile() - if not profile: return + if not profile: + return field_index = int(item_id) field = profile.fields[field_index] - if field.data_path in ENUM_REGISTRY or re.sub(r'\[\d+\]', '', field.data_path) in ENUM_REGISTRY: + base_path = re.sub(r"\[\d+\]", "", field.data_path) + if base_path in ENUM_REGISTRY: field.translate_with_enum = not field.translate_with_enum self._load_profile_into_ui() def _load_profile_into_ui(self): - for i in self.selected_tree.get_children(): self.selected_tree.delete(i) + for i in self.selected_tree.get_children(): + self.selected_tree.delete(i) profile = self._get_current_profile() - if not profile: return + if not profile: + return 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 checkbox_char = "☐" - if is_translatable: checkbox_char = "☑" if field.translate_with_enum else "☐" - self.selected_tree.insert("", "end", iid=str(index), values=(field.column_name, field.data_path, checkbox_char)) + if is_translatable: + checkbox_char = "☑" if field.translate_with_enum else "☐" + self.selected_tree.insert( + "", + "end", + iid=str(index), + values=(field.column_name, field.data_path, checkbox_char), + ) def _clear_selected_fields(self): - """Clears all fields from the currently selected profile.""" profile = self._get_current_profile() - if not profile or not profile.fields: return - if messagebox.askyesno("Confirm Clear", f"Are you sure you want to remove all fields from the profile '{profile.name}'?", parent=self): + if not profile or not profile.fields: + return + if messagebox.askyesno( + "Confirm Clear", + f"Are you sure you want to remove all fields from the profile '{profile.name}'?", + parent=self, + ): profile.fields.clear() self._load_profile_into_ui() - - # --- Other methods remain unchanged --- def _populate_available_fields_tree(self): self.fields_tree.delete(*self.fields_tree.get_children()) - batch_root = self.fields_tree.insert("", "end", iid="batch_properties", text="Batch Properties") - self.fields_tree.insert(batch_root, "end", iid="batch_id", text="batch_id", values=("batch_id", "batch_id")) - header_root = self.fields_tree.insert("", "end", iid="header_data", text="Header Data (from DSPHDRIN)") - self._recursive_populate_tree_ctypes(ds.GeHeader, header_root, "main_header.ge_header") - cdpsts_root = self.fields_tree.insert("", "end", iid="cdpsts_data", text="CDP/STS Block Data") - self._recursive_populate_tree_ctypes(ds.CdpDataLayout, cdpsts_root, "cdp_sts_results.payload.data") - timer_root = self.fields_tree.insert("", "end", iid="timer_data", text="Timer Block Data") - self._recursive_populate_tree_ctypes(ds.GrifoTimerBlob, timer_root, "timer_data.blob") + batch_root = self.fields_tree.insert( + "", "end", iid="batch_properties", text="Batch Properties" + ) + self.fields_tree.insert( + batch_root, + "end", + iid="batch_id", + text="batch_id", + values=("batch_id", "batch_id"), + ) + header_root = self.fields_tree.insert( + "", "end", iid="header_data", text="Header Data (from DSPHDRIN)" + ) + self._recursive_populate_tree_ctypes( + ds.GeHeader, header_root, "main_header.ge_header" + ) + cdpsts_root = self.fields_tree.insert( + "", "end", iid="cdpsts_data", text="CDP/STS Block Data" + ) + self._recursive_populate_tree_ctypes( + ds.CdpDataLayout, cdpsts_root, "cdp_sts_results.payload.data" + ) + timer_root = self.fields_tree.insert( + "", "end", iid="timer_data", text="Timer Block Data" + ) + self._recursive_populate_tree_ctypes( + ds.GrifoTimerBlob, timer_root, "timer_data.blob" + ) - def _recursive_populate_tree_ctypes(self, class_obj: Type[ctypes.Structure], parent_id: str, base_path: str): - if not hasattr(class_obj, '_fields_'): return + def _recursive_populate_tree_ctypes( + self, class_obj: Type[ctypes.Structure], parent_id: str, base_path: str + ): + if not hasattr(class_obj, "_fields_"): + return for field_name, field_type in class_obj._fields_: current_path = f"{base_path}.{field_name}" node_id = f"{parent_id}_{field_name}" - if hasattr(field_type, '_fields_'): - child_node = self.fields_tree.insert(parent_id, "end", iid=node_id, text=field_name) - self._recursive_populate_tree_ctypes(field_type, child_node, current_path) - elif hasattr(field_type, '_length_'): - self.fields_tree.insert(parent_id, "end", iid=node_id, text=f"{field_name} [Array]", values=(field_name, current_path)) + if hasattr(field_type, "_fields_"): + child_node = self.fields_tree.insert( + parent_id, "end", iid=node_id, text=field_name + ) + self._recursive_populate_tree_ctypes( + field_type, child_node, current_path + ) + elif hasattr(field_type, "_length_"): + self.fields_tree.insert( + parent_id, + "end", + iid=node_id, + text=f"{field_name} [Array]", + values=(field_name, current_path), + ) else: display_text = f"{field_name}" - if current_path in ENUM_REGISTRY: display_text += " (Enum)" - self.fields_tree.insert(parent_id, "end", iid=node_id, text=display_text, values=(field_name, current_path)) + if current_path in ENUM_REGISTRY: + display_text += " (Enum)" + self.fields_tree.insert( + parent_id, + "end", + iid=node_id, + text=display_text, + values=(field_name, current_path), + ) def _load_profiles_to_combobox(self): profile_names = [p.name for p in self.profiles] self.profile_combobox["values"] = profile_names - if profile_names: self.selected_profile_name.set(profile_names[0]) + if profile_names: + self.selected_profile_name.set(profile_names[0]) self._load_profile_into_ui() def _get_current_profile(self) -> Optional[ExportProfile]: @@ -274,46 +373,63 @@ class ProfileEditorWindow(tk.Toplevel): def _add_field(self): 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") 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 column_name, data_path = item_values 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): - 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 profile.fields.append(ExportField(column_name=column_name, data_path=data_path)) self._load_profile_into_ui() def _remove_field(self): selection = self.selected_tree.selection() - if not selection: return + if not selection: + return index_to_remove = int(selection[0]) profile = self._get_current_profile() - if not profile: return + if not profile: + return del profile.fields[index_to_remove] self._load_profile_into_ui() def _move_field(self, direction: int): selection = self.selected_tree.selection() - if not selection: return + if not selection: + return index = int(selection[0]) new_index = index + direction profile = self._get_current_profile() - if not profile or not (0 <= new_index < len(profile.fields)): return + if not profile or not (0 <= new_index < len(profile.fields)): + return fields = profile.fields fields.insert(new_index, fields.pop(index)) self._load_profile_into_ui() self.selected_tree.selection_set(str(new_index)) def _on_new_profile(self): - name = simpledialog.askstring("New Profile", "Enter a name for the new profile:", parent=self) - if not name or not name.strip(): return + name = simpledialog.askstring( + "New Profile", "Enter a name for the new profile:", parent=self + ) + if not name or not name.strip(): + return if any(p.name == name for p in self.profiles): - messagebox.showerror("Error", f"A profile with the name '{name}' already exists.", parent=self) + messagebox.showerror( + "Error", + f"A profile with the name '{name}' already exists.", + parent=self, + ) return new_profile = ExportProfile(name=name.strip()) self.profiles.append(new_profile) @@ -323,8 +439,13 @@ class ProfileEditorWindow(tk.Toplevel): def _on_delete_profile(self): profile = self._get_current_profile() - if not profile: return - if messagebox.askyesno("Confirm Delete", f"Are you sure you want to delete the profile '{profile.name}'?", parent=self): + if not profile: + return + if messagebox.askyesno( + "Confirm Delete", + f"Are you sure you want to delete the profile '{profile.name}'?", + parent=self, + ): self.profiles.remove(profile) self._load_profiles_to_combobox() @@ -339,8 +460,14 @@ class ProfileEditorWindow(tk.Toplevel): def _on_close(self): if self._check_unsaved_changes(): - response = messagebox.askyesnocancel("Unsaved Changes", "You have unsaved changes. Would you like to save them?", parent=self) - if response is True: self._on_save_and_close() - elif response is False: self.destroy() + response = messagebox.askyesnocancel( + "Unsaved Changes", + "You have unsaved changes. Would you like to save them?", + parent=self, + ) + if response is True: + self._on_save_and_close() + elif response is False: + self.destroy() else: - self.destroy() \ No newline at end of file + self.destroy() diff --git a/radar_data_reader/gui/rec_config_window.py b/radar_data_reader/gui/rec_config_window.py index 7056b6b..e7cf2e9 100644 --- a/radar_data_reader/gui/rec_config_window.py +++ b/radar_data_reader/gui/rec_config_window.py @@ -9,6 +9,8 @@ from tkinter import ttk, filedialog from typing import Dict, Any import os +from .gui_utils import center_window + class RecConfigWindow(tk.Toplevel): """A Toplevel window for managing g_reconvert.exe parameters.""" @@ -18,18 +20,22 @@ class RecConfigWindow(tk.Toplevel): self.controller = controller self.config_data = current_config.copy() + self.withdraw() # Hide window until it's ready to be shown self._init_window() self._init_vars() self._populate_vars_from_config() self._create_widgets() + # Center the window relative to its parent + center_window(self, self.master) + self.protocol("WM_DELETE_WINDOW", self._on_cancel) def _init_window(self): self.title("g_reconverter Advanced Configuration") - self.geometry("800x650") # Increased height slightly + self.geometry("800x650") self.transient(self.master) - self.grab_set() + # The centering is now handled by the utility function def _init_vars(self): """Initializes all Tkinter variables for the parameters."""