feat: initial Zynq XC7Z100 Flasher GUI project
Add cross-platform GUI application for programming Zynq devices with step-by-step workflow: - CustomTkinter GUI with dark/light theme support - 4-step workflow: Check Env → Flash → Bootloader → Main Program - YAML configuration with relative path resolution - Serial monitoring and boot log parsing - TFTP upload with CRC32 verification - SSH/serial reboot management - Vivado/Impact tool auto-detection Project structure: - app.py: Entry point - src/: Core modules (config, flash, bitstream, tftp, serial, gui) - config/: Default configuration template - .flasher_env/: Python virtual environment
This commit is contained in:
@@ -0,0 +1,656 @@
|
||||
"""Custom widget components for the Zynq Flasher GUI.
|
||||
|
||||
Provides reusable UI components built on CustomTkinter with
|
||||
consistent styling from gui/styles.py.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import customtkinter as ctk
|
||||
from typing import Callable
|
||||
|
||||
from gui.styles import (
|
||||
FONT_BODY,
|
||||
FONT_MONO,
|
||||
FONT_SMALL,
|
||||
PADDING,
|
||||
PADDING_LARGE,
|
||||
PADDING_SMALL,
|
||||
CORNER_RADIUS,
|
||||
PRIMARY_COLOR,
|
||||
SUCCESS_COLOR,
|
||||
DANGER_COLOR,
|
||||
WARNING_COLOR,
|
||||
INFO_COLOR,
|
||||
ACCENT_PENDING,
|
||||
ACCENT_RUNNING,
|
||||
ACCENT_COMPLETE,
|
||||
ACCENT_ERROR,
|
||||
ACCENT_DISABLED,
|
||||
CONNECTOR_COLOR,
|
||||
DARK_FG,
|
||||
LIGHT_FG,
|
||||
STEP_RUN_BG,
|
||||
STEP_RUN_BG_HOVER,
|
||||
STEP_RUNNER_BG,
|
||||
STEP_RUNNER_BG_HOVER,
|
||||
RUNNING_FADE_LIGHT,
|
||||
RUNNING_FADE_DARK,
|
||||
PULSE_DURATION,
|
||||
STEP_ICON_SIZE,
|
||||
STEP_BUTTON_SIZE,
|
||||
STEP_CARD_BG,
|
||||
STEP_CARD_BG_HOVER,
|
||||
STEP_DISABLED_ALPHA,
|
||||
)
|
||||
|
||||
|
||||
def _is_dark() -> bool:
|
||||
"""Check if dark mode is active.
|
||||
|
||||
Returns:
|
||||
True if dark mode is detected.
|
||||
"""
|
||||
try:
|
||||
import darkdetect
|
||||
return darkdetect.isDark()
|
||||
except ImportError:
|
||||
return True
|
||||
|
||||
|
||||
class StepHeader(ctk.CTkFrame):
|
||||
"""Enhanced step header widget with run/re-run buttons and visual feedback.
|
||||
|
||||
Displays a numbered step with title, status, accent bar, connector line,
|
||||
and per-step action buttons (run/re-run).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
master,
|
||||
step_number: int,
|
||||
title: str,
|
||||
status: str = "pending",
|
||||
description: str = "",
|
||||
callback: Callable[[], None] | None = None,
|
||||
can_run: bool = True,
|
||||
**kwargs,
|
||||
):
|
||||
"""Initialize the step header.
|
||||
|
||||
Args:
|
||||
master: Parent widget.
|
||||
step_number: Step number (1-based).
|
||||
title: Step title text.
|
||||
status: Status indicator ('pending', 'running', 'complete', 'error', 'disabled').
|
||||
description: Secondary description text shown below the title.
|
||||
callback: Called when the run/re-run button is clicked.
|
||||
can_run: Whether this step can be executed independently.
|
||||
"""
|
||||
super().__init__(
|
||||
master,
|
||||
corner_radius=CORNER_RADIUS,
|
||||
fg_color=STEP_CARD_BG,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
self._step_number = step_number
|
||||
self._title = title
|
||||
self._status = status
|
||||
self._description = description
|
||||
self._callback = callback
|
||||
self._can_run = can_run
|
||||
self._pulse_var = False
|
||||
self._pulse_job: str | None = None
|
||||
|
||||
self._create_widgets()
|
||||
self._set_status(status)
|
||||
|
||||
def _create_widgets(self) -> None:
|
||||
"""Create the step header UI components."""
|
||||
self.grid_rowconfigure(0, weight=1)
|
||||
self.grid_columnconfigure((0, 1, 2, 3), weight=0, minsize=PADDING_SMALL)
|
||||
self.grid_columnconfigure(1, weight=1)
|
||||
|
||||
# Accent bar (left edge)
|
||||
self._accent_bar = ctk.CTkFrame(
|
||||
self,
|
||||
width=4,
|
||||
height=40,
|
||||
corner_radius=2,
|
||||
fg_color=ACCENT_PENDING,
|
||||
)
|
||||
self._accent_bar.grid(
|
||||
row=0, column=0,
|
||||
sticky="n",
|
||||
pady=PADDING,
|
||||
)
|
||||
|
||||
# Step number circle
|
||||
self._num_label = ctk.CTkLabel(
|
||||
self,
|
||||
text=str(self._step_number),
|
||||
font=("Segoe UI", 13, "bold"),
|
||||
width=STEP_ICON_SIZE,
|
||||
height=STEP_ICON_SIZE,
|
||||
corner_radius=STEP_ICON_SIZE // 2,
|
||||
fg_color="transparent",
|
||||
text_color=DARK_FG if _is_dark() else LIGHT_FG,
|
||||
)
|
||||
self._num_label.grid(
|
||||
row=0, column=1,
|
||||
sticky="n",
|
||||
pady=PADDING,
|
||||
)
|
||||
|
||||
# Content area (title + description)
|
||||
content_frame = ctk.CTkFrame(self, fg_color="transparent")
|
||||
content_frame.grid(
|
||||
row=0, column=2,
|
||||
sticky="nsew",
|
||||
pady=PADDING,
|
||||
padx=PADDING_SMALL,
|
||||
)
|
||||
content_frame.grid_rowconfigure(0, weight=1)
|
||||
content_frame.grid_columnconfigure(0, weight=1)
|
||||
|
||||
self._title_label = ctk.CTkLabel(
|
||||
content_frame,
|
||||
text=self._title,
|
||||
font=("Segoe UI", 12, "bold"),
|
||||
anchor="w",
|
||||
text_color=DARK_FG if _is_dark() else LIGHT_FG,
|
||||
)
|
||||
self._title_label.grid(
|
||||
row=0, column=0,
|
||||
sticky="nw",
|
||||
pady=(0, 2),
|
||||
)
|
||||
|
||||
self._desc_label = ctk.CTkLabel(
|
||||
content_frame,
|
||||
text=self._description,
|
||||
font=FONT_SMALL,
|
||||
anchor="w",
|
||||
text_color="#999999",
|
||||
wraplength=400,
|
||||
)
|
||||
self._desc_label.grid(
|
||||
row=0, column=0,
|
||||
sticky="nw",
|
||||
pady=(0, 0),
|
||||
)
|
||||
|
||||
# Action buttons area
|
||||
btn_frame = ctk.CTkFrame(self, fg_color="transparent")
|
||||
btn_frame.grid(
|
||||
row=0, column=3,
|
||||
sticky="n",
|
||||
pady=PADDING,
|
||||
)
|
||||
btn_frame.grid_rowconfigure(0, weight=1)
|
||||
btn_frame.grid_columnconfigure(0, weight=1)
|
||||
|
||||
# Run button (▶)
|
||||
self._run_btn = ctk.CTkButton(
|
||||
btn_frame,
|
||||
text="▶",
|
||||
font=("Segoe UI", 12, "bold"),
|
||||
width=STEP_BUTTON_SIZE,
|
||||
height=STEP_BUTTON_SIZE,
|
||||
corner_radius=6,
|
||||
fg_color=STEP_RUN_BG,
|
||||
hover_color=STEP_RUN_BG_HOVER,
|
||||
text_color="white",
|
||||
command=self._on_run,
|
||||
state="disabled",
|
||||
)
|
||||
self._run_btn.grid(row=0, column=0, padx=(0, PADDING_SMALL))
|
||||
|
||||
# Re-run button (↻) — hidden by default
|
||||
self._rerun_btn = ctk.CTkButton(
|
||||
btn_frame,
|
||||
text="↻",
|
||||
font=("Segoe UI", 12, "bold"),
|
||||
width=STEP_BUTTON_SIZE,
|
||||
height=STEP_BUTTON_SIZE,
|
||||
corner_radius=6,
|
||||
fg_color=STEP_RUNNER_BG,
|
||||
hover_color=STEP_RUNNER_BG_HOVER,
|
||||
text_color="white",
|
||||
command=self._on_rerun,
|
||||
state="hidden",
|
||||
)
|
||||
self._rerun_btn.grid(row=0, column=0, padx=(0, PADDING_SMALL))
|
||||
|
||||
# Status icon
|
||||
self._status_label = ctk.CTkLabel(
|
||||
btn_frame,
|
||||
text="",
|
||||
font=("Segoe UI", 14),
|
||||
anchor="e",
|
||||
)
|
||||
self._status_label.grid(row=0, column=0)
|
||||
|
||||
# Hover effect bindings
|
||||
self.bind("<Enter>", self._on_hover_enter)
|
||||
self.bind("<Leave>", self._on_hover_leave)
|
||||
|
||||
def _on_hover_enter(self, event) -> None:
|
||||
"""Handle mouse enter for hover effect."""
|
||||
if self._status not in ("running", "disabled"):
|
||||
self.configure(fg_color=STEP_CARD_BG_HOVER)
|
||||
|
||||
def _on_hover_leave(self, event) -> None:
|
||||
"""Handle mouse leave for hover effect."""
|
||||
if self._status not in ("running", "disabled"):
|
||||
self.configure(fg_color=STEP_CARD_BG)
|
||||
|
||||
def _on_run(self) -> None:
|
||||
"""Handle run button click."""
|
||||
if self._callback:
|
||||
self._callback()
|
||||
|
||||
def _on_rerun(self) -> None:
|
||||
"""Handle re-run button click."""
|
||||
if self._callback:
|
||||
self._callback()
|
||||
|
||||
def _set_status(self, status: str) -> None:
|
||||
"""Set the step status with appropriate colors and icons.
|
||||
|
||||
Args:
|
||||
status: One of 'pending', 'running', 'complete', 'error', 'disabled'.
|
||||
"""
|
||||
self._status = status
|
||||
|
||||
# Accent bar color
|
||||
accent_colors = {
|
||||
"pending": ACCENT_PENDING,
|
||||
"running": RUNNING_FADE_LIGHT,
|
||||
"complete": ACCENT_COMPLETE,
|
||||
"error": ACCENT_ERROR,
|
||||
"disabled": ACCENT_DISABLED,
|
||||
}
|
||||
self._accent_bar.configure(fg_color=accent_colors.get(status, ACCENT_PENDING))
|
||||
|
||||
# Number circle
|
||||
circle_colors = {
|
||||
"pending": "#666666",
|
||||
"running": RUNNING_FADE_LIGHT,
|
||||
"complete": ACCENT_COMPLETE,
|
||||
"error": ACCENT_ERROR,
|
||||
"disabled": ACCENT_DISABLED,
|
||||
}
|
||||
circle_color = circle_colors.get(status, "#666666")
|
||||
self._num_label.configure(fg_color=circle_color)
|
||||
|
||||
# Status icon
|
||||
icons = {
|
||||
"pending": "",
|
||||
"running": "◌",
|
||||
"complete": "✓",
|
||||
"error": "✗",
|
||||
"disabled": "",
|
||||
}
|
||||
icon = icons.get(status, "")
|
||||
self._status_label.configure(text=icon)
|
||||
icon_colors = {
|
||||
"pending": "#666666",
|
||||
"running": RUNNING_FADE_LIGHT,
|
||||
"complete": ACCENT_COMPLETE,
|
||||
"error": ACCENT_ERROR,
|
||||
"disabled": ACCENT_DISABLED,
|
||||
}
|
||||
self._status_label.configure(text_color=icon_colors.get(status, "#666666"))
|
||||
|
||||
# Buttons visibility
|
||||
if status == "running":
|
||||
self._run_btn.configure(state="hidden")
|
||||
self._rerun_btn.configure(state="hidden")
|
||||
self._start_pulse()
|
||||
elif status == "disabled":
|
||||
self._run_btn.configure(state="hidden")
|
||||
self._rerun_btn.configure(state="hidden")
|
||||
self._stop_pulse()
|
||||
self.configure(fg_color="#2A2A2A")
|
||||
self._num_label.configure(fg_color=ACCENT_DISABLED)
|
||||
self._title_label.configure(text_color="#777777")
|
||||
self._desc_label.configure(text_color="#666666")
|
||||
elif status == "complete":
|
||||
self._run_btn.configure(state="hidden")
|
||||
self._rerun_btn.configure(state="normal")
|
||||
self._stop_pulse()
|
||||
elif status == "error":
|
||||
self._run_btn.configure(state="hidden")
|
||||
self._rerun_btn.configure(state="normal")
|
||||
self._stop_pulse()
|
||||
else: # pending
|
||||
if self._can_run:
|
||||
self._run_btn.configure(state="normal")
|
||||
else:
|
||||
self._run_btn.configure(state="disabled")
|
||||
self._rerun_btn.configure(state="hidden")
|
||||
self._stop_pulse()
|
||||
|
||||
# Update description
|
||||
desc_map = {
|
||||
"pending": self._description or "Not started",
|
||||
"running": "Running...",
|
||||
"complete": "Completed successfully",
|
||||
"error": "Failed — click ↻ to retry",
|
||||
"disabled": "Waiting for dependencies",
|
||||
}
|
||||
self._desc_label.configure(text=desc_map.get(status, self._description))
|
||||
|
||||
def set_status(self, status: str) -> None:
|
||||
"""Public method to update step status.
|
||||
|
||||
Args:
|
||||
status: New status string.
|
||||
"""
|
||||
self.after(0, lambda: self._set_status(status))
|
||||
|
||||
def set_description(self, description: str) -> None:
|
||||
"""Update the step description text.
|
||||
|
||||
Args:
|
||||
description: New description text.
|
||||
"""
|
||||
self._description = description
|
||||
if self._status == "pending":
|
||||
self._desc_label.configure(text=description or "Not started")
|
||||
|
||||
def set_callback(self, callback: Callable[[], None] | None) -> None:
|
||||
"""Update the callback for run/re-run buttons.
|
||||
|
||||
Args:
|
||||
callback: New callback function.
|
||||
"""
|
||||
self._callback = callback
|
||||
|
||||
def set_can_run(self, can_run: bool) -> None:
|
||||
"""Update whether this step can be run independently.
|
||||
|
||||
Args:
|
||||
can_run: Whether the step is runnable.
|
||||
"""
|
||||
self._can_run = can_run
|
||||
if self._status == "pending":
|
||||
self._run_btn.configure(state="normal" if can_run else "disabled")
|
||||
|
||||
def _start_pulse(self) -> None:
|
||||
"""Start the pulsing animation for running state."""
|
||||
self._stop_pulse()
|
||||
self._pulse_var = not self._pulse_var
|
||||
color = RUNNING_FADE_LIGHT if self._pulse_var else RUNNING_FADE_DARK
|
||||
self._accent_bar.configure(fg_color=color)
|
||||
self._num_label.configure(fg_color=color)
|
||||
self._pulse_job = self.after(PULSE_DURATION, self._start_pulse)
|
||||
|
||||
def _stop_pulse(self) -> None:
|
||||
"""Stop the pulsing animation."""
|
||||
if self._pulse_job:
|
||||
self.after_cancel(self._pulse_job)
|
||||
self._pulse_job = None
|
||||
|
||||
|
||||
class ProgressIndicator(ctk.CTkFrame):
|
||||
"""Progress indicator widget for workflow steps.
|
||||
|
||||
Shows a progress bar and percentage for long-running operations.
|
||||
"""
|
||||
|
||||
def __init__(self, master, **kwargs):
|
||||
"""Initialize the progress indicator.
|
||||
|
||||
Args:
|
||||
master: Parent widget.
|
||||
"""
|
||||
super().__init__(master, **kwargs)
|
||||
|
||||
self._progress_var = ctk.DoubleVar(value=0)
|
||||
self._create_widgets()
|
||||
|
||||
def _create_widgets(self) -> None:
|
||||
"""Create the progress indicator UI."""
|
||||
self.grid_rowconfigure(0, weight=1)
|
||||
self.grid_columnconfigure(0, weight=1)
|
||||
|
||||
self._progress_bar = ctk.CTkProgressBar(
|
||||
self,
|
||||
variable=self._progress_var,
|
||||
corner_radius=CORNER_RADIUS,
|
||||
height=8,
|
||||
)
|
||||
self._progress_bar.grid(
|
||||
row=0, column=0, sticky="nsew", padx=PADDING
|
||||
)
|
||||
|
||||
self._percent_label = ctk.CTkLabel(
|
||||
self,
|
||||
text="0%",
|
||||
font=FONT_SMALL,
|
||||
)
|
||||
self._percent_label.grid(
|
||||
row=0, column=0, sticky="ne", padx=PADDING, pady=PADDING_SMALL
|
||||
)
|
||||
|
||||
def set_value(self, value: float) -> None:
|
||||
"""Set the progress bar value.
|
||||
|
||||
Args:
|
||||
value: Progress value between 0.0 and 1.0.
|
||||
"""
|
||||
value = max(0.0, min(1.0, value))
|
||||
self._progress_var.set(value)
|
||||
self._percent_label.configure(text=f"{int(value * 100)}%")
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset the progress indicator to 0%."""
|
||||
self.set_value(0.0)
|
||||
|
||||
|
||||
class StatusDisplay(ctk.CTkFrame):
|
||||
"""Status display widget for operation results.
|
||||
|
||||
Shows a status message with color-coded feedback.
|
||||
"""
|
||||
|
||||
def __init__(self, master, **kwargs):
|
||||
"""Initialize the status display.
|
||||
|
||||
Args:
|
||||
master: Parent widget.
|
||||
"""
|
||||
super().__init__(master, **kwargs)
|
||||
|
||||
self._create_widgets()
|
||||
|
||||
def _create_widgets(self) -> None:
|
||||
"""Create the status display UI."""
|
||||
self._status_label = ctk.CTkLabel(
|
||||
self,
|
||||
text="Ready",
|
||||
font=FONT_BODY,
|
||||
anchor="w",
|
||||
)
|
||||
self._status_label.pack(
|
||||
fill="x", padx=PADDING, pady=PADDING_SMALL
|
||||
)
|
||||
|
||||
def set_status(self, message: str, status: str = "info") -> None:
|
||||
"""Update the status message and color.
|
||||
|
||||
Args:
|
||||
message: Status message text.
|
||||
status: Status type for coloring ('info', 'success', 'error', 'warning').
|
||||
"""
|
||||
self._status_label.configure(text=message)
|
||||
colors = {
|
||||
"info": INFO_COLOR,
|
||||
"success": SUCCESS_COLOR,
|
||||
"error": DANGER_COLOR,
|
||||
"warning": WARNING_COLOR,
|
||||
}
|
||||
self._status_label.configure(text_color=colors.get(status, INFO_COLOR))
|
||||
|
||||
|
||||
class FileSelector(ctk.CTkFrame):
|
||||
"""File selector widget with browse button and path display.
|
||||
|
||||
Allows users to browse for files and displays the selected path.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
master,
|
||||
label_text: str,
|
||||
file_types: list[tuple[str, str]] | None = None,
|
||||
callback: Callable[[str], None] | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Initialize the file selector.
|
||||
|
||||
Args:
|
||||
master: Parent widget.
|
||||
label_text: Label text for the selector.
|
||||
file_types: File dialog filter types (e.g., [("BIN files", "*.bin")]).
|
||||
callback: Optional callback when file is selected.
|
||||
"""
|
||||
super().__init__(master, **kwargs)
|
||||
|
||||
self._callback = callback
|
||||
self._file_types = file_types or [("All files", "*")]
|
||||
self._selected_path = ctk.StringVar(value="")
|
||||
|
||||
self._create_widgets(label_text)
|
||||
|
||||
def _create_widgets(self, label_text: str) -> None:
|
||||
"""Create the file selector UI."""
|
||||
self.grid_rowconfigure(0, weight=1)
|
||||
self.grid_columnconfigure(1, weight=1)
|
||||
|
||||
# Label
|
||||
label = ctk.CTkLabel(
|
||||
self,
|
||||
text=label_text,
|
||||
font=FONT_SMALL,
|
||||
anchor="w",
|
||||
)
|
||||
label.grid(row=0, column=0, sticky="w", padx=(PADDING, PADDING_SMALL))
|
||||
|
||||
# Path entry
|
||||
self._entry = ctk.CTkEntry(
|
||||
self,
|
||||
textvariable=self._selected_path,
|
||||
font=FONT_MONO,
|
||||
state="readonly",
|
||||
)
|
||||
self._entry.grid(
|
||||
row=0, column=1, sticky="nsew", padx=PADDING_SMALL
|
||||
)
|
||||
|
||||
# Browse button
|
||||
browse_btn = ctk.CTkButton(
|
||||
self,
|
||||
text="Browse",
|
||||
font=FONT_SMALL,
|
||||
command=self._browse,
|
||||
width=80,
|
||||
)
|
||||
browse_btn.grid(
|
||||
row=0, column=2, padx=(PADDING_SMALL, PADDING)
|
||||
)
|
||||
|
||||
def _browse(self) -> None:
|
||||
"""Open file dialog and set selected path."""
|
||||
import customtkinter
|
||||
from tkinter import filedialog
|
||||
|
||||
file_path = filedialog.askopenfilename(
|
||||
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.
|
||||
|
||||
Args:
|
||||
path: Absolute path to the selected file.
|
||||
"""
|
||||
self._selected_path.set(path)
|
||||
if self._callback:
|
||||
self._callback(path)
|
||||
|
||||
def get_path(self) -> str:
|
||||
"""Get the currently selected file path.
|
||||
|
||||
Returns:
|
||||
Selected file path string.
|
||||
"""
|
||||
return self._selected_path.get()
|
||||
|
||||
|
||||
class LogDisplay(ctk.CTkFrame):
|
||||
"""Log display widget for operation output.
|
||||
|
||||
Shows a scrollable text area with monospace font for log output.
|
||||
"""
|
||||
|
||||
def __init__(self, master, height: int = 200, **kwargs):
|
||||
"""Initialize the log display.
|
||||
|
||||
Args:
|
||||
master: Parent widget.
|
||||
height: Height of the log display in pixels.
|
||||
"""
|
||||
super().__init__(master, **kwargs)
|
||||
|
||||
self._create_widgets(height)
|
||||
|
||||
def _create_widgets(self, height: int) -> None:
|
||||
"""Create the log display UI."""
|
||||
self.grid_rowconfigure(0, weight=1)
|
||||
self.grid_columnconfigure(0, weight=1)
|
||||
|
||||
self._text_widget = ctk.CTkTextbox(
|
||||
self,
|
||||
height=height,
|
||||
font=FONT_MONO,
|
||||
corner_radius=CORNER_RADIUS,
|
||||
state="disabled",
|
||||
)
|
||||
self._text_widget.grid(
|
||||
row=0, column=0, sticky="nsew", padx=PADDING, pady=PADDING
|
||||
)
|
||||
|
||||
def append(self, message: str) -> None:
|
||||
"""Append a message to the log display.
|
||||
|
||||
Args:
|
||||
message: Text to append.
|
||||
"""
|
||||
self._text_widget.configure(state="normal")
|
||||
self._text_widget.insert("end", message + "\n")
|
||||
self._text_widget.see("end")
|
||||
self._text_widget.configure(state="disabled")
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clear the log display."""
|
||||
self._text_widget.configure(state="normal")
|
||||
self._text_widget.delete("1.0", "end")
|
||||
self._text_widget.configure(state="disabled")
|
||||
|
||||
def set_text(self, text: str) -> None:
|
||||
"""Replace all text in the log display.
|
||||
|
||||
Args:
|
||||
text: New text content.
|
||||
"""
|
||||
self._text_widget.configure(state="normal")
|
||||
self._text_widget.delete("1.0", "end")
|
||||
self._text_widget.insert("1.0", text)
|
||||
self._text_widget.configure(state="disabled")
|
||||
Reference in New Issue
Block a user