✨ 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:
+3
-3
@@ -1,10 +1,10 @@
|
|||||||
bootloader_bin_path: /home/ly0kos/work/Verify/FW/tftp_app/bootloader/bootloader_z100_800M.bin
|
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_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
|
bootloader_elf_path: /home/ly0kos/work/Verify/FW/tftp_app/bootloader/app.elf
|
||||||
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_type: qspi-x4-single
|
|
||||||
flash_model: s25fl256s1
|
|
||||||
erase_before_program: true
|
erase_before_program: true
|
||||||
|
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
|
||||||
fsbl_elf_path: /home/ly0kos/work/Verify/FPGA/Xilinx/Z100/fsbl_qspi_bypass.elf
|
fsbl_elf_path: /home/ly0kos/work/Verify/FPGA/Xilinx/Z100/fsbl_qspi_bypass.elf
|
||||||
inter_step_delay: 2
|
inter_step_delay: 2
|
||||||
ping_count: 3
|
ping_count: 3
|
||||||
|
|||||||
@@ -22,13 +22,12 @@ bootloader_elf_path: ""
|
|||||||
# bootloader_bin_path is the bootloader image to program into QSPI flash
|
# bootloader_bin_path is the bootloader image to program into QSPI flash
|
||||||
# fsbl_elf_path is the FSBL (First Stage Boot Loader) required by program_flash
|
# fsbl_elf_path is the FSBL (First Stage Boot Loader) required by program_flash
|
||||||
# flash_type sets the QSPI interface mode (qspi-x4-single, qspi-x8-dual_parallel, etc.)
|
# flash_type sets the QSPI interface mode (qspi-x4-single, qspi-x8-dual_parallel, etc.)
|
||||||
# flash_model determines chip capacity: s25fl256s1=32MiB, s25fl128s=16MiB, etc.
|
# erase_all: if true, erase entire flash chip; if false, only erase sectors needed
|
||||||
# erase_before_program: if true, erase flash sectors before programming
|
|
||||||
bootloader_bin_path: ""
|
bootloader_bin_path: ""
|
||||||
fsbl_elf_path: ""
|
fsbl_elf_path: ""
|
||||||
flash_type: "qspi-x4-single"
|
flash_type: "qspi-x4-single"
|
||||||
flash_model: "s25fl256s1"
|
flash_model: "s25fl256s1"
|
||||||
erase_before_program: true
|
erase_all: false
|
||||||
|
|
||||||
# Firmware upload (Step 4: Load Main Program via TFTP)
|
# Firmware upload (Step 4: Load Main Program via TFTP)
|
||||||
firmware_bin_path: ""
|
firmware_bin_path: ""
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ DEFAULT_CONFIG: dict[str, Any] = {
|
|||||||
"fsbl_elf_path": "",
|
"fsbl_elf_path": "",
|
||||||
"flash_type": "qspi-x4-single",
|
"flash_type": "qspi-x4-single",
|
||||||
"flash_model": "s25fl256s1",
|
"flash_model": "s25fl256s1",
|
||||||
"erase_before_program": True,
|
"erase_all": False,
|
||||||
"firmware_bin_path": "",
|
"firmware_bin_path": "",
|
||||||
"reboot_timeout": 30,
|
"reboot_timeout": 30,
|
||||||
"ping_timeout": 5,
|
"ping_timeout": 5,
|
||||||
@@ -63,7 +63,7 @@ class Config:
|
|||||||
fsbl_elf_path: str = ""
|
fsbl_elf_path: str = ""
|
||||||
flash_type: str = "qspi-x4-single"
|
flash_type: str = "qspi-x4-single"
|
||||||
flash_model: str = "s25fl256s1"
|
flash_model: str = "s25fl256s1"
|
||||||
erase_before_program: bool = True
|
erase_all: bool = False
|
||||||
firmware_bin_path: str = ""
|
firmware_bin_path: str = ""
|
||||||
reboot_timeout: int = 30
|
reboot_timeout: int = 30
|
||||||
ping_timeout: int = 5
|
ping_timeout: int = 5
|
||||||
@@ -171,7 +171,7 @@ class Config:
|
|||||||
"fsbl_elf_path": self._relative_path(self.fsbl_elf_path),
|
"fsbl_elf_path": self._relative_path(self.fsbl_elf_path),
|
||||||
"flash_type": self.flash_type,
|
"flash_type": self.flash_type,
|
||||||
"flash_model": self.flash_model,
|
"flash_model": self.flash_model,
|
||||||
"erase_before_program": self.erase_before_program,
|
"erase_all": self.erase_all,
|
||||||
"firmware_bin_path": self._relative_path(self.firmware_bin_path),
|
"firmware_bin_path": self._relative_path(self.firmware_bin_path),
|
||||||
"reboot_timeout": self.reboot_timeout,
|
"reboot_timeout": self.reboot_timeout,
|
||||||
"ping_timeout": self.ping_timeout,
|
"ping_timeout": self.ping_timeout,
|
||||||
|
|||||||
+16
-20
@@ -98,11 +98,11 @@ def wipe_flash(
|
|||||||
config: Config,
|
config: Config,
|
||||||
callback: ProgressCallback = None,
|
callback: ProgressCallback = None,
|
||||||
) -> FlashResult:
|
) -> FlashResult:
|
||||||
"""Erase QSPI flash sectors matching the bootloader image size.
|
"""Erase QSPI flash before programming.
|
||||||
|
|
||||||
Uses -erase_only to erase only the sectors covered by the BIN file.
|
Uses config.erase_all to determine erase scope:
|
||||||
Avoids -erase_all which fails on some flash chips (e.g. s25fl256s1)
|
- True: -erase_all -erase_only (erase entire chip, may fail on some)
|
||||||
that don't report memory density information.
|
- False: -erase_only (erase only sectors matching the BIN file, safer)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
config: Application configuration (needs fsbl_elf_path).
|
config: Application configuration (needs fsbl_elf_path).
|
||||||
@@ -131,7 +131,10 @@ def wipe_flash(
|
|||||||
callback("start", "Erasing QSPI flash...")
|
callback("start", "Erasing QSPI flash...")
|
||||||
|
|
||||||
cmd = _build_program_flash_cmd_base(flash_tool, config)
|
cmd = _build_program_flash_cmd_base(flash_tool, config)
|
||||||
cmd += ["-erase_only"] # Erase sectors matching the BIN size (safer than -erase_all)
|
if getattr(config, 'erase_all', False):
|
||||||
|
cmd += ["-erase_all", "-erase_only"] # Full chip erase
|
||||||
|
else:
|
||||||
|
cmd += ["-erase_only"] # Erase sectors matching BIN size
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = _run_command(cmd, timeout=config.step_timeouts.get("flash_erase", 120), callback=callback)
|
result = _run_command(cmd, timeout=config.step_timeouts.get("flash_erase", 120), callback=callback)
|
||||||
@@ -279,11 +282,11 @@ def full_flash_program(
|
|||||||
bin_path: str | Path,
|
bin_path: str | Path,
|
||||||
callback: ProgressCallback = None,
|
callback: ProgressCallback = None,
|
||||||
) -> list[FlashResult]:
|
) -> list[FlashResult]:
|
||||||
"""Execute full flash workflow: (optional wipe) → program → verify.
|
"""Execute full flash workflow: wipe → program → verify.
|
||||||
|
|
||||||
If config.erase_before_program is True, erases flash sectors before
|
Erase scope is controlled by config.erase_all:
|
||||||
programming. Otherwise, program_flash handles sector erase during
|
- True: entire flash chip (uses -erase_all)
|
||||||
programming (per-sector, not full chip).
|
- False: sectors matching BIN file size (uses -erase_only)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
config: Application configuration.
|
config: Application configuration.
|
||||||
@@ -295,17 +298,10 @@ def full_flash_program(
|
|||||||
"""
|
"""
|
||||||
results: list[FlashResult] = []
|
results: list[FlashResult] = []
|
||||||
|
|
||||||
# Step 1: Erase (optional — controlled by config.erase_before_program)
|
# Step 1: Erase (scope determined by config.erase_all)
|
||||||
if getattr(config, 'erase_before_program', True):
|
results.append(wipe_flash(config, callback))
|
||||||
results.append(wipe_flash(config, callback))
|
if not results[-1].success:
|
||||||
if not results[-1].success:
|
return results
|
||||||
return results
|
|
||||||
else:
|
|
||||||
results.append(FlashResult(
|
|
||||||
step="wipe",
|
|
||||||
success=True,
|
|
||||||
message="Erase skipped (handled by program_flash)",
|
|
||||||
))
|
|
||||||
|
|
||||||
# Step 2: Program and verify (program_flash -verify)
|
# Step 2: Program and verify (program_flash -verify)
|
||||||
results.append(program_flash_bin(config, bin_path, callback))
|
results.append(program_flash_bin(config, bin_path, callback))
|
||||||
|
|||||||
+34
-8
@@ -52,6 +52,7 @@ from gui.widgets import (
|
|||||||
StatusDisplay,
|
StatusDisplay,
|
||||||
FileSelector,
|
FileSelector,
|
||||||
LogDisplay,
|
LogDisplay,
|
||||||
|
SubStepFrame,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -187,7 +188,7 @@ class MainWindow(ctk.CTk):
|
|||||||
self._config.bootloader_elf_path = files["bootloader_elf_path"]
|
self._config.bootloader_elf_path = files["bootloader_elf_path"]
|
||||||
self._config.bootloader_bin_path = files["bootloader_bin_path"]
|
self._config.bootloader_bin_path = files["bootloader_bin_path"]
|
||||||
self._config.fsbl_elf_path = files["fsbl_elf_path"]
|
self._config.fsbl_elf_path = files["fsbl_elf_path"]
|
||||||
self._config.erase_before_program = self._erase_cb_var.get()
|
self._config.erase_all = self._erase_cb_var.get()
|
||||||
self._config.firmware_bin_path = files["firmware_bin_path"]
|
self._config.firmware_bin_path = files["firmware_bin_path"]
|
||||||
|
|
||||||
def _save_config(self) -> None:
|
def _save_config(self) -> None:
|
||||||
@@ -338,6 +339,16 @@ class MainWindow(ctk.CTk):
|
|||||||
pady=(0, 2),
|
pady=(0, 2),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Step 2: add collapsible sub-step frame
|
||||||
|
if i == 1:
|
||||||
|
self._sub_step_frame = SubStepFrame(panel)
|
||||||
|
self._sub_step_frame.grid(
|
||||||
|
row=i * 2 + 2, column=0,
|
||||||
|
sticky="nsew",
|
||||||
|
padx=PADDING_SMALL + 16,
|
||||||
|
pady=(0, 2),
|
||||||
|
)
|
||||||
|
|
||||||
# ── Progress indicator ──
|
# ── Progress indicator ──
|
||||||
self._progress = ProgressIndicator(panel)
|
self._progress = ProgressIndicator(panel)
|
||||||
self._progress.grid(
|
self._progress.grid(
|
||||||
@@ -468,10 +479,10 @@ class MainWindow(ctk.CTk):
|
|||||||
flash_frame.grid(row=6, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
flash_frame.grid(row=6, column=0, sticky="nsew", padx=PADDING, pady=PADDING_SMALL)
|
||||||
flash_frame.grid_columnconfigure(0, weight=1)
|
flash_frame.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
self._erase_cb_var = ctk.BooleanVar(value=self._config.erase_before_program)
|
self._erase_cb_var = ctk.BooleanVar(value=self._config.erase_all)
|
||||||
self._erase_cb = ctk.CTkCheckBox(
|
self._erase_cb = ctk.CTkCheckBox(
|
||||||
flash_frame,
|
flash_frame,
|
||||||
text="Erase flash before programming",
|
text="整片Flash擦除 (Erase entire flash)",
|
||||||
variable=self._erase_cb_var,
|
variable=self._erase_cb_var,
|
||||||
font=FONT_SMALL,
|
font=FONT_SMALL,
|
||||||
)
|
)
|
||||||
@@ -748,20 +759,25 @@ class MainWindow(ctk.CTk):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def _flash_callback(self, status: str, message: str) -> None:
|
def _flash_callback(self, status: str, message: str) -> None:
|
||||||
"""Callback for flash programming progress — tracks phases."""
|
"""Callback for flash programming — drives sub-step UI."""
|
||||||
if status == "phase":
|
if status == "phase":
|
||||||
# Track current flash phase for UI
|
self._flash_phase = message
|
||||||
self._flash_phase = message # 'erase', 'program', 'verify', 'done'
|
|
||||||
phase_names = {"erase": "Erasing", "program": "Programming", "verify": "Verifying", "done": "Done"}
|
phase_names = {"erase": "Erasing", "program": "Programming", "verify": "Verifying", "done": "Done"}
|
||||||
label = phase_names.get(message, message)
|
label = phase_names.get(message, message)
|
||||||
self._log_message(f" ▸ {label}")
|
self._log_message(f" ▸ {label}")
|
||||||
|
if hasattr(self, '_sub_step_frame'):
|
||||||
|
self._sub_step_frame.set_phase(message)
|
||||||
return
|
return
|
||||||
if status == "progress":
|
if status == "progress":
|
||||||
pct = message.rstrip('%')
|
try:
|
||||||
# Show progress within current phase
|
pct = int(message.rstrip('%'))
|
||||||
|
except ValueError:
|
||||||
|
pct = -1
|
||||||
phase_label = {"erase": "Erase", "program": "Program", "verify": "Verify"}.get(
|
phase_label = {"erase": "Erase", "program": "Program", "verify": "Verify"}.get(
|
||||||
self._flash_phase, "Flash")
|
self._flash_phase, "Flash")
|
||||||
self._log_message(f" [{phase_label}] {pct}%")
|
self._log_message(f" [{phase_label}] {pct}%")
|
||||||
|
if hasattr(self, '_sub_step_frame'):
|
||||||
|
self._sub_step_frame.set_phase(self._flash_phase, pct)
|
||||||
return
|
return
|
||||||
self._log_message(f" [{status}] {message}")
|
self._log_message(f" [{status}] {message}")
|
||||||
|
|
||||||
@@ -1018,6 +1034,12 @@ class MainWindow(ctk.CTk):
|
|||||||
step_idx = i - start_index
|
step_idx = i - start_index
|
||||||
self._set_step_status(i, "running")
|
self._set_step_status(i, "running")
|
||||||
|
|
||||||
|
# Step 2: expand sub-step frame and reset
|
||||||
|
if i == 1 and hasattr(self, '_sub_step_frame'):
|
||||||
|
self._sub_step_frame.reset()
|
||||||
|
self._sub_step_frame.set_expanded(True)
|
||||||
|
self._flash_phase = ""
|
||||||
|
|
||||||
# Log step timeout estimate
|
# Log step timeout estimate
|
||||||
timeout_key, default_timeout = step_timeout_keys[i] if i < len(step_timeout_keys) else ("", 30)
|
timeout_key, default_timeout = step_timeout_keys[i] if i < len(step_timeout_keys) else ("", 30)
|
||||||
step_timeout = self._config.step_timeouts.get(timeout_key, default_timeout) if self._config else default_timeout
|
step_timeout = self._config.step_timeouts.get(timeout_key, default_timeout) if self._config else default_timeout
|
||||||
@@ -1032,6 +1054,10 @@ class MainWindow(ctk.CTk):
|
|||||||
self._set_step_status(i, "complete" if success else "error")
|
self._set_step_status(i, "complete" if success else "error")
|
||||||
elapsed = time.time() - t_step_start
|
elapsed = time.time() - t_step_start
|
||||||
self._log_message(f" ⏱ Step {i+1} completed in {elapsed:.0f}s")
|
self._log_message(f" ⏱ Step {i+1} completed in {elapsed:.0f}s")
|
||||||
|
|
||||||
|
# Step 2: mark all sub-steps done
|
||||||
|
if i == 1 and hasattr(self, '_sub_step_frame'):
|
||||||
|
self._sub_step_frame.set_phase("done")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
results.append(False)
|
results.append(False)
|
||||||
self._set_step_status(i, "error")
|
self._set_step_status(i, "error")
|
||||||
|
|||||||
@@ -623,3 +623,135 @@ class LogDisplay(ctk.CTkFrame):
|
|||||||
self._text_widget.delete("1.0", "end")
|
self._text_widget.delete("1.0", "end")
|
||||||
self._text_widget.insert("1.0", text)
|
self._text_widget.insert("1.0", text)
|
||||||
self._text_widget.configure(state="disabled")
|
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()
|
||||||
|
|||||||
Reference in New Issue
Block a user