Files
anonymisation/gui_v6/app.py
Domi31tls 6a0a5811a5 fix(gui): retours Dom GUI V6 — thème, Administration, Règles, aide
Cinq retours utilisateur sur l'exécutable Windows GUI V6.

- Thème : `_render()` vidait les widgets mais conservait le cache
  `_tab_frames`/`_visible_tab` → l'onglet Utilisation se vidait (TclError
  sur widget détruit) au changement de thème. Reset du cache dans
  `_render()` → onglet actif recréé proprement.
- Onglet principal « Configuration » → « Administration » (clé interne
  inchangée).
- Sous-onglet « Règles  2 » → « Règles » (le « 2 » était un badge non
  câblé).
- Actions de maquette non câblées (Partage Export/Import, Règles Nouvelle
  règle/Recharger/Tester/Fermer) désactivées + suffixe « (à venir) » via
  `_mockup_button` : plus aucune action morte qui semble fonctionner.
- Aide « ? » restaurée (façon V5) : `ui_kit.HelpButton`/`help_button`
  réutilisable ouvrant une fenêtre d'aide en français simple, posée sur
  Utilisation, Administration (Réglages/Masquage/Partage/Règles) et
  À propos. Partage : phrase visible + aide expliquant qu'on partage les
  réglages, jamais les documents patients.

`tests/unit/test_gui_v6_app_shell.py` : régression thème, libellés,
présence d'aide, navigation. 228 tests unit OK (0 régression), self-test
GUI V6 OK. V5/moteur/app_aivanov non touchés, aucune dépendance ajoutée.
Verdict Qwen requis avant push/build/diffusion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 16:39:53 +02:00

171 lines
6.1 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")
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")
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 _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