ICRA/app/gui/ui.py

152 lines
5.8 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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"]