"""Composants UI stylés de la GUI V6 (G4), alignés sur la maquette. Chaque helper reçoit une ``palette`` (cf. theme.py) et applique les couleurs correspondantes à des widgets customtkinter. Aucune logique métier ici. Les widgets ne sont créés qu'à l'appel (import sûr pour ``--self-test``). """ from __future__ import annotations from typing import Optional import customtkinter as ctk CARD_RADIUS = 8 def font(size: int = 13, weight: str = "normal") -> "ctk.CTkFont": return ctk.CTkFont(size=size, weight=weight) class Card(ctk.CTkFrame): """Carte maquette : fond `card`, bordure `card_border`, titre uppercase optionnel.""" def __init__(self, master, palette: dict, title: Optional[str] = None, **kwargs): super().__init__( master, fg_color=palette["card"], border_color=palette["card_border"], border_width=1, corner_radius=CARD_RADIUS, **kwargs, ) self._palette = palette self.body = self # alias pour clarté if title: ctk.CTkLabel( self, text=title.upper(), text_color=palette["text_dim"], font=font(11, "bold"), anchor="w", ).pack(anchor="w", padx=16, pady=(14, 8)) def primary_button(master, palette: dict, text: str, command=None, large: bool = False): return ctk.CTkButton( master, text=text, command=command, fg_color=palette["primary"], hover_color=palette["primary_dim"], text_color="#ffffff", corner_radius=CARD_RADIUS, height=38 if large else 32, font=font(14 if large else 13, "bold"), ) def secondary_button(master, palette: dict, text: str, command=None): return ctk.CTkButton( master, text=text, command=command, fg_color=palette["btn_sec_bg"], hover_color=palette["card_border"], text_color=palette["text"], border_color=palette["btn_sec_border"], border_width=1, corner_radius=CARD_RADIUS, height=32, font=font(13), ) def success_button(master, palette: dict, text: str, command=None): return ctk.CTkButton( master, text=text, command=command, fg_color=palette["success"], hover_color=palette["success"], text_color="#ffffff", corner_radius=CARD_RADIUS, height=32, font=font(13, "bold"), ) def pill_button(master, palette: dict, text: str, command=None, active: bool = False): """Bouton pilule (sélecteur de thème / sous-onglet).""" return ctk.CTkButton( master, text=text, command=command, fg_color=palette["primary"] if active else "transparent", hover_color=palette["primary_dim"], text_color="#ffffff" if active else palette["text_dim"], border_color=palette["card_border"] if not active else palette["primary"], border_width=2, corner_radius=99, height=30, font=font(12, "bold" if active else "normal"), ) class StatCard(ctk.CTkFrame): """Carte statistique (rgrid/sc) : grande valeur + label.""" def __init__(self, master, palette: dict, value: str, label: str, value_color: Optional[str] = None, **kwargs): super().__init__( master, fg_color=palette["btn_sec_bg"], border_color=palette["btn_sec_border"], border_width=1, corner_radius=CARD_RADIUS, **kwargs, ) ctk.CTkLabel( self, text=value, text_color=value_color or palette["primary"], font=font(22, "bold") ).pack(pady=(10, 0)) ctk.CTkLabel(self, text=label, text_color=palette["text_muted"], font=font(11)).pack( pady=(0, 10) ) class ToggleRow(ctk.CTkFrame): """Ligne de réglage (srow) : libellé + indice + interrupteur.""" def __init__(self, master, palette: dict, label: str, hint: str = "", value: bool = True, command=None, **kwargs): super().__init__(master, fg_color="transparent", **kwargs) left = ctk.CTkFrame(self, fg_color="transparent") left.pack(side="left", fill="x", expand=True) ctk.CTkLabel(left, text=label, text_color=palette["text"], font=font(13), anchor="w").pack(anchor="w") if hint: ctk.CTkLabel(left, text=hint, text_color=palette["text_muted"], font=font(11), anchor="w").pack(anchor="w") self.var = ctk.BooleanVar(value=value) self.switch = ctk.CTkSwitch( self, text="", variable=self.var, command=command, progress_color=palette["primary"], width=44, ) self.switch.pack(side="right", padx=(8, 0)) def get(self) -> bool: return bool(self.var.get()) class HelpButton(ctk.CTkButton): """Petit bouton « ? » ouvrant une fenêtre d'aide en français simple. Restaure l'affordance d'aide de la V5 (``ToolTip`` / « Comment ça marche ? ») pour les utilisateurs non informaticiens. """ def __init__(self, master, palette: dict, text: str, *, title: str = "Aide", **kwargs): self._palette = palette self._help_text = text self._help_title = title self._window = None super().__init__( master, text="?", command=self.open_help, width=26, height=26, corner_radius=13, fg_color=palette["btn_sec_bg"], hover_color=palette["card_border"], text_color=palette["text_dim"], border_color=palette["btn_sec_border"], border_width=1, font=font(13, "bold"), **kwargs, ) def open_help(self): if self._window is not None: try: if self._window.winfo_exists(): self._window.lift() self._window.focus_force() return self._window except Exception: pass p = self._palette win = ctk.CTkToplevel(self) win.title(self._help_title) win.geometry("480x380") win.minsize(360, 240) try: win.configure(fg_color=p["bg"]) except Exception: pass ctk.CTkLabel( win, text=self._help_title, text_color=p["text"], font=font(15, "bold"), anchor="w" ).pack(fill="x", padx=16, pady=(14, 4)) box = ctk.CTkScrollableFrame(win, fg_color=p["card"]) box.pack(fill="both", expand=True, padx=12, pady=(0, 8)) ctk.CTkLabel( box, text=self._help_text, text_color=p["text_dim"], font=font(12), justify="left", wraplength=420, anchor="w", ).pack(fill="x", padx=10, pady=10) ctk.CTkButton( win, text="Fermer", command=win.destroy, fg_color=p["btn_sec_bg"], hover_color=p["card_border"], text_color=p["text"], border_color=p["btn_sec_border"], border_width=1, corner_radius=CARD_RADIUS, height=30, ).pack(padx=12, pady=(0, 12)) try: win.transient(self.winfo_toplevel()) win.after(120, lambda: (win.lift(), win.focus_force())) except Exception: pass self._window = win return win def help_button(master, palette: dict, text: str, title: str = "Aide") -> "HelpButton": return HelpButton(master, palette, text, title=title)