feat(gui): GUI V6 G4 — alignement visuel sur la maquette v6 (option A)
Refonte de la couche présentation pour reprendre docs/ui_mockup_v6.html, sans changer de techno UI ni la logique G1-G3. - theme.py : 4 thèmes aux tokens EXACTS de la maquette (sombre #1a1a2e/#16213e/ #e94560, clair, médical, neutre), palette complète + status_color. - ui_kit.py (nouveau) : composants stylés (Card titrée, boutons primary/secondary/ success/pilule, StatCard, ToggleRow) appliquant la palette. - app.py : shell étroit, header identité + version + statut licence + liseré accent, barre d'onglets custom (plus de CTkTabview brut), navigation par recréation, changement de thème à chaud. - tab_usage : carte Apparence (sélecteur de thème), dropzone stylée, grille formats, barre d'actions, progression à étapes + journal, résultats en cartes statistiques. - tab_config : sous-navigation Réglages/Masquage/Partage/Règles ; Réglages câblé au ConfigState (profil, moteurs NER, dossier sortie). - tab_about : grille d'informations + bloc licence (logique inchangée). Logique inchangée : engine_bridge, config_state, license_client/store, runner. Tests : +9 (theme). self-test exit 0, 55 tests gui_v6, 202 tests/unit (0 régression). Smoke construction headless (Xvfb) : 3 onglets × 4 thèmes rendus sans erreur. Pas de pywebview, aucun .exe. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,9 @@
|
||||
"""Onglet « Configuration » de la GUI V6 (G3-B).
|
||||
"""Onglet « Configuration » de la GUI V6 (G4 — alignement maquette).
|
||||
|
||||
Édite un :class:`ConfigState` partagé : profil métier, raster burn, NER local,
|
||||
dossier de sortie. Affiche l'état des managers NER. Les options sensibles ne sont
|
||||
visibles/éditables qu'en mode admin (``admin_mode.is_admin``). Aucune logique de
|
||||
détection : on édite seulement l'état lu par l'onglet Utilisation.
|
||||
|
||||
Les widgets ne sont créés qu'à l'instanciation (import sûr pour ``--self-test``).
|
||||
Sous-navigation Réglages / Masquage / Partage / Règles (cf. maquette). Le
|
||||
sous-onglet Réglages édite un :class:`ConfigState` partagé (profil, NER local,
|
||||
moteurs). Les autres sous-onglets reprennent le style maquette (cartes denses).
|
||||
Aucune logique de détection ; ``config_defaults.py`` n'est pas modifié.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -15,23 +13,20 @@ from tkinter import filedialog
|
||||
|
||||
import customtkinter as ctk
|
||||
|
||||
from gui_v6 import theme as theme_mod
|
||||
from gui_v6 import ui_kit
|
||||
from gui_v6.config_state import ConfigState, default_profile_key, list_profile_keys
|
||||
|
||||
|
||||
def _is_admin() -> bool:
|
||||
try:
|
||||
from admin_mode import is_admin
|
||||
|
||||
return bool(is_admin())
|
||||
except Exception:
|
||||
return False
|
||||
_SUBTABS = [("reg", "⚙️ Réglages"), ("msk", "🎭 Masquage"), ("shr", "🔄 Partage"), ("rul", "🛡️ Règles")]
|
||||
|
||||
|
||||
class ConfigTab(ctk.CTkFrame):
|
||||
def __init__(self, master, state: ConfigState | None = None, **kwargs):
|
||||
super().__init__(master, **kwargs)
|
||||
def __init__(self, master, state: ConfigState | None = None, palette: dict | None = None, **kwargs):
|
||||
self._p = palette or theme_mod.get_palette(theme_mod.DEFAULT_THEME)
|
||||
super().__init__(master, fg_color=self._p["bg"], **kwargs)
|
||||
self._state = state if state is not None else ConfigState()
|
||||
self._admin = _is_admin()
|
||||
self._sub = "reg"
|
||||
self._sub_buttons: dict = {}
|
||||
self._build()
|
||||
|
||||
@property
|
||||
@@ -39,90 +34,137 @@ class ConfigTab(ctk.CTkFrame):
|
||||
return self._state
|
||||
|
||||
def _build(self) -> None:
|
||||
ctk.CTkLabel(
|
||||
self, text="Configuration", font=ctk.CTkFont(size=16, weight="bold")
|
||||
).pack(anchor="w", padx=16, pady=(16, 8))
|
||||
p = self._p
|
||||
bar = ctk.CTkFrame(self, fg_color="transparent")
|
||||
bar.pack(fill="x", padx=14, pady=(14, 4))
|
||||
for key, label in _SUBTABS:
|
||||
btn = ctk.CTkButton(
|
||||
bar, text=label, command=lambda k=key: self._show_sub(k),
|
||||
fg_color="transparent", hover_color=p["card_border"],
|
||||
text_color=p["primary"] if key == self._sub else p["text_dim"],
|
||||
font=ui_kit.font(13, "bold" if key == self._sub else "normal"),
|
||||
corner_radius=0, width=10,
|
||||
)
|
||||
btn.pack(side="left", padx=3)
|
||||
self._sub_buttons[key] = btn
|
||||
|
||||
# Profil métier
|
||||
self._body = ctk.CTkFrame(self, fg_color="transparent")
|
||||
self._body.pack(fill="both", expand=True)
|
||||
self._show_sub("reg")
|
||||
|
||||
def _show_sub(self, key: str) -> None:
|
||||
self._sub = key
|
||||
p = self._p
|
||||
for k, btn in self._sub_buttons.items():
|
||||
btn.configure(text_color=p["primary"] if k == key else p["text_dim"],
|
||||
font=ui_kit.font(13, "bold" if k == key else "normal"))
|
||||
for w in self._body.winfo_children():
|
||||
w.destroy()
|
||||
{"reg": self._build_reglages, "msk": self._build_masquage,
|
||||
"shr": self._build_partage, "rul": self._build_regles}[key]()
|
||||
|
||||
# -- Réglages (câblé ConfigState) -------------------------------------
|
||||
|
||||
def _build_reglages(self) -> None:
|
||||
p = self._p
|
||||
|
||||
# Profil + sortie
|
||||
prof = ui_kit.Card(self._body, p, title="🗂️ Profil métier")
|
||||
prof.pack(fill="x", padx=14, pady=7)
|
||||
row = ctk.CTkFrame(prof, fg_color="transparent")
|
||||
row.pack(fill="x", padx=16, pady=(0, 10))
|
||||
profiles = list_profile_keys()
|
||||
current = self._state.profile or default_profile_key() or (profiles[0] if profiles else "")
|
||||
self._state.profile = current or None
|
||||
row = ctk.CTkFrame(self)
|
||||
row.pack(fill="x", padx=16, pady=4)
|
||||
ctk.CTkLabel(row, text="Profil :").pack(side="left", padx=(0, 8))
|
||||
self._profile_menu = ctk.CTkOptionMenu(
|
||||
row,
|
||||
values=profiles or ["(aucun profil)"],
|
||||
command=self._on_profile,
|
||||
)
|
||||
ctk.CTkLabel(row, text="Profil :", text_color=p["text"], font=ui_kit.font(13)).pack(side="left", padx=(0, 8))
|
||||
menu = ctk.CTkOptionMenu(row, values=profiles or ["(aucun profil)"], command=self._on_profile,
|
||||
fg_color=p["btn_sec_bg"], button_color=p["primary"], text_color=p["text"])
|
||||
if current:
|
||||
self._profile_menu.set(current)
|
||||
self._profile_menu.pack(side="left")
|
||||
menu.set(current)
|
||||
menu.pack(side="left")
|
||||
outrow = ctk.CTkFrame(prof, fg_color="transparent")
|
||||
outrow.pack(fill="x", padx=16, pady=(0, 14))
|
||||
ui_kit.secondary_button(outrow, p, "📁 Dossier de sortie…", command=self._pick_output).pack(side="left", padx=(0, 8))
|
||||
self._out_label = ctk.CTkLabel(outrow, text=str(self._state.output_dir or "(défaut anonymise/)"),
|
||||
text_color=p["text_muted"], font=ui_kit.font(12))
|
||||
self._out_label.pack(side="left")
|
||||
|
||||
# Options simples
|
||||
self._raster = ctk.CTkCheckBox(
|
||||
self, text="Caviardage raster (burn)", command=self._on_raster
|
||||
)
|
||||
self._raster.select() if self._state.raster_burn else self._raster.deselect()
|
||||
self._raster.pack(anchor="w", padx=16, pady=4)
|
||||
# Moteurs NER (câblé)
|
||||
ner = ui_kit.Card(self._body, p, title="🧠 Moteurs NER")
|
||||
ner.pack(fill="x", padx=14, pady=7)
|
||||
self._tog_ner = ui_kit.ToggleRow(ner, p, "CamemBERT-bio ⚡", "~10 ms/doc · F1 = 0.963", value=self._state.use_local_ner, command=self._on_ner)
|
||||
self._tog_ner.pack(fill="x", padx=16, pady=2)
|
||||
self._tog_eds = ui_kit.ToggleRow(ner, p, "EDS-Pseudo", "médical français (optionnel)", value=self._state.enable_eds, command=self._on_eds)
|
||||
self._tog_eds.pack(fill="x", padx=16, pady=2)
|
||||
self._tog_gli = ui_kit.ToggleRow(ner, p, "GLiNER", "vote croisé (optionnel)", value=self._state.enable_gliner, command=self._on_gliner)
|
||||
self._tog_gli.pack(fill="x", padx=16, pady=(2, 14))
|
||||
|
||||
self._ner = ctk.CTkCheckBox(
|
||||
self, text="NER local actif", command=self._on_ner
|
||||
)
|
||||
self._ner.select() if self._state.use_local_ner else self._ner.deselect()
|
||||
self._ner.pack(anchor="w", padx=16, pady=4)
|
||||
# Données à détecter (visuel)
|
||||
det = ui_kit.Card(self._body, p, title="🔍 Données à détecter")
|
||||
det.pack(fill="x", padx=14, pady=7)
|
||||
for label, hint in [
|
||||
("Noms et prénoms", "Gazetteers INSEE · CamemBERT"),
|
||||
("Dates de naissance", "Uniquement la date de naissance"),
|
||||
("Établissements", "Répertoire FINESS + contexte"),
|
||||
("Adresses et codes postaux", ""),
|
||||
("N° sécurité sociale", ""),
|
||||
("Téléphones et e-mails", ""),
|
||||
]:
|
||||
ui_kit.ToggleRow(det, p, label, hint, value=True).pack(fill="x", padx=16, pady=2)
|
||||
ctk.CTkFrame(det, fg_color="transparent", height=8).pack()
|
||||
|
||||
# Dossier de sortie
|
||||
out_row = ctk.CTkFrame(self)
|
||||
out_row.pack(fill="x", padx=16, pady=4)
|
||||
ctk.CTkButton(out_row, text="Dossier de sortie…", command=self._pick_output).pack(
|
||||
side="left", padx=(0, 8)
|
||||
)
|
||||
self._output_label = ctk.CTkLabel(
|
||||
out_row, text=str(self._state.output_dir or "(défaut anonymise/)")
|
||||
)
|
||||
self._output_label.pack(side="left")
|
||||
def _build_masquage(self) -> None:
|
||||
p = self._p
|
||||
card = ui_kit.Card(self._body, p, title="🎭 Masquage")
|
||||
card.pack(fill="x", padx=14, pady=7)
|
||||
ctk.CTkLabel(card, text="Couleur, style des marqueurs et éditeur de masques PDF.",
|
||||
text_color=p["text_dim"], font=ui_kit.font(12), anchor="w", justify="left").pack(anchor="w", padx=16)
|
||||
codes = ui_kit.Card(self._body, p, title="🔒 Codes de remplacement")
|
||||
codes.pack(fill="x", padx=14, pady=7)
|
||||
for k, v in [("Nom/Prénom", "[NOM]"), ("Date naissance", "[DATE_NAISSANCE]"),
|
||||
("Établissement", "[ETABLISSEMENT]"), ("Adresse", "[ADRESSE]"), ("N° sécu", "[NIR]")]:
|
||||
r = ctk.CTkFrame(codes, fg_color="transparent")
|
||||
r.pack(fill="x", padx=16, pady=1)
|
||||
ctk.CTkLabel(r, text=k, text_color=p["text_muted"], font=ui_kit.font(12)).pack(side="left")
|
||||
ctk.CTkLabel(r, text=v, text_color=p["primary"], font=ui_kit.font(12, "bold")).pack(side="right")
|
||||
ctk.CTkFrame(codes, fg_color="transparent", height=8).pack()
|
||||
ctk.CTkLabel(self._body, text="Éditeur de masques PDF (zones fixes) : intégré au lot suivant.",
|
||||
text_color=p["text_muted"], font=ui_kit.font(11)).pack(anchor="w", padx=18, pady=6)
|
||||
|
||||
# Options admin-only
|
||||
if self._admin:
|
||||
ctk.CTkLabel(self, text="Options avancées (admin)").pack(
|
||||
anchor="w", padx=16, pady=(12, 4)
|
||||
)
|
||||
self._gliner = ctk.CTkCheckBox(
|
||||
self, text="GLiNER (vote croisé)", command=self._on_gliner
|
||||
)
|
||||
self._gliner.pack(anchor="w", padx=16, pady=2)
|
||||
self._eds = ctk.CTkCheckBox(
|
||||
self, text="EDS-Pseudo", command=self._on_eds
|
||||
)
|
||||
self._eds.pack(anchor="w", padx=16, pady=2)
|
||||
def _build_partage(self) -> None:
|
||||
p = self._p
|
||||
card = ui_kit.Card(self._body, p, title="🔄 Partage de configuration")
|
||||
card.pack(fill="x", padx=14, pady=7)
|
||||
ctk.CTkLabel(card, text="Exporter / importer la configuration (whitelist, blacklist, profils) par JSON.",
|
||||
text_color=p["text_dim"], font=ui_kit.font(12), anchor="w", justify="left").pack(anchor="w", padx=16, pady=(0, 8))
|
||||
actrow = ctk.CTkFrame(card, fg_color="transparent")
|
||||
actrow.pack(fill="x", padx=16, pady=(0, 14))
|
||||
ui_kit.secondary_button(actrow, p, "⬇ Exporter", command=lambda: None).pack(side="left", padx=(0, 8))
|
||||
ui_kit.secondary_button(actrow, p, "⬆ Importer", command=lambda: None).pack(side="left")
|
||||
|
||||
# État des managers
|
||||
self._managers_label = ctk.CTkLabel(self, text="Managers NER : non chargé", anchor="w")
|
||||
self._managers_label.pack(anchor="w", padx=16, pady=(12, 16))
|
||||
def _build_regles(self) -> None:
|
||||
p = self._p
|
||||
card = ui_kit.Card(self._body, p, title="🛡️ Règles personnalisées")
|
||||
card.pack(fill="x", padx=14, pady=7)
|
||||
ctk.CTkLabel(card, text="Règles système (lecture seule) et règles personnalisées de l'établissement.",
|
||||
text_color=p["text_dim"], font=ui_kit.font(12), anchor="w", justify="left").pack(anchor="w", padx=16, pady=(0, 14))
|
||||
|
||||
# -- callbacks --------------------------------------------------------
|
||||
|
||||
def _on_profile(self, value: str) -> None:
|
||||
self._state.profile = value
|
||||
|
||||
def _on_raster(self) -> None:
|
||||
self._state.raster_burn = bool(self._raster.get())
|
||||
|
||||
def _on_ner(self) -> None:
|
||||
self._state.use_local_ner = bool(self._ner.get())
|
||||
|
||||
def _on_gliner(self) -> None:
|
||||
self._state.enable_gliner = bool(self._gliner.get())
|
||||
self._state.use_local_ner = self._tog_ner.get()
|
||||
|
||||
def _on_eds(self) -> None:
|
||||
self._state.enable_eds = bool(self._eds.get())
|
||||
self._state.enable_eds = self._tog_eds.get()
|
||||
|
||||
def _on_gliner(self) -> None:
|
||||
self._state.enable_gliner = self._tog_gli.get()
|
||||
|
||||
def _pick_output(self) -> None:
|
||||
path = filedialog.askdirectory(title="Dossier de sortie")
|
||||
if path:
|
||||
self._state.output_dir = Path(path)
|
||||
self._output_label.configure(text=str(self._state.output_dir))
|
||||
|
||||
def set_managers_state(self, state_text: str) -> None:
|
||||
self._managers_label.configure(text=f"Managers NER : {state_text}")
|
||||
self._out_label.configure(text=str(self._state.output_dir))
|
||||
|
||||
Reference in New Issue
Block a user