Files
anonymisation/gui_v6/ui_kit.py
Domi31tls 60fb41c2e7 fix(gui): clarifier aide et disponibilite moteurs
Passe theme clair, libelles utilisateur, aides conteneurs, recherche de mise a jour et indication honnete des moteurs optionnels non embarques. Tests GUI unitaires: 126 passed.
2026-06-17 18:01:25 +02:00

319 lines
10 KiB
Python

"""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
import tkinter as tk
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,
help_text: Optional[str] = None,
help_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:
header = ctk.CTkFrame(self, fg_color="transparent")
header.pack(fill="x", padx=16, pady=(14, 8))
ctk.CTkLabel(
header,
text=title.upper(),
text_color=palette["text_dim"],
font=font(11, "bold"),
anchor="w",
).pack(side="left", fill="x", expand=True)
if help_text:
HelpButton(
header,
palette,
help_text,
title=help_title or title.lstrip("⚙️👤🏷️⬛🧠📝🛡️🔍✅📤📥 ").strip() or "Aide",
).pack(side="right", padx=(8, 0))
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)
class Tooltip:
"""Infobulle au survol (façon V5 ``ToolTip``), pour les éléments ambigus."""
def __init__(self, widget, text: str, delay: int = 450):
self.widget = widget
self.text = text
self.delay = delay
self._tip = None
self._after = None
widget.bind("<Enter>", self._schedule, add="+")
widget.bind("<Leave>", self.hide, add="+")
widget.bind("<ButtonPress>", self.hide, add="+")
def _schedule(self, *_):
self._cancel()
try:
self._after = self.widget.after(self.delay, self.show)
except Exception:
pass
def _cancel(self):
if self._after is not None:
try:
self.widget.after_cancel(self._after)
except Exception:
pass
self._after = None
def show(self, *_):
if self._tip is not None or not self.text:
return self._tip
try:
x = self.widget.winfo_rootx() + 16
y = self.widget.winfo_rooty() + self.widget.winfo_height() + 4
except Exception:
return None
self._tip = tw = tk.Toplevel(self.widget)
tw.wm_overrideredirect(True)
tw.wm_geometry(f"+{x}+{y}")
tk.Label(
tw,
text=self.text,
justify="left",
background="#1f2937",
foreground="#f9fafb",
relief="solid",
borderwidth=1,
wraplength=320,
padx=8,
pady=5,
).pack()
return tw
def hide(self, *_):
self._cancel()
if self._tip is not None:
try:
self._tip.destroy()
except Exception:
pass
self._tip = None
def attach_tooltip(widget, text: str, delay: int = 450) -> "Tooltip":
return Tooltip(widget, text, delay)