fix: correct indentation errors in widgets.py (TftpSubStepFrame, UartMonitorWindow)
This commit is contained in:
+235
-31
@@ -173,14 +173,20 @@ class MainWindow(ctk.CTk):
|
||||
self._config._config_path = user_config_path
|
||||
|
||||
def _reload_config(self) -> None:
|
||||
"""Re-read config.yaml and update UI selectors with latest paths."""
|
||||
"""Re-read config.yaml and update UI selectors with latest paths.
|
||||
|
||||
Reads the file on disk, applies values to the internal Config
|
||||
object, then updates every UI selector so that changes made
|
||||
outside the GUI (or between runs) take effect immediately.
|
||||
"""
|
||||
user_path = self._get_user_config_path()
|
||||
if not user_path.exists():
|
||||
return
|
||||
try:
|
||||
fresh = Config.from_file(user_path)
|
||||
self._config = fresh
|
||||
# Update UI selectors
|
||||
# Update UI selectors with resolved (absolute) paths — selectors
|
||||
# display only the filename internally.
|
||||
for attr, selector in [
|
||||
("bootloader_bit_path", self._bit_selector),
|
||||
("bootloader_elf_path", self._elf_selector),
|
||||
@@ -190,7 +196,9 @@ class MainWindow(ctk.CTk):
|
||||
]:
|
||||
val = getattr(fresh, attr, "")
|
||||
if val:
|
||||
selector.set_path(str(fresh.resolve_path(val)))
|
||||
resolved = fresh.resolve_path(val)
|
||||
selector.set_relative_path(val)
|
||||
selector.set_path(str(resolved))
|
||||
# Update IP
|
||||
if self._ip_string_var:
|
||||
self._ip_string_var.set(fresh.zynq_ip)
|
||||
@@ -205,24 +213,63 @@ class MainWindow(ctk.CTk):
|
||||
|
||||
This must be called before _save_config() to ensure
|
||||
the Config object has the latest UI values.
|
||||
|
||||
File paths are synced as relative paths (from the Config
|
||||
object's perspective); the FileSelector stores both the
|
||||
absolute path (for internal use) and the relative path
|
||||
(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:
|
||||
return
|
||||
# Sync IP address
|
||||
# Sync IP address (may not exist yet during _build_config_panel)
|
||||
if self._ip_string_var:
|
||||
self._config.zynq_ip = self._ip_string_var.get()
|
||||
# Sync Xilinx Kit path
|
||||
self._config.xilinx_path = self._xilinx_path_var.get()
|
||||
# Sync serial port
|
||||
self._config.serial_port = self._port_var.get()
|
||||
if hasattr(self, '_xilinx_path_var'):
|
||||
self._config.xilinx_path = self._xilinx_path_var.get()
|
||||
# Sync serial port (may not exist yet during _build_config_panel)
|
||||
if hasattr(self, '_port_var'):
|
||||
self._config.serial_port = self._port_var.get()
|
||||
# Sync file paths from FileSelectors
|
||||
files = self._get_selected_files()
|
||||
self._config.bootloader_bit_path = files["bootloader_bit_path"]
|
||||
self._config.bootloader_elf_path = files["bootloader_elf_path"]
|
||||
self._config.bootloader_bin_path = files["bootloader_bin_path"]
|
||||
self._config.fsbl_elf_path = files["fsbl_elf_path"]
|
||||
self._config.erase_all = self._erase_cb_var.get()
|
||||
self._config.firmware_bin_path = files["firmware_bin_path"]
|
||||
for attr, selector in [
|
||||
("bootloader_bit_path", self._bit_selector),
|
||||
("bootloader_elf_path", self._elf_selector),
|
||||
("bootloader_bin_path", self._bootloader_bin_selector),
|
||||
("fsbl_elf_path", self._fsbl_selector),
|
||||
("firmware_bin_path", self._firmware_bin_selector),
|
||||
]:
|
||||
rel = selector.get_relative_path()
|
||||
if rel:
|
||||
setattr(self._config, attr, rel)
|
||||
else:
|
||||
abs_path = selector.get_path()
|
||||
if abs_path:
|
||||
self._config._config_path # ensure _config_path is set
|
||||
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()
|
||||
|
||||
def _auto_save_config(self) -> None:
|
||||
"""Auto-save config to disk after UI changes.
|
||||
|
||||
Ensures that changes made in the UI (file paths, IP, etc.)
|
||||
are persisted immediately so that _reload_config() picks them
|
||||
up on the next step execution.
|
||||
"""
|
||||
if not self._config:
|
||||
return
|
||||
self._sync_config_from_ui()
|
||||
user_config_path = self._get_user_config_path()
|
||||
self._config._config_path = user_config_path
|
||||
try:
|
||||
self._config.save()
|
||||
except Exception:
|
||||
pass # Silently fail — user can manually save
|
||||
|
||||
def _save_config(self) -> None:
|
||||
"""Save current configuration to user config file (config.yaml).
|
||||
@@ -256,26 +303,44 @@ class MainWindow(ctk.CTk):
|
||||
path = filedialog.askdirectory(title="Select Xilinx Root Directory")
|
||||
if path:
|
||||
self._xilinx_path_var.set(path)
|
||||
if self._config:
|
||||
self._config.xilinx_path = path
|
||||
# Re-check tools with new path
|
||||
self._check_vitis()
|
||||
self._on_xilinx_path_changed()
|
||||
# Re-check tools with new path
|
||||
self._check_vitis()
|
||||
|
||||
def _on_xilinx_path_changed(self) -> None:
|
||||
"""Handle Xilinx path entry change — sync config immediately."""
|
||||
if self._config:
|
||||
self._config.xilinx_path = self._xilinx_path_var.get()
|
||||
|
||||
def _on_ip_changed(self) -> None:
|
||||
"""Handle IP address entry change — sync config immediately."""
|
||||
if self._config:
|
||||
self._config.zynq_ip = self._ip_string_var.get()
|
||||
|
||||
def _on_file_selected(self, path: str) -> None:
|
||||
"""Handle file selection — sync and auto-save to disk.
|
||||
|
||||
When the user selects a file via the browse dialog, immediately
|
||||
sync the path to the Config object and persist it to YAML so
|
||||
that subsequent step executions pick up the new value.
|
||||
"""
|
||||
self._auto_save_config()
|
||||
|
||||
# ── UI Construction ────────────────────────────────────────
|
||||
|
||||
def _build_ui(self) -> None:
|
||||
"""Build the complete UI layout."""
|
||||
# Scrollable main container — scrollbar appears when content overflows
|
||||
main_frame = ctk.CTkScrollableFrame(self)
|
||||
main_frame.pack(fill="both", expand=True, padx=PADDING_LARGE, pady=PADDING_LARGE)
|
||||
main_frame.grid_rowconfigure(1, weight=1)
|
||||
main_frame.grid_columnconfigure(0, weight=1)
|
||||
self.main_frame = ctk.CTkScrollableFrame(self)
|
||||
self.main_frame.pack(fill="both", expand=True, padx=PADDING_LARGE, pady=PADDING_LARGE)
|
||||
self.main_frame.grid_rowconfigure(1, weight=1)
|
||||
self.main_frame.grid_columnconfigure(0, weight=1)
|
||||
|
||||
# ── Header ──
|
||||
self._build_header(main_frame)
|
||||
self._build_header(self.main_frame)
|
||||
|
||||
# ── Left Panel: Workflow Steps ──
|
||||
left_frame = ctk.CTkFrame(main_frame)
|
||||
left_frame = ctk.CTkFrame(self.main_frame)
|
||||
left_frame.grid(row=1, column=0, sticky="nsew", padx=(0, PADDING))
|
||||
left_frame.grid_rowconfigure(1, weight=1)
|
||||
left_frame.grid_columnconfigure(0, weight=1)
|
||||
@@ -284,7 +349,7 @@ class MainWindow(ctk.CTk):
|
||||
self._build_log_panel(left_frame)
|
||||
|
||||
# ── Right Panel: Configuration ──
|
||||
right_frame = ctk.CTkFrame(main_frame)
|
||||
right_frame = ctk.CTkFrame(self.main_frame)
|
||||
right_frame.grid(row=1, column=1, sticky="nsew", padx=(PADDING, 0))
|
||||
right_frame.grid_rowconfigure(3, weight=1)
|
||||
right_frame.grid_columnconfigure(0, weight=1)
|
||||
@@ -294,6 +359,10 @@ class MainWindow(ctk.CTk):
|
||||
self._build_utility_panel(right_frame)
|
||||
self._build_status_panel(right_frame)
|
||||
|
||||
# ── Maximize / resize handling ──
|
||||
self.bind("<Configure>", self._on_window_resize)
|
||||
self._maximized = False
|
||||
|
||||
def _build_header(self, parent: ctk.CTkFrame) -> None:
|
||||
"""Build the application header."""
|
||||
header = ctk.CTkFrame(parent, corner_radius=CORNER_RADIUS)
|
||||
@@ -335,6 +404,7 @@ class MainWindow(ctk.CTk):
|
||||
row=0, column=0, sticky="nsew",
|
||||
padx=PADDING, pady=(PADDING, 0),
|
||||
)
|
||||
panel.grid_rowconfigure(0, weight=1)
|
||||
panel.grid_columnconfigure(0, weight=1)
|
||||
|
||||
# ── Title ──
|
||||
@@ -448,6 +518,7 @@ class MainWindow(ctk.CTk):
|
||||
row=1, column=0, sticky="nsew",
|
||||
padx=PADDING, pady=PADDING,
|
||||
)
|
||||
self._log_panel = panel
|
||||
panel.grid_rowconfigure(1, weight=1)
|
||||
panel.grid_columnconfigure(0, weight=1)
|
||||
|
||||
@@ -496,6 +567,8 @@ class MainWindow(ctk.CTk):
|
||||
self._xilinx_path_var = ctk.StringVar(value=self._config.xilinx_path)
|
||||
self._xilinx_entry = ctk.CTkEntry(xilinx_frame, textvariable=self._xilinx_path_var)
|
||||
self._xilinx_entry.grid(row=0, column=1, sticky="nsew", padx=PADDING_SMALL)
|
||||
# Bind real-time sync so changes take effect immediately
|
||||
self._xilinx_path_var.trace_add("write", lambda *args: self._on_xilinx_path_changed())
|
||||
ctk.CTkButton(
|
||||
xilinx_frame, text="Browse", font=FONT_SMALL, width=70,
|
||||
command=self._browse_xilinx_path,
|
||||
@@ -510,15 +583,20 @@ class MainWindow(ctk.CTk):
|
||||
self._ip_string_var = ctk.StringVar(value=self._config.zynq_ip)
|
||||
self._ip_entry = ctk.CTkEntry(ip_frame, textvariable=self._ip_string_var)
|
||||
self._ip_entry.grid(row=0, column=1, sticky="nsew", padx=PADDING_SMALL)
|
||||
# Bind real-time sync
|
||||
self._ip_string_var.trace_add("write", lambda *args: self._on_ip_changed())
|
||||
|
||||
# FSBL ELF path (First Stage Bootloader — initialises PS, forces JTAG boot)
|
||||
self._fsbl_selector = FileSelector(
|
||||
panel,
|
||||
label_text="FSBL ELF (.elf):",
|
||||
file_types=[("ELF files", "*.elf"), ("All files", "*")],
|
||||
callback=self._on_file_selected,
|
||||
)
|
||||
self._fsbl_selector._suppress_callback = True
|
||||
if 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_path(str(resolved))
|
||||
self._fsbl_selector.grid(row=3, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
||||
|
||||
@@ -527,9 +605,12 @@ class MainWindow(ctk.CTk):
|
||||
panel,
|
||||
label_text="Bootloader BIT (.bit):",
|
||||
file_types=[("BIT files", "*.bit"), ("All files", "*")],
|
||||
callback=self._on_file_selected,
|
||||
)
|
||||
self._bit_selector._suppress_callback = True
|
||||
if 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_path(str(resolved))
|
||||
self._bit_selector.grid(row=4, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
||||
|
||||
@@ -538,9 +619,12 @@ class MainWindow(ctk.CTk):
|
||||
panel,
|
||||
label_text="Bootloader ELF (.elf):",
|
||||
file_types=[("ELF files", "*.elf"), ("All files", "*")],
|
||||
callback=self._on_file_selected,
|
||||
)
|
||||
self._elf_selector._suppress_callback = True
|
||||
if 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_path(str(resolved))
|
||||
self._elf_selector.grid(row=5, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
||||
|
||||
@@ -549,9 +633,12 @@ class MainWindow(ctk.CTk):
|
||||
panel,
|
||||
label_text="Bootloader BIN (.bin):",
|
||||
file_types=[("BIN files", "*.bin"), ("All files", "*")],
|
||||
callback=self._on_file_selected,
|
||||
)
|
||||
self._bootloader_bin_selector._suppress_callback = True
|
||||
if 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_path(str(resolved))
|
||||
self._bootloader_bin_selector.grid(row=6, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
||||
|
||||
@@ -582,12 +669,23 @@ class MainWindow(ctk.CTk):
|
||||
panel,
|
||||
label_text="Firmware BIN (.bin):",
|
||||
file_types=[("BIN files", "*.bin"), ("All files", "*")],
|
||||
callback=self._on_file_selected,
|
||||
)
|
||||
self._firmware_bin_selector._suppress_callback = True
|
||||
if 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_path(str(resolved))
|
||||
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_btn = ctk.CTkButton(
|
||||
panel,
|
||||
@@ -702,15 +800,17 @@ class MainWindow(ctk.CTk):
|
||||
self._test_version_btn.grid(row=2, column=1, padx=PADDING_SMALL, pady=PADDING_SMALL, sticky="ew")
|
||||
|
||||
def _build_status_panel(self, parent: ctk.CTkFrame) -> None:
|
||||
"""Build the status display panel."""
|
||||
"""Build the status display panel — uses full available space."""
|
||||
panel = ctk.CTkFrame(parent, corner_radius=CORNER_RADIUS)
|
||||
panel.grid(
|
||||
row=3, column=0, sticky="nsew",
|
||||
padx=PADDING, pady=PADDING,
|
||||
)
|
||||
panel.grid_rowconfigure((0, 1), weight=1)
|
||||
panel.grid_columnconfigure(0, weight=1)
|
||||
|
||||
self._status = StatusDisplay(panel)
|
||||
self._status.pack(fill="x", padx=PADDING, pady=PADDING)
|
||||
self._status.grid(row=0, column=0, sticky="nsew", padx=PADDING, pady=PADDING)
|
||||
|
||||
# UART warning label (persistent, not overwritten by other status updates)
|
||||
self._uart_warning_label = ctk.CTkLabel(
|
||||
@@ -720,10 +820,10 @@ class MainWindow(ctk.CTk):
|
||||
anchor="w",
|
||||
text_color=WARNING_COLOR,
|
||||
)
|
||||
self._uart_warning_label.pack(
|
||||
fill="x", padx=PADDING, pady=(0, PADDING_SMALL)
|
||||
self._uart_warning_label.grid(
|
||||
row=1, column=0, sticky="ew", padx=PADDING, pady=(0, PADDING_SMALL)
|
||||
)
|
||||
self._uart_warning_label.pack_forget() # Hide by default
|
||||
self._uart_warning_label.grid_remove() # Hide by default
|
||||
|
||||
# ── Workflow Steps ─────────────────────────────────────────
|
||||
|
||||
@@ -744,6 +844,8 @@ class MainWindow(ctk.CTk):
|
||||
def _get_selected_files(self) -> dict:
|
||||
"""Get selected file paths from the UI.
|
||||
|
||||
Returns absolute paths for immediate backend use.
|
||||
|
||||
Returns:
|
||||
Dictionary with all file paths from UI selectors.
|
||||
"""
|
||||
@@ -881,11 +983,113 @@ class MainWindow(ctk.CTk):
|
||||
msg: Warning message to display.
|
||||
"""
|
||||
self._uart_warning_label.configure(text=msg)
|
||||
self._uart_warning_label.pack(fill="x", padx=PADDING, pady=(0, PADDING_SMALL))
|
||||
self._uart_warning_label.grid()
|
||||
|
||||
def _hide_uart_warning(self) -> None:
|
||||
"""Hide the persistent UART warning label."""
|
||||
self._uart_warning_label.pack_forget()
|
||||
self._uart_warning_label.grid_remove()
|
||||
|
||||
def _on_window_resize(self, event) -> None:
|
||||
"""Handle window resize / maximize events.
|
||||
|
||||
When the window is maximized, scale content to fit one screen
|
||||
and hide scrollbars. When restored, use normal sizing with
|
||||
scrollbars on overflow.
|
||||
"""
|
||||
try:
|
||||
window_state = self.state()
|
||||
is_maximized = window_state == "zoomed"
|
||||
|
||||
if is_maximized != self._maximized:
|
||||
self._maximized = is_maximized
|
||||
self._adjust_layout_for_maximize()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _adjust_layout_for_maximize(self) -> None:
|
||||
"""Scale content and toggle scrollbars based on maximized state."""
|
||||
if not hasattr(self, 'main_frame'):
|
||||
return
|
||||
sf = self.main_frame
|
||||
has_scrollbars = hasattr(sf, '_scrollbar_y')
|
||||
|
||||
if self._maximized:
|
||||
# Hide scrollbars when maximized
|
||||
if has_scrollbars:
|
||||
sf._scrollbar_y.grid_remove()
|
||||
sf._scrollbar_x.grid_remove()
|
||||
# Scale down to fit one screen
|
||||
self._scale_ui(0.6)
|
||||
# Hide the log panel when maximized to save space
|
||||
if hasattr(self, '_log_panel'):
|
||||
self._log_panel.grid_remove()
|
||||
else:
|
||||
# Show scrollbars when not maximized
|
||||
if has_scrollbars:
|
||||
sf._scrollbar_y.grid()
|
||||
sf._scrollbar_x.grid()
|
||||
# Restore normal scale
|
||||
self._scale_ui(1.0)
|
||||
# Show the log panel when not maximized
|
||||
if hasattr(self, '_log_panel'):
|
||||
self._log_panel.grid()
|
||||
|
||||
def _scale_ui(self, factor: float) -> None:
|
||||
"""Scale UI elements by a factor to fit screen.
|
||||
|
||||
Args:
|
||||
factor: Scaling factor (1.0 = normal, <1.0 = smaller).
|
||||
"""
|
||||
# Scale font sizes in key labels
|
||||
scale_font = lambda orig: (orig[0], max(8, int(orig[1] * factor)), orig[2] if len(orig) > 2 else "")
|
||||
|
||||
# Header fonts
|
||||
if hasattr(self, '_vitis_status'):
|
||||
self._vitis_status.configure(font=scale_font(FONT_BODY))
|
||||
if hasattr(self, '_ip_status'):
|
||||
self._ip_status.configure(font=scale_font(FONT_BODY))
|
||||
if hasattr(self, '_jtag_status'):
|
||||
self._jtag_status.configure(font=scale_font(FONT_BODY))
|
||||
|
||||
# Workflow panel fonts
|
||||
if hasattr(self, '_steps'):
|
||||
for step in self._steps:
|
||||
if hasattr(step, '_title_label'):
|
||||
step._title_label.configure(font=scale_font((FONT_BODY[0], 13, "bold")))
|
||||
if hasattr(step, '_desc_label'):
|
||||
step._desc_label.configure(font=scale_font(FONT_SMALL))
|
||||
if hasattr(step, '_action_btn'):
|
||||
step._action_btn.configure(font=scale_font((FONT_BODY[0], 12)))
|
||||
|
||||
# Config panel fonts
|
||||
if hasattr(self, '_xilinx_entry'):
|
||||
self._xilinx_entry.configure(font=scale_font(FONT_BODY))
|
||||
if hasattr(self, '_ip_entry'):
|
||||
self._ip_entry.configure(font=scale_font(FONT_BODY))
|
||||
|
||||
# Log display font
|
||||
if hasattr(self, '_log_display') and hasattr(self._log_display, '_text_widget'):
|
||||
self._log_display._text_widget.configure(font=scale_font(FONT_MONO))
|
||||
|
||||
# Status display font
|
||||
if hasattr(self, '_status') and hasattr(self._status, '_status_text'):
|
||||
self._status._status_text.configure(font=scale_font(FONT_BODY))
|
||||
|
||||
# Scale padding/spacing on all frames
|
||||
if hasattr(self, 'config_frame'):
|
||||
self.config_frame.configure(padx=int(5 * factor), pady=int(5 * factor))
|
||||
if hasattr(self, 'workflow_frame'):
|
||||
self.workflow_frame.configure(padx=int(5 * factor), pady=int(5 * factor))
|
||||
if hasattr(self, 'log_frame'):
|
||||
self.log_frame.configure(padx=int(5 * factor), pady=int(5 * factor))
|
||||
if hasattr(self, 'status_frame'):
|
||||
self.status_frame.configure(padx=int(5 * factor), pady=int(5 * factor))
|
||||
|
||||
# Scale step frame spacing
|
||||
if hasattr(self, '_steps'):
|
||||
for step in self._steps:
|
||||
if hasattr(step, '_inner_frame'):
|
||||
step._inner_frame.configure(padx=int(5 * factor), pady=int(5 * factor))
|
||||
|
||||
def _on_port_changed(self, new_port: str) -> None:
|
||||
"""Handle serial port selection change — validate immediately.
|
||||
|
||||
Reference in New Issue
Block a user