Files
anonymisation/gui_v6/app.py
Domi31tls a9e8b2c2e6 feat(gui): addenda Dom GUI V6 — sous-onglet Profils, libellés, aide, bêta
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>
2026-06-15 17:02:54 +02:00

181 lines
6.4 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 panneaux mis en cache après leur
première ouverture visible, 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", "⚙️ Administration"),
("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._tab_frames: dict = {}
self._visible_tab = None
self.title("Pseudonymisation de vos documents — bêta")
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()
# Les frames d'onglets mis en cache étaient des enfants détruits ci-dessus :
# on vide le cache pour que ``_show`` recrée proprement l'onglet actif
# (sinon on re-packe un widget mort → onglet vide / TclError au changement de thème).
self._tab_frames = {}
self._visible_tab = None
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")
identity = ctk.CTkFrame(header, fg_color="transparent")
identity.pack(side="left", padx=16, pady=10)
ctk.CTkLabel(
identity, text="🛡️ aivanonym", text_color=p["text"], font=ui_kit.font(18, "bold")
).pack(side="left")
ctk.CTkLabel(
identity,
text="bêta",
text_color="#ffffff",
fg_color=p["primary"],
corner_radius=8,
font=ui_kit.font(10, "bold"),
).pack(side="left", padx=(8, 0), ipadx=6, ipady=1)
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 _create_tab(self, key: str):
p = self._palette
status = self._safe_local_status()
if key == "use":
return UsageTab(
self._content,
palette=p,
config_provider=lambda: self._config,
on_theme_change=self.set_theme,
current_theme=self._theme_name,
)
if key == "cfg":
return ConfigTab(self._content, palette=p, state=self._config)
return AboutTab(
self._content,
palette=p,
status=status,
theme_name=self._theme_name,
license_client=self._license_client,
)
def _show(self, key: str) -> None:
self._active = key
self._refresh_tabbar()
if self._visible_tab is not None:
self._tab_frames[self._visible_tab].pack_forget()
if key not in self._tab_frames:
self._tab_frames[key] = self._create_tab(key)
self._tab_frames[key].pack(fill="both", expand=True)
self._visible_tab = key