"""UI helpers and reusable Tk callbacks.""" from __future__ import annotations import tkinter as tk from tkinter import ttk from .theme import HAS_TTKBOOTSTRAP class UIBuilderMixin: """Constructs the Tkinter UI and common widgets.""" def setup_ui(self) -> None: toolbar = ttk.Frame(self.root) toolbar.pack(fill=tk.X, padx=12, pady=8) buttons = [ ("📂 Bild laden", self.load_image), ("🎨 Farbe wählen", self.choose_color), ("🖱️Farbe aus Bild klicken", self.enable_pick_mode), ("💾 Overlay speichern", self.save_overlay), ("🧹 Excludes löschen", self.clear_excludes), ("↩️ Letztes Exclude entfernen", self.undo_exclude), ("🔄 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 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) sliders_frame = ttk.Frame(self.root) sliders_frame.pack(fill=tk.X, padx=12, pady=4) sliders = [ ("Hue Min (°)", self.hue_min, 0, 360), ("Hue Max (°)", self.hue_max, 0, 360), ("Sättigung Min (%)", self.sat_min, 0, 100), ("Helligkeit Min (%)", self.val_min, 0, 100), ("Helligkeit Max (%)", self.val_max, 0, 100), ("Overlay Alpha", self.alpha, 0, 255), ] for index, (label, variable, minimum, maximum) in enumerate(sliders): self.add_slider_with_value(sliders_frame, label, variable, minimum, maximum, column=index) sliders_frame.grid_columnconfigure(index, weight=1) 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)) 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)) 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.hint.pack(anchor="w", pady=(2, 0)) self._attach_copy_menu(self.hint) self.root.bind("", self.disable_pick_mode) def add_slider_with_value(self, parent, text, var, minimum, maximum, column=0): cell = ttk.Frame(parent) cell.grid(row=0, column=column, sticky="we", padx=6) header = ttk.Frame(cell) header.pack(fill="x") name_lbl = ttk.Label(header, text=text) name_lbl.pack(side="left") self._attach_copy_menu(name_lbl) val_lbl = ttk.Label(header, text=f"{float(var.get()):.0f}") val_lbl.pack(side="right") self._attach_copy_menu(val_lbl) style_name = getattr(self, "scale_style", "Horizontal.TScale") ttk.Scale( cell, from_=minimum, to=maximum, orient="horizontal", variable=var, style=style_name, command=self.on_slider_change, ).pack(fill="x", pady=(2, 8)) def on_var_change(*_): val_lbl.config(text=f"{float(var.get()):.0f}") try: var.trace_add("write", on_var_change) except Exception: var.trace("w", lambda *_: on_var_change()) # type: ignore[attr-defined] def on_slider_change(self, *_): if self._update_job is not None: try: self.root.after_cancel(self._update_job) except Exception: pass self._update_job = self.root.after(self.update_delay_ms, self.update_preview) def _init_copy_menu(self): self._copy_target = None self.copy_menu = tk.Menu(self.root, tearoff=0) self.copy_menu.add_command(label="Kopieren", command=self._copy_current_label) def _attach_copy_menu(self, widget): widget.bind("", lambda event, w=widget: self._show_copy_menu(event, w)) widget.bind("", lambda event, w=widget: self._copy_widget_text(w)) def _show_copy_menu(self, event, widget): self._copy_target = widget try: self.copy_menu.tk_popup(event.x_root, event.y_root) finally: self.copy_menu.grab_release() def _copy_current_label(self): if self._copy_target is not None: self._copy_widget_text(self._copy_target) def _copy_widget_text(self, widget): try: text = widget.cget("text") except Exception: text = "" if not text: return try: self.root.clipboard_clear() self.root.clipboard_append(text) except Exception: pass __all__ = ["UIBuilderMixin"]