Compare commits

...
12 Commits
Author SHA1 Message Date
yuysh c9636b2fcd Revert "fix: UART window stability on Windows — lift/focus, remove stray grab_release"
This reverts commit 4ef1ce0c6b.
2026-06-12 14:18:49 +08:00
yuysh 03bc5d5268 Revert "fix: try both COM11 and \\.\COM11 formats when opening serial port"
This reverts commit 36fbb6a662.
2026-06-12 14:18:49 +08:00
yuysh ef685d6657 Revert "fix: 5 UI improvements — relative paths, filename-only display, real-time config, multi-line status, maximize layout"
This reverts commit a196211708.
2026-06-12 14:18:49 +08:00
yuysh 8a83794dc5 Revert "fix: tag_config for CTkTextbox + auto-save on file selection"
This reverts commit e2e1fdd2a0.
2026-06-12 14:18:45 +08:00
yuysh 5704ada93c Revert "fix: suppress FileSelector callbacks during construction"
This reverts commit 5f9049a321.
2026-06-12 14:18:45 +08:00
yuysh 63711ffede merge: fast-forward to origin/master 2026-06-12 14:18:38 +08:00
yuysh 5f9049a321 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.
2026-06-12 10:29:58 +08:00
yuysh e2e1fdd2a0 fix: tag_config for CTkTextbox + auto-save on file selection
- StatusDisplay: use tag_config (not tag_configure) for CTkTextbox
- FileSelector callbacks now trigger _auto_save_config() so that
  file path changes are persisted immediately to config.yaml
- This fixes the issue where UI file path changes weren't picked up
  by subsequent step executions
2026-06-12 10:20:07 +08:00
yuysh a196211708 fix: 5 UI improvements — relative paths, filename-only display, real-time config, multi-line status, maximize layout
1. FileSelector stores relative paths internally and displays only the
   filename (not full path).  set_relative_path() / get_relative_path()
   methods added.

2. Config changes take effect in real-time: IP and Xilinx path entries
   are bound to trace handlers that sync immediately.  FileSelector
   relative paths are preserved between save/load cycles.

3. StatusDisplay uses a multi-line CTkTextbox instead of a single-line
   label, filling the entire available space.

4. Window maximize handling: bind to <Configure> event, detect
   maximized state, and adjust layout so content fits in one screen
   without scrolling.

5. Workflow panel expanded with weight=1 so it fills available space
   when maximized.
2026-06-12 10:13:11 +08:00
yuysh 36fbb6a662 fix: try both COM11 and \\.\COM11 formats when opening serial port
pyserial should auto-add \\.\ prefix for COM>=10, but some versions
don't. Added _open_serial() helper that tries the raw port name first,
then falls back to explicit NT namespace prefix on Windows.

Replaces all serial.Serial() calls across serial_monitor, reboot_manager,
and widgets.
2026-06-12 09:46:31 +08:00
yuysh 4ef1ce0c6b fix: UART window stability on Windows — lift/focus, remove stray grab_release
CTkToplevel on Windows can fail to display (flash then disappear)
without explicit lift()/focus(). Also removed grab_release() call
in _on_close since grab_set() was never called — calling release
without a matching grab can cause issues on some Windows tkinter
builds.
2026-06-11 18:24:42 +08:00
yuysh 906f787748 fix: guard UART window creation with try-except to prevent silent crash
On Windows the UartMonitorWindow could crash instantly without any
error message. Added try-except around window creation in _read_serial
so errors are logged instead of silently killing the window.

