diff --git a/app/__init__.py b/app/__init__.py index bab5a40..20b175c 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,5 +1,5 @@ """Application package.""" -from .app import PurpleTunerApp, start_app +from .app import ColorCalcApp, start_app -__all__ = ["PurpleTunerApp", "start_app"] +__all__ = ["ColorCalcApp", "start_app"] diff --git a/app/app.py b/app/app.py index cfacbd8..1e35b60 100644 --- a/app/app.py +++ b/app/app.py @@ -8,7 +8,7 @@ from .gui import ColorPickerMixin, ExclusionMixin, ThemeMixin, UIBuilderMixin from .logic import DEFAULTS, ImageProcessingMixin, ResetMixin -class PurpleTunerApp( +class ColorCalcApp( ThemeMixin, UIBuilderMixin, ImageProcessingMixin, @@ -16,11 +16,11 @@ class PurpleTunerApp( ColorPickerMixin, ResetMixin, ): - """Tkinter based application for highlighting purple hues in images.""" + """Tkinter based application for isolating configurable colour ranges.""" def __init__(self, root: tk.Tk): self.root = root - self.root.title("Purple Tuner — Bild + Overlay") + self.root.title("ColorCalc — Bild + Overlay") try: self.root.state("zoomed") except Exception: @@ -66,8 +66,8 @@ class PurpleTunerApp( def start_app() -> None: """Entry point used by the CLI script.""" root = tk.Tk() - app = PurpleTunerApp(root) + app = ColorCalcApp(root) root.mainloop() -__all__ = ["PurpleTunerApp", "start_app"] +__all__ = ["ColorCalcApp", "start_app"] diff --git a/app/gui/color_picker.py b/app/gui/color_picker.py index 72135db..0959ea1 100644 --- a/app/gui/color_picker.py +++ b/app/gui/color_picker.py @@ -11,19 +11,34 @@ class ColorPickerMixin: """Handles colour selection from dialogs and mouse clicks.""" ref_hue: float | None + hue_span: float = 45.0 # degrees around the picked hue def choose_color(self): rgb, hex_colour = colorchooser.askcolor(title="Farbe wählen") if rgb is None: return - r, g, b = [int(channel) for channel in rgb] - h, s, v = colorsys.rgb_to_hsv(r / 255.0, g / 255.0, b / 255.0) - self.ref_hue = h * 360.0 - span = 30 - self.hue_min.set((self.ref_hue - span) % 360) - self.hue_max.set((self.ref_hue + span) % 360) - self.update_preview() - self.status.config(text=f"Farbe gewählt: {hex_colour} (Hue {self.ref_hue:.1f}°)") + r, g, b = (int(round(channel)) for channel in rgb) + hue_deg, sat_pct, val_pct = self._apply_rgb_selection(r, g, b) + label = hex_colour or f"RGB({r}, {g}, {b})" + self.status.config( + text=f"Farbe gewählt: {label} — Hue {hue_deg:.1f}°, S {sat_pct:.0f}%, V {val_pct:.0f}%" + ) + + def apply_sample_colour(self, hex_colour: str, name: str | None = None) -> None: + """Apply a predefined colour preset.""" + rgb = self._parse_hex_colour(hex_colour) + if rgb is None: + return + hue_deg, sat_pct, val_pct = self._apply_rgb_selection(*rgb) + if self.pick_mode: + self.pick_mode = False + label = name or hex_colour.upper() + self.status.config( + text=( + f"Beispielfarbe gewählt: {label} ({hex_colour}) — " + f"Hue {hue_deg:.1f}°, S {sat_pct:.0f}%, V {val_pct:.0f}%" + ) + ) def enable_pick_mode(self): if self.preview_img is None: @@ -47,14 +62,62 @@ class ColorPickerMixin: r, g, b, a = self.preview_img.getpixel((x, y)) if a == 0: return - h, s, v = colorsys.rgb_to_hsv(r / 255.0, g / 255.0, b / 255.0) - self.ref_hue = h * 360.0 - span = 30 - self.hue_min.set((self.ref_hue - span) % 360) - self.hue_max.set((self.ref_hue + span) % 360) + hue_deg, sat_pct, val_pct = self._apply_rgb_selection(r, g, b) self.disable_pick_mode() + self.status.config( + text=f"Farbe vom Bild gewählt: Hue {hue_deg:.1f}°, S {sat_pct:.0f}%, V {val_pct:.0f}%" + ) + + def _apply_rgb_selection(self, r: int, g: int, b: int) -> tuple[float, float, float]: + """Update slider ranges based on an RGB colour and return HSV summary.""" + h, s, v = colorsys.rgb_to_hsv(r / 255.0, g / 255.0, b / 255.0) + hue_deg = (h * 360.0) % 360.0 + self.ref_hue = hue_deg + self._set_slider_targets(hue_deg, s, v) self.update_preview() - self.status.config(text=f"Farbe vom Bild gewählt: Hue {self.ref_hue:.1f}°") + return hue_deg, s * 100.0, v * 100.0 + + def _set_slider_targets(self, hue_deg: float, saturation: float, value: float) -> None: + span = getattr(self, "hue_span", 45.0) + self.hue_min.set((hue_deg - span) % 360) + self.hue_max.set((hue_deg + span) % 360) + + sat_pct = saturation * 100.0 + sat_margin = 35.0 + sat_min = max(0.0, min(100.0, sat_pct - sat_margin)) + if saturation <= 0.05: + sat_min = 0.0 + self.sat_min.set(sat_min) + + v_pct = value * 100.0 + val_margin = 35.0 + val_min = max(0.0, v_pct - val_margin) + val_max = min(100.0, v_pct + val_margin) + if value <= 0.15: + val_max = min(45.0, max(val_max, 25.0)) + if value >= 0.85: + val_min = max(55.0, min(val_min, 80.0)) + if val_max <= val_min: + val_max = min(100.0, val_min + 10.0) + self.val_min.set(val_min) + self.val_max.set(val_max) + + @staticmethod + def _parse_hex_colour(hex_colour: str | None) -> tuple[int, int, int] | None: + if not hex_colour: + return None + value = hex_colour.strip().lstrip("#") + if len(value) == 3: + value = "".join(ch * 2 for ch in value) + if len(value) != 6: + return None + try: + r = int(value[0:2], 16) + g = int(value[2:4], 16) + b = int(value[4:6], 16) + except ValueError: + return None + return r, g, b __all__ = ["ColorPickerMixin"] diff --git a/app/gui/theme.py b/app/gui/theme.py index 3242120..ca670a3 100644 --- a/app/gui/theme.py +++ b/app/gui/theme.py @@ -57,7 +57,12 @@ class ThemeMixin: else: self.scale_style = "Horizontal.TScale" - bg, fg = ("#0f0f10", "#f1f1f1") if self.theme == "dark" else ("#f2f2f7", "#202020") + if self.theme == "dark": + bg, fg = "#0f0f10", "#f1f1f1" + status_fg = "#f5f5f5" + else: + bg, fg = "#ededf2", "#202020" + status_fg = "#1c1c1c" self.root.configure(bg=bg) # type: ignore[attr-defined] s = self.style @@ -69,6 +74,14 @@ class ThemeMixin: ) s.map("TButton", background=[("active", "#d0d0d0")]) + button_refresher = getattr(self, "_refresh_toolbar_buttons_theme", None) + if callable(button_refresher): + button_refresher() + + status_refresher = getattr(self, "_refresh_status_palette", None) + if callable(status_refresher) and hasattr(self, "status"): + status_refresher(status_fg) + def detect_system_theme(self) -> str: """Best-effort detection of the OS theme preference.""" try: diff --git a/app/gui/ui.py b/app/gui/ui.py index 879ebb6..163d68a 100644 --- a/app/gui/ui.py +++ b/app/gui/ui.py @@ -3,10 +3,9 @@ from __future__ import annotations import tkinter as tk +import tkinter.font as tkfont from tkinter import ttk -from .theme import HAS_TTKBOOTSTRAP - class UIBuilderMixin: """Constructs the Tkinter UI and common widgets.""" @@ -24,13 +23,33 @@ class UIBuilderMixin: ("🔄 Slider zurücksetzen", self.reset_sliders), ("🌓 Theme umschalten", self.toggle_theme), ] - for text, command in buttons: - if HAS_TTKBOOTSTRAP: - from ttkbootstrap import Button # type: ignore + self._toolbar_buttons: list[dict[str, object]] = [] - Button(toolbar, text=text, command=command, bootstyle="secondary").pack(side=tk.LEFT, padx=6) - else: - ttk.Button(toolbar, text=text, command=command).pack(side=tk.LEFT, padx=6) + buttons_frame = ttk.Frame(toolbar) + buttons_frame.pack(side=tk.LEFT) + for text, command in buttons: + self._add_toolbar_button(buttons_frame, text, command) + + status_container = ttk.Frame(toolbar) + status_container.pack(side=tk.RIGHT, expand=True, fill=tk.X) + self.status = ttk.Label( + status_container, + text="Keine Datei geladen.", + anchor="e", + foreground="#efefef", + ) + self.status.pack(fill=tk.X) + self._attach_copy_menu(self.status) + self.status_default_text = self.status.cget("text") + self._status_palette = {"fg": self.status.cget("foreground")} + + palette_frame = ttk.Frame(self.root) + palette_frame.pack(fill=tk.X, padx=12, pady=(0, 8)) + ttk.Label(palette_frame, text="Beispielfarben:").pack(side=tk.LEFT, padx=(0, 8)) + swatch_container = ttk.Frame(palette_frame) + swatch_container.pack(side=tk.LEFT) + for name, hex_code in self._preset_colours(): + self._add_palette_swatch(swatch_container, name, hex_code) sliders_frame = ttk.Frame(self.root) sliders_frame.pack(fill=tk.X, padx=12, pady=4) @@ -49,30 +68,45 @@ class UIBuilderMixin: main = ttk.Frame(self.root) main.pack(fill=tk.BOTH, expand=True, padx=12, pady=12) - self.canvas_orig = tk.Canvas(main, bg="#1e1e1e", highlightthickness=0, relief="flat") - self.canvas_orig.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 6)) + left_column = ttk.Frame(main) + left_column.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 6)) + + self.canvas_orig = tk.Canvas(left_column, bg="#1e1e1e", highlightthickness=0, relief="flat") + self.canvas_orig.pack(fill=tk.BOTH, expand=True) self.canvas_orig.bind("", self.on_canvas_click) self.canvas_orig.bind("", self._exclude_start) self.canvas_orig.bind("", self._exclude_drag) self.canvas_orig.bind("", self._exclude_end) - self.canvas_overlay = tk.Canvas(main, bg="#1e1e1e", highlightthickness=0, relief="flat") - self.canvas_overlay.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(6, 0)) + right_column = ttk.Frame(main) + right_column.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(6, 0)) - status_frame = ttk.Frame(self.root) - status_frame.pack(fill=tk.X, padx=12, pady=6) - self.status = ttk.Label(status_frame, text="Keine Datei geladen.") - self.status.pack(anchor="w") - self._attach_copy_menu(self.status) - self.ratio_label = ttk.Label(status_frame, text="Purple (mit Excludes): —") - self.ratio_label.pack(anchor="w") - self._attach_copy_menu(self.ratio_label) - self.hint = ttk.Label( - status_frame, - text="Tipp: Rechtsklick + Ziehen auf dem linken Bild, um Bereiche auszuschließen. Esc beendet den Pick-Modus.", + self.canvas_overlay = tk.Canvas(right_column, bg="#1e1e1e", highlightthickness=0, relief="flat") + self.canvas_overlay.pack(fill=tk.BOTH, expand=True) + + info_frame = ttk.Frame(self.root) + info_frame.pack(fill=tk.X, padx=12, pady=(0, 12)) + self.filename_label = ttk.Label( + info_frame, + text="—", + foreground="#f2c744", + font=("Segoe UI", 10, "bold"), + anchor="center", + justify="center", ) - self.hint.pack(anchor="w", pady=(2, 0)) - self._attach_copy_menu(self.hint) + self.filename_label.pack(anchor="center") + self._attach_copy_menu(self.filename_label) + + self.ratio_label = ttk.Label( + info_frame, + text="Treffer (mit Excludes): —", + foreground="#f2c744", + font=("Segoe UI", 10, "bold"), + anchor="center", + justify="center", + ) + self.ratio_label.pack(anchor="center", pady=(4, 0)) + self._attach_copy_menu(self.ratio_label) self.root.bind("", self.disable_pick_mode) @@ -114,6 +148,211 @@ class UIBuilderMixin: pass self._update_job = self.root.after(self.update_delay_ms, self.update_preview) + def _preset_colours(self): + return [ + ("Rot", "#ff3b30"), + ("Orange", "#ff9500"), + ("Gelb", "#ffd60a"), + ("Grün", "#34c759"), + ("Türkis", "#5ac8fa"), + ("Blau", "#0a84ff"), + ("Violett", "#af52de"), + ("Magenta", "#ff2d55"), + ("Weiß", "#ffffff"), + ("Grau", "#8e8e93"), + ("Schwarz", "#000000"), + ] + + def _add_palette_swatch(self, parent, name: str, hex_code: str) -> None: + swatch = tk.Canvas( + parent, + width=26, + height=26, + highlightthickness=1, + highlightbackground="#b1b1b6", + background="#ffffff", + bd=0, + relief="flat", + takefocus=1, + cursor="hand2", + ) + swatch.pack(side=tk.LEFT, padx=4, pady=2) + swatch.create_rectangle(3, 3, 23, 23, outline="#4a4a4a", fill=hex_code) + + def trigger(_event=None, colour=hex_code, label=name): + self.apply_sample_colour(colour, label) + + swatch.bind("", trigger) + swatch.bind("", trigger) + swatch.bind("", trigger) + swatch.bind("", lambda _e: swatch.configure(highlightbackground="#71717a")) + swatch.bind("", lambda _e: swatch.configure(highlightbackground="#b1b1b6")) + + def _add_toolbar_button(self, parent, text: str, command) -> None: + font = tkfont.Font(root=self.root, family="Segoe UI", size=10, weight="bold") + padding_x = 18 + width = font.measure(text) + padding_x * 2 + height = 32 + radius = 12 + bg = self.root.cget("bg") if hasattr(self.root, "cget") else "#f2f2f7" + canvas = tk.Canvas( + parent, + width=width, + height=height, + bd=0, + highlightthickness=0, + bg=bg, + relief="flat", + cursor="hand2", + takefocus=1, + ) + canvas.pack(side=tk.LEFT, padx=6) + + palette = self._toolbar_palette() + rect_id = self._create_round_rect( + canvas, + 1, + 1, + width - 1, + height - 1, + radius, + fill=palette["normal"], + outline=palette["outline"], + width=1, + ) + text_id = canvas.create_text( + width / 2, + height / 2, + text=text, + font=font, + fill=palette["text"], + ) + + button_data = { + "canvas": canvas, + "rect": rect_id, + "text_id": text_id, + "command": command, + "palette": palette.copy(), + "dimensions": (width, height, radius), + } + self._toolbar_buttons.append(button_data) + + def set_fill(state: str) -> None: + pal: dict[str, str] = button_data["palette"] # type: ignore[index] + canvas.itemconfigure(rect_id, fill=pal[state]) # type: ignore[index] + + def execute(): + command() + + def on_press(_event=None): + set_fill("active") + + def on_release(event=None): + if event is not None and ( + event.x < 0 or event.y < 0 or event.x > width or event.y > height + ): + set_fill("normal") + return + set_fill("hover") + self.root.after_idle(execute) + + def on_enter(_event): + set_fill("hover") + + def on_leave(_event): + set_fill("normal") + + def on_focus_in(_event): + pal: dict[str, str] = button_data["palette"] # type: ignore[index] + canvas.itemconfigure(rect_id, outline=pal["outline_focus"]) # type: ignore[index] + + def on_focus_out(_event): + pal: dict[str, str] = button_data["palette"] # type: ignore[index] + canvas.itemconfigure(rect_id, outline=pal["outline"]) # type: ignore[index] + + def invoke_keyboard(_event=None): + set_fill("active") + canvas.after(120, lambda: set_fill("hover")) + self.root.after_idle(execute) + + canvas.bind("", on_press) + canvas.bind("", on_release) + canvas.bind("", on_enter) + canvas.bind("", on_leave) + canvas.bind("", on_focus_in) + canvas.bind("", on_focus_out) + canvas.bind("", invoke_keyboard) + canvas.bind("", invoke_keyboard) + + @staticmethod + def _create_round_rect(canvas: tk.Canvas, x1, y1, x2, y2, radius, **kwargs): + points = [ + x1 + radius, + y1, + x2 - radius, + y1, + x2, + y1, + x2, + y1 + radius, + x2, + y2 - radius, + x2, + y2, + x2 - radius, + y2, + x1 + radius, + y2, + x1, + y2, + x1, + y2 - radius, + x1, + y1 + radius, + x1, + y1, + ] + return canvas.create_polygon(points, smooth=True, splinesteps=24, **kwargs) + + def _toolbar_palette(self) -> dict[str, str]: + is_dark = getattr(self, "theme", "light") == "dark" + if is_dark: + return { + "normal": "#2f2f35", + "hover": "#3a3a40", + "active": "#1f1f25", + "outline": "#4d4d50", + "outline_focus": "#7c7c88", + "text": "#f1f1f5", + } + return { + "normal": "#ffffff", + "hover": "#ededf4", + "active": "#dcdce6", + "outline": "#d0d0d8", + "outline_focus": "#a9a9b2", + "text": "#1f1f1f", + } + + def _refresh_toolbar_buttons_theme(self) -> None: + if not getattr(self, "_toolbar_buttons", None): + return + bg = self.root.cget("bg") if hasattr(self.root, "cget") else "#f2f2f7" + palette = self._toolbar_palette() + for data in self._toolbar_buttons: + canvas = data["canvas"] # type: ignore[index] + rect_id = data["rect"] # type: ignore[index] + text_id = data["text_id"] # type: ignore[index] + data["palette"] = palette.copy() + canvas.configure(bg=bg) + canvas.itemconfigure(rect_id, fill=palette["normal"], outline=palette["outline"]) + canvas.itemconfigure(text_id, fill=palette["text"]) + + def _refresh_status_palette(self, fg: str) -> None: + self.status.configure(foreground=fg) + self._status_palette["fg"] = fg + def _init_copy_menu(self): self._copy_target = None self.copy_menu = tk.Menu(self.root, tearoff=0) diff --git a/app/logic/constants.py b/app/logic/constants.py index 3657339..76c4b47 100644 --- a/app/logic/constants.py +++ b/app/logic/constants.py @@ -14,7 +14,7 @@ except ModuleNotFoundError: # pragma: no cover - fallback for <3.11 if "tomllib" not in globals(): tomllib = None # type: ignore[assignment] -PREVIEW_MAX_SIZE = (1200, 800) +PREVIEW_MAX_SIZE = (900, 660) BASE_DIR = Path(__file__).resolve().parents[2] IMAGES_DIR = BASE_DIR / "images" diff --git a/app/logic/image_processing.py b/app/logic/image_processing.py index ed257e9..c15a906 100644 --- a/app/logic/image_processing.py +++ b/app/logic/image_processing.py @@ -40,9 +40,12 @@ class ImageProcessingMixin: self.orig_img = image self.prepare_preview() self.update_preview() - self.status.config( - text=f"Geladen: {self.image_path.name} — {self.orig_img.width}x{self.orig_img.height}" - ) + dimensions = f"{self.orig_img.width}x{self.orig_img.height}" + status_text = f"Geladen: {self.image_path.name} — {dimensions}" + self.status.config(text=status_text) + self.status_default_text = status_text + if hasattr(self, "filename_label"): + self.filename_label.config(text=f"{self.image_path.name} — {dimensions}") def save_overlay(self) -> None: if self.orig_img is None: @@ -57,7 +60,7 @@ class ImageProcessingMixin: self.exclude_rects, alpha=int(self.alpha.get()), scale_from_preview=self.preview_img.size, - is_purple_fn=self.is_purple_pixel, + is_match_fn=self.matches_target_color, ) merged = Image.alpha_composite(self.orig_img.convert("RGBA"), overlay) @@ -100,18 +103,18 @@ class ImageProcessingMixin: stats = self.compute_stats_preview() if stats: - p_all, t_all = stats["all"] - p_keep, t_keep = stats["keep"] - p_ex, t_ex = stats["excl"] - r_with = (p_keep / t_keep * 100) if t_keep else 0.0 - r_no = (p_all / t_all * 100) if t_all else 0.0 - excl_share = (t_ex / t_all * 100) if t_all else 0.0 - excl_purp = (p_ex / t_ex * 100) if t_ex else 0.0 + matches_all, total_all = stats["all"] + matches_keep, total_keep = stats["keep"] + matches_ex, total_ex = stats["excl"] + r_with = (matches_keep / total_keep * 100) if total_keep else 0.0 + r_no = (matches_all / total_all * 100) if total_all else 0.0 + excl_share = (total_ex / total_all * 100) if total_all else 0.0 + excl_match = (matches_ex / total_ex * 100) if total_ex else 0.0 self.ratio_label.config( text=( - f"Purple (mit Excludes): {r_with:.2f}% | " - f"Purple (ohne Excludes): {r_no:.2f}% | " - f"Excluded: {excl_share:.2f}% vom Bild, davon {excl_purp:.2f}% purple" + f"Treffer (mit Excludes): {r_with:.2f}% | " + f"Treffer (ohne Excludes): {r_no:.2f}% | " + f"Excluded: {excl_share:.2f}% vom Bild, davon {excl_match:.2f}% Treffer" ) ) @@ -135,7 +138,7 @@ class ImageProcessingMixin: r, g, b, a = pixels[x, y] if a == 0: continue - if self.is_purple_pixel(r, g, b): + if self.matches_target_color(r, g, b): draw.point((x, y), fill=(255, 0, 0, alpha)) merged = Image.alpha_composite(base, overlay) for (x0, y0, x1, y1) in self.exclude_rects: @@ -147,9 +150,9 @@ class ImageProcessingMixin: return None px = self.preview_img.convert("RGBA").load() width, height = self.preview_img.size - purple_all = total_all = 0 - purple_keep = total_keep = 0 - purple_excl = total_excl = 0 + matches_all = total_all = 0 + matches_keep = total_keep = 0 + matches_excl = total_excl = 0 for y in range(height): for x in range(width): r, g, b, a = px[x, y] @@ -157,19 +160,23 @@ class ImageProcessingMixin: continue is_excluded = self._is_excluded(x, y) total_all += 1 - if self.is_purple_pixel(r, g, b): - purple_all += 1 + if self.matches_target_color(r, g, b): + matches_all += 1 if not is_excluded: total_keep += 1 - if self.is_purple_pixel(r, g, b): - purple_keep += 1 + if self.matches_target_color(r, g, b): + matches_keep += 1 else: total_excl += 1 - if self.is_purple_pixel(r, g, b): - purple_excl += 1 - return {"all": (purple_all, total_all), "keep": (purple_keep, total_keep), "excl": (purple_excl, total_excl)} + if self.matches_target_color(r, g, b): + matches_excl += 1 + return { + "all": (matches_all, total_all), + "keep": (matches_keep, total_keep), + "excl": (matches_excl, total_excl), + } - def is_purple_pixel(self, r, g, b) -> bool: + def matches_target_color(self, r, g, b) -> bool: h, s, v = colorsys.rgb_to_hsv(r / 255.0, g / 255.0, b / 255.0) hue = h * 360.0 hmin = float(self.hue_min.get()) @@ -209,7 +216,7 @@ class ImageProcessingMixin: *, alpha: int, scale_from_preview: Tuple[int, int], - is_purple_fn, + is_match_fn, ) -> Image.Image: overlay = Image.new("RGBA", image.size, (0, 0, 0, 0)) draw = ImageDraw.Draw(overlay) @@ -223,7 +230,7 @@ class ImageProcessingMixin: r, g, b, a = pixels[x, y] if a == 0: continue - if is_purple_fn(r, g, b): + if is_match_fn(r, g, b): draw.point((x, y), fill=(255, 0, 0, alpha)) return overlay diff --git a/app/logic/reset.py b/app/logic/reset.py index 45e849e..6b6275b 100644 --- a/app/logic/reset.py +++ b/app/logic/reset.py @@ -12,6 +12,9 @@ class ResetMixin: self.val_max.set(self.DEFAULTS["val_max"]) self.alpha.set(self.DEFAULTS["alpha"]) self.update_preview() + default_text = getattr(self, "status_default_text", "Standardwerte aktiv.") + if hasattr(self, "status"): + self.status.config(text=default_text) __all__ = ["ResetMixin"]