fix: suppress FileSelector callbacks during construction

During _build_config_panel(), set_relative_path() triggered callbacks
that called _get_selected_files() before all selectors were created,
causing AttributeError.

Fix: FileSelector now has _suppress_callback flag. Set True during
construction, then False after all selectors are created. Finally
call _auto_save_config() once at the end of the panel build.
This commit is contained in:
2026-06-12 10:29:58 +08:00
parent e2e1fdd2a0
commit 5f9049a321
2 changed files with 28 additions and 7 deletions
+22 -2
View File
@@ -218,15 +218,20 @@ class MainWindow(ctk.CTk):
object's perspective); the FileSelector stores both the object's perspective); the FileSelector stores both the
absolute path (for internal use) and the relative path absolute path (for internal use) and the relative path
(for saving to YAML). (for saving to YAML).
All attribute accesses are defensive — widgets may not exist
yet during construction (e.g. _port_var created in _build_serial).
""" """
if not self._config: if not self._config:
return return
# Sync IP address # Sync IP address (may not exist yet during _build_config_panel)
if self._ip_string_var: if self._ip_string_var:
self._config.zynq_ip = self._ip_string_var.get() self._config.zynq_ip = self._ip_string_var.get()
# Sync Xilinx Kit path # Sync Xilinx Kit path
if hasattr(self, '_xilinx_path_var'):
self._config.xilinx_path = self._xilinx_path_var.get() self._config.xilinx_path = self._xilinx_path_var.get()
# Sync serial port # Sync serial port (may not exist yet during _build_config_panel)
if hasattr(self, '_port_var'):
self._config.serial_port = self._port_var.get() self._config.serial_port = self._port_var.get()
# Sync file paths from FileSelectors # Sync file paths from FileSelectors
files = self._get_selected_files() files = self._get_selected_files()
@@ -245,6 +250,8 @@ class MainWindow(ctk.CTk):
if abs_path: if abs_path:
self._config._config_path # ensure _config_path is set self._config._config_path # ensure _config_path is set
setattr(self._config, attr, self._config._relative_path(abs_path)) setattr(self._config, attr, self._config._relative_path(abs_path))
# Sync erase checkbox (may not exist yet)
if hasattr(self, '_erase_cb_var'):
self._config.erase_all = self._erase_cb_var.get() self._config.erase_all = self._erase_cb_var.get()
def _auto_save_config(self) -> None: def _auto_save_config(self) -> None:
@@ -586,6 +593,7 @@ class MainWindow(ctk.CTk):
file_types=[("ELF files", "*.elf"), ("All files", "*")], file_types=[("ELF files", "*.elf"), ("All files", "*")],
callback=self._on_file_selected, callback=self._on_file_selected,
) )
self._fsbl_selector._suppress_callback = True
if self._config.fsbl_elf_path: if self._config.fsbl_elf_path:
resolved = self._config.resolve_path(self._config.fsbl_elf_path) resolved = self._config.resolve_path(self._config.fsbl_elf_path)
self._fsbl_selector.set_relative_path(self._config.fsbl_elf_path) self._fsbl_selector.set_relative_path(self._config.fsbl_elf_path)
@@ -599,6 +607,7 @@ class MainWindow(ctk.CTk):
file_types=[("BIT files", "*.bit"), ("All files", "*")], file_types=[("BIT files", "*.bit"), ("All files", "*")],
callback=self._on_file_selected, callback=self._on_file_selected,
) )
self._bit_selector._suppress_callback = True
if self._config.bootloader_bit_path: if self._config.bootloader_bit_path:
resolved = self._config.resolve_path(self._config.bootloader_bit_path) resolved = self._config.resolve_path(self._config.bootloader_bit_path)
self._bit_selector.set_relative_path(self._config.bootloader_bit_path) self._bit_selector.set_relative_path(self._config.bootloader_bit_path)
@@ -612,6 +621,7 @@ class MainWindow(ctk.CTk):
file_types=[("ELF files", "*.elf"), ("All files", "*")], file_types=[("ELF files", "*.elf"), ("All files", "*")],
callback=self._on_file_selected, callback=self._on_file_selected,
) )
self._elf_selector._suppress_callback = True
if self._config.bootloader_elf_path: if self._config.bootloader_elf_path:
resolved = self._config.resolve_path(self._config.bootloader_elf_path) resolved = self._config.resolve_path(self._config.bootloader_elf_path)
self._elf_selector.set_relative_path(self._config.bootloader_elf_path) self._elf_selector.set_relative_path(self._config.bootloader_elf_path)
@@ -625,6 +635,7 @@ class MainWindow(ctk.CTk):
file_types=[("BIN files", "*.bin"), ("All files", "*")], file_types=[("BIN files", "*.bin"), ("All files", "*")],
callback=self._on_file_selected, callback=self._on_file_selected,
) )
self._bootloader_bin_selector._suppress_callback = True
if self._config.bootloader_bin_path: if self._config.bootloader_bin_path:
resolved = self._config.resolve_path(self._config.bootloader_bin_path) resolved = self._config.resolve_path(self._config.bootloader_bin_path)
self._bootloader_bin_selector.set_relative_path(self._config.bootloader_bin_path) self._bootloader_bin_selector.set_relative_path(self._config.bootloader_bin_path)
@@ -660,12 +671,21 @@ class MainWindow(ctk.CTk):
file_types=[("BIN files", "*.bin"), ("All files", "*")], file_types=[("BIN files", "*.bin"), ("All files", "*")],
callback=self._on_file_selected, callback=self._on_file_selected,
) )
self._firmware_bin_selector._suppress_callback = True
if self._config.firmware_bin_path: if self._config.firmware_bin_path:
resolved = self._config.resolve_path(self._config.firmware_bin_path) resolved = self._config.resolve_path(self._config.firmware_bin_path)
self._firmware_bin_selector.set_relative_path(self._config.firmware_bin_path) self._firmware_bin_selector.set_relative_path(self._config.firmware_bin_path)
self._firmware_bin_selector.set_path(str(resolved)) self._firmware_bin_selector.set_path(str(resolved))
self._firmware_bin_selector.grid(row=8, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL) self._firmware_bin_selector.grid(row=8, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
# Enable callbacks now that all selectors are created
for sel in (self._fsbl_selector, self._bit_selector, self._elf_selector,
self._bootloader_bin_selector, self._firmware_bin_selector):
sel._suppress_callback = False
# Now sync and save config (all selectors exist)
self._auto_save_config()
# Save button # Save button
save_btn = ctk.CTkButton( save_btn = ctk.CTkButton(
panel, panel,
+3 -2
View File
@@ -528,6 +528,7 @@ class FileSelector(ctk.CTkFrame):
self._full_path: str = "" # absolute path (internal) self._full_path: str = "" # absolute path (internal)
self._relative_path: str = "" # relative path (synced to Config) self._relative_path: str = "" # relative path (synced to Config)
self._display_text = ctk.StringVar(value="") self._display_text = ctk.StringVar(value="")
self._suppress_callback: bool = False # True during construction
self._create_widgets(label_text) self._create_widgets(label_text)
@@ -590,7 +591,7 @@ class FileSelector(ctk.CTkFrame):
""" """
self._full_path = path self._full_path = path
self._display_text.set(os.path.basename(path) if path else "") self._display_text.set(os.path.basename(path) if path else "")
if self._callback: if self._callback and not self._suppress_callback:
self._callback(path) self._callback(path)
def set_relative_path(self, path: str) -> None: def set_relative_path(self, path: str) -> None:
@@ -606,7 +607,7 @@ class FileSelector(ctk.CTkFrame):
if path: if path:
self._full_path = path # caller should resolve before calling self._full_path = path # caller should resolve before calling
self._display_text.set(os.path.basename(path) if path else "") self._display_text.set(os.path.basename(path) if path else "")
if self._callback: if self._callback and not self._suppress_callback:
self._callback(path) self._callback(path)
def get_path(self) -> str: def get_path(self) -> str: