152 lines
5.8 KiB
Python
152 lines
5.8 KiB
Python
"""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("<Button-1>", self.on_canvas_click)
|
||
self.canvas_orig.bind("<ButtonPress-3>", self._exclude_start)
|
||
self.canvas_orig.bind("<B3-Motion>", self._exclude_drag)
|
||
self.canvas_orig.bind("<ButtonRelease-3>", 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("<Escape>", 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("<Button-3>", lambda event, w=widget: self._show_copy_menu(event, w))
|
||
widget.bind("<Control-c>", 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"]
|