🐛 fix: sub-step default expanded, real-time checkbox, startup freeze, color sync

1. SubStepFrame defaults to expanded (▼), not collapsed
2. erase_all checkbox has command=_on_erase_toggle → immediate config update
3. _deferred_init runs _check_vitis/_refresh_ports in background thread
   Fixes UI freeze during startup (subprocess.run for tool versions)
4. Sub-step colors now match StepHeader:
   - pending: gray ACCENT_PENDING (#888888)
   - running: blue CIRCLE_RUNNING (#0078D4) with pulse animation
   - complete: green ACCENT_COMPLETE (#107C10)
   - error: red ACCENT_ERROR (#D13438)
   - Running sub-step pulses between light/dark blue like StepHeader
This commit is contained in:
Jeremy Shen
2026-06-10 14:11:59 +08:00
parent 35bd6e600c
commit 699ad82834
3 changed files with 73 additions and 26 deletions
+61 -22
View File
@@ -637,17 +637,16 @@ class LogDisplay(ctk.CTkFrame):
class SubStepFrame(ctk.CTkFrame):
"""Collapsible frame showing flash operation sub-steps with status.
Three sub-steps: Erase → Program → Verify.
Simple white-text dots and labels.
Collapsed by default.
Colors match StepHeader: gray pending, blue running (+pulse),
green complete, red error. Default expanded.
"""
def __init__(self, parent: ctk.CTkFrame) -> None:
super().__init__(parent, fg_color="transparent")
self._collapsed = True
self._collapsed = False
self._toggle_btn = ctk.CTkButton(
self, text="", width=22, height=22,
self, text="", width=22, height=22,
font=(FONT_SMALL[0], 9), command=self._toggle,
)
self._toggle_btn.grid(row=0, column=0, padx=(4, 4), pady=(2, 2), sticky="w")
@@ -661,18 +660,16 @@ class SubStepFrame(ctk.CTkFrame):
self._sub_items: list[dict] = []
names = ["Erase ", "Program", "Verify "]
for i, name in enumerate(names):
dot = ctk.CTkLabel(self, text="", font=FONT_BODY, anchor="w")
label = ctk.CTkLabel(self, text=name, font=FONT_BODY, anchor="w")
pct = ctk.CTkLabel(self, text="", font=FONT_BODY, anchor="e")
dot = ctk.CTkLabel(self, text="", font=FONT_BODY, text_color=ACCENT_PENDING, anchor="w")
label = ctk.CTkLabel(self, text=name, font=FONT_BODY, text_color=ACCENT_PENDING, anchor="w")
pct = ctk.CTkLabel(self, text="", font=FONT_BODY, text_color=ACCENT_PENDING, anchor="e")
dot.grid(row=i + 1, column=0, padx=(16, 2), pady=(2, 2), sticky="w")
label.grid(row=i + 1, column=1, padx=(2, 4), pady=(2, 2), sticky="w")
pct.grid(row=i + 1, column=2, padx=(4, 8), pady=(2, 2), sticky="e")
dot.grid_remove()
label.grid_remove()
pct.grid_remove()
self._sub_items.append({"dot": dot, "label": label, "pct": pct})
self.grid_columnconfigure(1, weight=1)
self._pulse_job: str | None = None
def _toggle(self) -> None:
self._collapsed = not self._collapsed
@@ -692,33 +689,75 @@ class SubStepFrame(ctk.CTkFrame):
self._toggle()
def set_phase(self, phase: str, pct: int = -1) -> None:
_C_PENDING = ACCENT_PENDING
_C_RUNNING = CIRCLE_RUNNING
_C_DONE = ACCENT_COMPLETE
idx = {"erase": 0, "program": 1, "verify": 2}.get(phase, -1)
if idx < 0:
if phase == "done":
self._stop_pulse()
for i in self._sub_items:
i["dot"].configure(text="")
i["pct"].configure(text="OK")
i["dot"].configure(text="", text_color=_C_DONE)
i["label"].configure(text_color=_C_DONE)
i["pct"].configure(text="OK", text_color=_C_DONE)
elif phase == "error":
self._stop_pulse()
for i in self._sub_items:
i["dot"].configure(text="")
i["dot"].configure(text="", text_color=ACCENT_ERROR)
i["label"].configure(text_color=ACCENT_ERROR)
i["pct"].configure(text="", text_color=ACCENT_ERROR)
return
pct_text = f"{pct}%" if pct >= 0 else ""
for i, item in enumerate(self._sub_items):
if i < idx:
item["dot"].configure(text="")
item["pct"].configure(text="OK")
item["dot"].configure(text="", text_color=_C_DONE)
item["label"].configure(text_color=_C_DONE)
item["pct"].configure(text="OK", text_color=_C_DONE)
elif i == idx:
item["dot"].configure(text="")
item["pct"].configure(text=pct_text)
item["dot"].configure(text="", text_color=_C_RUNNING)
item["label"].configure(text_color=_C_RUNNING)
item["pct"].configure(text=pct_text, text_color=_C_RUNNING)
else:
item["dot"].configure(text="")
item["pct"].configure(text="")
item["dot"].configure(text="", text_color=_C_PENDING)
item["label"].configure(text_color=_C_PENDING)
item["pct"].configure(text="", text_color=_C_PENDING)
if phase in ("erase", "program", "verify") and not self._pulse_job:
self._start_pulse(idx)
def _start_pulse(self, idx: int) -> None:
"""Start pulsing the running sub-step to match StepHeader animation."""
_LIGHT = RUNNING_FADE_LIGHT
_DARK = RUNNING_FADE_DARK
def _pulse(state=[True]):
if idx >= len(self._sub_items):
return
item = self._sub_items[idx]
c = _LIGHT if state[0] else _DARK
item["dot"].configure(text_color=c)
item["label"].configure(text_color=c)
item["pct"].configure(text_color=c)
state[0] = not state[0]
self._pulse_job = self.after(PULSE_DURATION, lambda: _pulse(state))
_pulse()
def _stop_pulse(self) -> None:
if self._pulse_job:
self.after_cancel(self._pulse_job)
self._pulse_job = None
def reset(self) -> None:
self._stop_pulse()
_C = ACCENT_PENDING
for i in self._sub_items:
i["dot"].configure(text="")
i["pct"].configure(text="")
i["dot"].configure(text="", text_color=_C)
i["label"].configure(text_color=_C)
i["pct"].configure(text="", text_color=_C)
if not self._collapsed:
self._toggle()
self._toggle() # collapse then expand to reset
self._toggle()