ICRA/app/gui/color_picker.py

142 lines
5.0 KiB
Python

"""Color selection utilities."""
from __future__ import annotations
import colorsys
from tkinter import colorchooser, messagebox
class ColorPickerMixin:
"""Handles colour selection from dialogs and mouse clicks."""
ref_hue: float | None
hue_span: float = 45.0 # degrees around the picked hue
selected_colour: tuple[int, int, int] | None = None
def choose_color(self):
rgb, hex_colour = colorchooser.askcolor(title="Farbe wählen")
if rgb is None:
return
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}%"
)
self._update_selected_colour(r, g, b)
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}%"
)
)
self._update_selected_colour(*rgb)
def enable_pick_mode(self):
if self.preview_img is None:
messagebox.showinfo("Info", "Bitte zuerst ein Bild laden.")
return
self.pick_mode = True
self.status.config(text="Pick-Modus: Klicke links ins Bild, um Farbe zu wählen (Esc beendet)")
def disable_pick_mode(self, event=None):
if self.pick_mode:
self.pick_mode = False
self.status.config(text="Pick-Modus beendet.")
def on_canvas_click(self, event):
if not self.pick_mode or self.preview_img is None:
return
x = int(event.x)
y = int(event.y)
if x < 0 or y < 0 or x >= self.preview_img.width or y >= self.preview_img.height:
return
r, g, b, a = self.preview_img.getpixel((x, y))
if a == 0:
return
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}%"
)
self._update_selected_colour(r, g, b)
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()
return hue_deg, s * 100.0, v * 100.0
def _update_selected_colour(self, r: int, g: int, b: int) -> None:
self.selected_colour = (r, g, b)
hex_colour = f"#{r:02x}{g:02x}{b:02x}"
if hasattr(self, "current_colour_sw"):
try:
self.current_colour_sw.configure(background=hex_colour)
except Exception:
pass
if hasattr(self, "current_colour_label"):
try:
self.current_colour_label.configure(text=f"Aktuelle Farbe ({hex_colour})")
except Exception:
pass
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"]