feat: collapsible flash sub-steps UI + erase_all checkbox

Rename erase_before_program → erase_all (semantics fix):
- erase_all: true  → -erase_all -erase_only (entire flash chip)
- erase_all: false → -erase_only (sectors matching BIN size)
Both options always erase; the checkbox controls scope, not yes/no.

UI additions:
- SubStepFrame widget: collapsible Erase/Program/Verify sub-steps
  with status dots (○/◉/●) and colored labels
- Automatically expands on Step 2 start, collapses on completion
- Phase and progress driven by real-time program_flash output parsing
- Checkbox label: '整片Flash擦除 (Erase entire flash)'

subprocess_utils.py phase detection:
- Parses 'Performing Erase/Program/Verify Operation' for phase transitions
- Emits 'phase' callbacks (erase/program/verify/done)
- Progress % tracked within current phase
This commit is contained in:
Jeremy Shen
2026-06-10 13:45:57 +08:00
parent ea3e3b8ac7
commit 95c04d94ae
6 changed files with 190 additions and 37 deletions
+132
View File
@@ -623,3 +623,135 @@ class LogDisplay(ctk.CTkFrame):
self._text_widget.delete("1.0", "end")
self._text_widget.insert("1.0", text)
self._text_widget.configure(state="disabled")
# ════════════════════════════════════════════════════════════════════
# SubStepFrame — collapsible sub-step progress display
# ════════════════════════════════════════════════════════════════════
class SubStepFrame(ctk.CTkFrame):
"""Collapsible frame showing flash operation sub-steps with status.
Three sub-steps: Erase → Program → Verify.
Each shows a status indicator (dot + label) updated via set_phase().
Collapsed by default, expands when a workflow step starts.
"""
def __init__(self, parent: ctk.CTkFrame) -> None:
"""Initialize the sub-step frame.
Args:
parent: Parent CTkFrame widget.
"""
super().__init__(parent, fg_color="transparent")
self.grid_columnconfigure(1, weight=1)
# Collapse toggle
self._collapsed = True
self._toggle_btn = ctk.CTkButton(
self,
text="",
width=24,
height=24,
font=FONT_SMALL,
command=self._toggle,
)
self._toggle_btn.grid(row=0, column=0, padx=(PADDING_SMALL, 4), pady=(2, 0), sticky="nw")
self._header_label = ctk.CTkLabel(
self,
text="Sub-steps",
font=FONT_SMALL,
)
self._header_label.grid(row=0, column=1, padx=(0, PADDING), pady=(2, 0), sticky="w")
# Sub-items (hidden initially)
self._sub_items: list[dict] = []
sub_names = ["Erase", "Program", "Verify"]
for i, name in enumerate(sub_names):
dot = ctk.CTkLabel(self, text="", font=(FONT_SMALL[0], 10), text_color=CONNECTOR_COLOR)
label = ctk.CTkLabel(self, text=name, font=FONT_SMALL, text_color=CONNECTOR_COLOR)
dot.grid(row=i + 1, column=0, padx=(PADDING_SMALL + 14, 4), pady=(1, 1), sticky="e")
label.grid(row=i + 1, column=1, padx=(4, PADDING), pady=(1, 1), sticky="w")
dot.grid_remove()
label.grid_remove()
self._sub_items.append({"dot": dot, "label": label, "name": name})
def _toggle(self) -> None:
"""Toggle collapse/expand."""
self._collapsed = not self._collapsed
self._toggle_btn.configure(text="" if self._collapsed else "")
for item in self._sub_items:
if self._collapsed:
item["dot"].grid_remove()
item["label"].grid_remove()
else:
item["dot"].grid()
item["label"].grid()
def set_expanded(self, expanded: bool) -> None:
"""Set expanded state.
Args:
expanded: True to expand, False to collapse.
"""
if expanded == self._collapsed:
self._toggle()
def set_phase(self, phase: str, pct: int = -1) -> None:
"""Update a sub-step's status.
Args:
phase: One of 'erase', 'program', 'verify', 'done'.
pct: Progress percentage, -1 to show "running", 100 for "done".
"""
phase_map = {"erase": 0, "program": 1, "verify": 2}
colors = {
"pending": CONNECTOR_COLOR,
"running": PRIMARY_COLOR,
"complete": SUCCESS_COLOR,
"error": DANGER_COLOR,
}
if phase == "done":
# Mark all as complete
for item in self._sub_items:
item["dot"].configure(text="", text_color=SUCCESS_COLOR)
item["label"].configure(text_color=SUCCESS_COLOR)
return
idx = phase_map.get(phase, -1)
if idx < 0:
return
# Determine state
if pct >= 100:
state = "complete"
dot_text = ""
elif pct >= 0:
state = "running"
dot_text = ""
else:
state = "running"
dot_text = ""
# Update current and previous
for i, item in enumerate(self._sub_items):
if i < idx:
item["dot"].configure(text="", text_color=SUCCESS_COLOR)
item["label"].configure(text_color=SUCCESS_COLOR)
elif i == idx:
item["dot"].configure(text=dot_text, text_color=colors[state])
item["label"].configure(text_color=colors[state])
else:
item["dot"].configure(text="", text_color=colors["pending"])
item["label"].configure(text_color=colors["pending"])
def reset(self) -> None:
"""Reset all sub-steps to pending state."""
for item in self._sub_items:
item["dot"].configure(text="", text_color=CONNECTOR_COLOR)
item["label"].configure(text_color=CONNECTOR_COLOR)
if not self._collapsed:
self._toggle()