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