From 699ad828340b87c9534f1c17b14d809ea5e6c092 Mon Sep 17 00:00:00 2001 From: Jeremy Shen Date: Wed, 10 Jun 2026 14:11:59 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix:=20sub-step=20default=20expa?= =?UTF-8?q?nded,=20real-time=20checkbox,=20startup=20freeze,=20color=20syn?= =?UTF-8?q?c?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- config.yaml | 2 +- src/gui/main_window.py | 14 +++++-- src/gui/widgets.py | 83 +++++++++++++++++++++++++++++++----------- 3 files changed, 73 insertions(+), 26 deletions(-) diff --git a/config.yaml b/config.yaml index a59ed29..aafc114 100644 --- a/config.yaml +++ b/config.yaml @@ -1,7 +1,7 @@ bootloader_bin_path: /home/ly0kos/work/Verify/FW/tftp_app/bootloader/bootloader_z100_800M.bin bootloader_bit_path: /home/ly0kos/work/Verify/FW/tftp_app/bootloader/pl_arm_wrapper_hw_platform_0/pl_arm_wrapper.bit bootloader_elf_path: /home/ly0kos/work/Verify/FW/tftp_app/bootloader/app.elf -erase_all: true +erase_all: false firmware_bin_path: /home/ly0kos/work/Verify/FW/V1.3.1.3.3_06_01_pmu_cpld_check/V1.3.1.3.3_06_01_pmu_cpld_check.bin flash_model: s25fl256s1 flash_type: qspi-x4-single diff --git a/src/gui/main_window.py b/src/gui/main_window.py index 65f503a..310a465 100644 --- a/src/gui/main_window.py +++ b/src/gui/main_window.py @@ -97,9 +97,11 @@ class MainWindow(ctk.CTk): self.after(100, self._deferred_init) def _deferred_init(self) -> None: - """Run blocking operations after the window is shown.""" - self._check_vitis() - self._refresh_ports_initial() + """Run blocking operations in background thread after window shown.""" + def _bg_init(): + self._check_vitis() + self._refresh_ports_initial() + threading.Thread(target=_bg_init, daemon=True).start() # ── Config ───────────────────────────────────────────────── @@ -212,6 +214,11 @@ class MainWindow(ctk.CTk): self._log_message(f" ✗ Save failed: {e}") self._status.set_status(f"Save failed: {e}", "error") + def _on_erase_toggle(self) -> None: + """Update config.erase_all immediately when checkbox toggles.""" + if self._config: + self._config.erase_all = self._erase_cb_var.get() + # ── UI Construction ──────────────────────────────────────── def _build_ui(self) -> None: @@ -485,6 +492,7 @@ class MainWindow(ctk.CTk): text="整片Flash擦除 (Erase entire flash)", variable=self._erase_cb_var, font=FONT_SMALL, + command=self._on_erase_toggle, ) self._erase_cb.grid(row=0, column=0, sticky="w", padx=PADDING_SMALL) diff --git a/src/gui/widgets.py b/src/gui/widgets.py index 3d812d2..649e51a 100644 --- a/src/gui/widgets.py +++ b/src/gui/widgets.py @@ -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()