159 lines
5.3 KiB
Python
159 lines
5.3 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):
|
|
title = self._t("dialog.choose_colour_title") if hasattr(self, "_t") else "Choose colour"
|
|
rgb, hex_colour = colorchooser.askcolor(title=title)
|
|
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})"
|
|
message = self._t(
|
|
"status.color_selected",
|
|
label=label,
|
|
hue=hue_deg,
|
|
saturation=sat_pct,
|
|
value=val_pct,
|
|
)
|
|
self.status.config(text=message)
|
|
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()
|
|
message = self._t(
|
|
"status.sample_colour",
|
|
label=label,
|
|
hex_code=hex_colour,
|
|
hue=hue_deg,
|
|
saturation=sat_pct,
|
|
value=val_pct,
|
|
)
|
|
self.status.config(text=message)
|
|
self._update_selected_colour(*rgb)
|
|
|
|
def enable_pick_mode(self):
|
|
if self.preview_img is None:
|
|
messagebox.showinfo(
|
|
self._t("dialog.info_title"),
|
|
self._t("dialog.load_image_first"),
|
|
)
|
|
return
|
|
self.pick_mode = True
|
|
self.status.config(text=self._t("status.pick_mode_ready"))
|
|
|
|
def disable_pick_mode(self, event=None):
|
|
if self.pick_mode:
|
|
self.pick_mode = False
|
|
self.status.config(text=self._t("status.pick_mode_ended"))
|
|
|
|
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=self._t(
|
|
"status.pick_mode_from_image",
|
|
hue=hue_deg,
|
|
saturation=sat_pct,
|
|
value=val_pct,
|
|
)
|
|
)
|
|
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"({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"]
|