Also added empty port guard before attempting to open.
2026-06-11 18:23:40 +08:00
2 changed files with 26 additions and 126 deletions
+5 -56
View File
@@ -218,21 +218,16 @@ 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 (may not exist yet during _build_config_panel) # Sync IP address
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) self._config.serial_port = self._port_var.get()
if hasattr(self, '_port_var'):
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()
for attr, selector in [ for attr, selector in [
@@ -250,26 +245,7 @@ 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) self._config.erase_all = self._erase_cb_var.get()
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: def _save_config(self) -> None:
"""Save current configuration to user config file (config.yaml). """Save current configuration to user config file (config.yaml).
@@ -317,15 +293,6 @@ class MainWindow(ctk.CTk):
if self._config: if self._config:
self._config.zynq_ip = self._ip_string_var.get() 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 ──────────────────────────────────────── # ── UI Construction ────────────────────────────────────────
def _build_ui(self) -> None: def _build_ui(self) -> None:
@@ -591,9 +558,7 @@ class MainWindow(ctk.CTk):
panel, panel,
label_text="FSBL ELF (.elf):", label_text="FSBL ELF (.elf):",
file_types=[("ELF files", "*.elf"), ("All files", "*")], file_types=[("ELF files", "*.elf"), ("All files", "*")],
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)
@@ -605,9 +570,7 @@ class MainWindow(ctk.CTk):
panel, panel,
label_text="Bootloader BIT (.bit):", label_text="Bootloader BIT (.bit):",
file_types=[("BIT files", "*.bit"), ("All files", "*")], file_types=[("BIT files", "*.bit"), ("All files", "*")],
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)
@@ -619,9 +582,7 @@ class MainWindow(ctk.CTk):
panel, panel,
label_text="Bootloader ELF (.elf):", label_text="Bootloader ELF (.elf):",
file_types=[("ELF files", "*.elf"), ("All files", "*")], file_types=[("ELF files", "*.elf"), ("All files", "*")],
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)
@@ -633,9 +594,7 @@ class MainWindow(ctk.CTk):
panel, panel,
label_text="Bootloader BIN (.bin):", label_text="Bootloader BIN (.bin):",
file_types=[("BIN files", "*.bin"), ("All files", "*")], 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: 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)
@@ -669,23 +628,13 @@ class MainWindow(ctk.CTk):
panel, panel,
label_text="Firmware BIN (.bin):", label_text="Firmware BIN (.bin):",
file_types=[("BIN files", "*.bin"), ("All files", "*")], 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: 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,
+21 -70
View File
@@ -6,7 +6,6 @@ consistent styling from gui/styles.py.
from __future__ import annotations from __future__ import annotations
import os
import threading import threading
import customtkinter as ctk import customtkinter as ctk
@@ -439,9 +438,7 @@ class ProgressIndicator(ctk.CTkFrame):
class StatusDisplay(ctk.CTkFrame): class StatusDisplay(ctk.CTkFrame):
"""Status display widget for operation results. """Status display widget for operation results.
Shows status messages in a multi-line, expandable text area that Shows a status message with color-coded feedback.
uses the full available space. Messages are color-coded and
automatically wrapped.
""" """
def __init__(self, master, **kwargs): def __init__(self, master, **kwargs):
@@ -455,19 +452,15 @@ class StatusDisplay(ctk.CTkFrame):
self._create_widgets() self._create_widgets()
def _create_widgets(self) -> None: def _create_widgets(self) -> None:
"""Create the status display UI — multi-line expandable area.""" """Create the status display UI."""
self.grid_rowconfigure(0, weight=1) self._status_label = ctk.CTkLabel(
self.grid_columnconfigure(0, weight=1)
self._status_text = ctk.CTkTextbox(
self, self,
text="Ready",
font=FONT_BODY, font=FONT_BODY,
corner_radius=CORNER_RADIUS, anchor="w",
state="disabled",
wrap="word",
) )
self._status_text.grid( self._status_label.pack(
row=0, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL fill="x", padx=PADDING, pady=PADDING_SMALL
) )
def set_status(self, message: str, status: str = "info") -> None: def set_status(self, message: str, status: str = "info") -> None:
@@ -477,32 +470,20 @@ class StatusDisplay(ctk.CTkFrame):
message: Status message text. message: Status message text.
status: Status type for coloring ('info', 'success', 'error', 'warning'). status: Status type for coloring ('info', 'success', 'error', 'warning').
""" """
self._status_text.configure(state="normal") self._status_label.configure(text=message)
self._status_text.delete("1.0", "end")
colors = { colors = {
"info": INFO_COLOR, "info": INFO_COLOR,
"success": SUCCESS_COLOR, "success": SUCCESS_COLOR,
"error": DANGER_COLOR, "error": DANGER_COLOR,
"warning": WARNING_COLOR, "warning": WARNING_COLOR,
} }
color = colors.get(status, INFO_COLOR) self._status_label.configure(text_color=colors.get(status, INFO_COLOR))
self._status_text.insert("1.0", message)
# Tag the entire text with the color (CTkTextbox uses tag_config)
self._status_text.tag_config("status", foreground=color)
self._status_text.tag_add("status", "1.0", "end")
self._status_text.configure(state="disabled")
class FileSelector(ctk.CTkFrame): class FileSelector(ctk.CTkFrame):
"""File selector widget with browse button and path display. """File selector widget with browse button and path display.
Stores the full absolute path internally but displays only the Allows users to browse for files and displays the selected path.
filename in the entry field. Supports relative paths: the
``set_relative_path()`` / ``get_relative_path()`` methods work
with paths relative to a given base directory.
""" """
def __init__( def __init__(
@@ -525,10 +506,7 @@ class FileSelector(ctk.CTkFrame):
self._callback = callback self._callback = callback
self._file_types = file_types or [("All files", "*")] self._file_types = file_types or [("All files", "*")]
self._full_path: str = "" # absolute path (internal) self._selected_path = ctk.StringVar(value="")
self._relative_path: str = "" # relative path (synced to Config)
self._display_text = ctk.StringVar(value="")
self._suppress_callback: bool = False # True during construction
self._create_widgets(label_text) self._create_widgets(label_text)
@@ -546,10 +524,10 @@ class FileSelector(ctk.CTkFrame):
) )
label.grid(row=0, column=0, sticky="w", padx=(PADDING, PADDING_SMALL)) label.grid(row=0, column=0, sticky="w", padx=(PADDING, PADDING_SMALL))
# Path entry — shows only the filename # Path entry
self._entry = ctk.CTkEntry( self._entry = ctk.CTkEntry(
self, self,
textvariable=self._display_text, textvariable=self._selected_path,
font=FONT_MONO, font=FONT_MONO,
state="readonly", state="readonly",
) )
@@ -571,60 +549,33 @@ class FileSelector(ctk.CTkFrame):
def _browse(self) -> None: def _browse(self) -> None:
"""Open file dialog and set selected path.""" """Open file dialog and set selected path."""
import customtkinter
from tkinter import filedialog from tkinter import filedialog
file_path = filedialog.askopenfilename( file_path = filedialog.askopenfilename(
title="Select file", title=f"Select {self._selected_path.get() or 'file'}",
filetypes=self._file_types, filetypes=self._file_types,
) )
if file_path: if file_path:
self.set_path(file_path) self.set_path(file_path)
def set_path(self, path: str) -> None: def set_path(self, path: str) -> None:
"""Set the selected file path (absolute or relative). """Set the selected file path.
Internally stores the absolute path and displays only the
filename.
Args: Args:
path: Absolute or relative path to the selected file. path: Absolute path to the selected file.
""" """
self._full_path = path self._selected_path.set(path)
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)
def set_relative_path(self, path: str) -> None:
"""Set a relative path (from config).
Resolves to absolute for internal storage, shows only filename.
Args:
path: Relative path string.
"""
self._relative_path = path
# Resolve to absolute for internal use
if path:
self._full_path = path # caller should resolve before calling
self._display_text.set(os.path.basename(path) if path else "")
if self._callback and not self._suppress_callback:
self._callback(path) self._callback(path)
def get_path(self) -> str: def get_path(self) -> str:
"""Get the currently selected file path (absolute). """Get the currently selected file path.
Returns: Returns:
Selected file path string. Selected file path string.
""" """
return self._full_path return self._selected_path.get()
def get_relative_path(self) -> str:
"""Get the currently selected relative path.
Returns:
Relative path string, or empty string.
"""
return self._relative_path
class LogDisplay(ctk.CTkFrame): class LogDisplay(ctk.CTkFrame):