Suite des retours Dom sur la GUI V6 (par-dessus 6a0a581).
Addendum Profils / Réglages :
- Nouveau sous-onglet Administration « 👤 Profils » : le profil actif devient
un objet lisible (nom, description, masque requis, template, listes locales
avec compteurs) — données réelles lues depuis profile_defaults.
- Fenêtre « Tableau des termes » (terms_table_window.py) : table scrollable
avec recherche/filtre, colonnes Type/Terme/Source ; reste lisible à 50+
termes. Ajouter/éditer/supprimer désactivés « (à venir) » (écriture par
profil non câblée).
- Réglages : « Profil métier » → « Profil d'anonymisation », « Sortie… » →
« Dossier de sortie… » (+ infobulle), hints moteurs (standard/optionnel/
plus lent), bouton « Voir le profil », « Ouvrir le tableau des termes ».
- Aide « ? » + infobulles (ui_kit.attach_tooltip) près des éléments ambigus.
- profile_view.py : logique pure (résumé profil + lignes du tableau),
testable sans display.
Addendum bêta : en-tête « aivanonym » + badge « bêta », titre fenêtre
« … — bêta ». Détail version conservé dans À propos.
tests/unit/test_gui_v6_profiles.py + ajouts shell. 237 tests unit OK
(228 → 237, 0 régression), self-test GUI V6 OK, navigation des 5 sous-onglets
+ thème OK. V5/moteur/app_aivanov/profile_defaults non touchés, 0 dépendance.
Aucun build/push sans GO Dom — validation visuelle Dom attendue.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
302 lines
9.4 KiB
Python
302 lines
9.4 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, **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)
|
|
|
|
|
|
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)
|