Files
anonymisation/gui_v6/app.py
Domi31tls 34c681b791 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>
2026-06-12 12:06:05 +02:00

159 lines
5.5 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Shell de la GUI V6 (G4 — alignement maquette).
Reproduit l'identité de ``docs/ui_mockup_v6.html`` : shell étroit, header avec
identité produit + version + statut licence + liseré accent, barre d'onglets
custom (pas CTkTabview brut), navigation par recréation du contenu, changement
de thème à chaud. La logique (runner moteur, config, licence) est inchangée.
La fenêtre n'est créée qu'à l'instanciation de :class:`AnonymisationApp`.
"""
from __future__ import annotations
from typing import Optional
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
from gui_v6.license_client import LicenseClient, LicenseStatus
from gui_v6.tabs.tab_about import AboutTab
from gui_v6.tabs.tab_config import ConfigTab
from gui_v6.tabs.tab_usage import UsageTab
_TABS = [
("use", "📄 Utilisation"),
("cfg", "⚙️ Configuration"),
("about", " À propos"),
]
class AnonymisationApp(ctk.CTk):
def __init__(
self,
license_client: Optional[LicenseClient] = None,
theme_name: str = theme_mod.DEFAULT_THEME,
) -> None:
super().__init__()
self._theme_name = theme_name
self._license_client = license_client or LicenseClient("http://localhost")
self._config = ConfigState()
self._active = "use"
self._tab_buttons: dict = {}
self.title("Pseudonymisation de vos documents")
self.geometry("820x880")
self.minsize(720, 680)
self._render()
# -- thème / rendu ----------------------------------------------------
def set_theme(self, name: str) -> None:
self._theme_name = name
self._render()
def _render(self) -> None:
self._palette = theme_mod.apply_theme(self._theme_name)
p = self._palette
try:
self.configure(fg_color=p["bg"])
except Exception:
pass
for child in self.winfo_children():
child.destroy()
self._build_header(p)
self._build_tabsbar(p)
self._content = ctk.CTkScrollableFrame(self, fg_color=p["bg"])
self._content.pack(fill="both", expand=True)
self._show(self._active)
# -- header -----------------------------------------------------------
def _safe_local_status(self) -> LicenseStatus:
try:
return self._license_client.local_status()
except Exception:
return LicenseStatus.unavailable()
def _build_header(self, p: dict) -> None:
header = ctk.CTkFrame(self, fg_color=p["card"], corner_radius=0)
header.pack(fill="x")
ctk.CTkLabel(
header, text="🛡️ aivanonym", text_color=p["text"], font=ui_kit.font(18, "bold")
).pack(side="left", padx=16, pady=10)
status = self._safe_local_status()
ctk.CTkLabel(
header,
text=f"licence : {status.status}",
text_color=theme_mod.status_color(self._theme_name, status.status),
font=ui_kit.font(11),
).pack(side="right", padx=(8, 16))
ctk.CTkLabel(
header, text="v6.0", text_color=p["text_muted"], font=ui_kit.font(11)
).pack(side="right", padx=4)
# Liseré accent sous le header (border-bottom 3px primary).
ctk.CTkFrame(self, fg_color=p["primary"], height=3, corner_radius=0).pack(fill="x")
# -- barre d'onglets --------------------------------------------------
def _build_tabsbar(self, p: dict) -> None:
bar = ctk.CTkFrame(self, fg_color=p["card"], corner_radius=0)
bar.pack(fill="x")
self._tab_buttons = {}
for key, label in _TABS:
active = key == self._active
btn = ctk.CTkButton(
bar,
text=label,
command=lambda k=key: self._show(k),
fg_color="transparent",
hover_color=p["card_border"],
text_color=p["primary"] if active else p["text_dim"],
font=ui_kit.font(13, "bold" if active else "normal"),
corner_radius=0,
width=10,
)
btn.pack(side="left", padx=4, pady=4)
self._tab_buttons[key] = btn
def _refresh_tabbar(self) -> None:
p = self._palette
for key, btn in self._tab_buttons.items():
active = key == self._active
btn.configure(
text_color=p["primary"] if active else p["text_dim"],
font=ui_kit.font(13, "bold" if active else "normal"),
)
# -- contenu ----------------------------------------------------------
def _show(self, key: str) -> None:
self._active = key
self._refresh_tabbar()
for child in self._content.winfo_children():
child.destroy()
p = self._palette
status = self._safe_local_status()
if key == "use":
tab = UsageTab(
self._content,
palette=p,
config_provider=lambda: self._config,
on_theme_change=self.set_theme,
current_theme=self._theme_name,
)
elif key == "cfg":
tab = ConfigTab(self._content, palette=p, state=self._config)
else:
tab = AboutTab(
self._content,
palette=p,
status=status,
theme_name=self._theme_name,
license_client=self._license_client,
)
tab.pack(fill="both", expand=True)