"""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