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