"""Onglet « Utilisation » de la GUI V6 (G4 — alignement maquette). Reprend la structure de ``docs/ui_mockup_v6.html`` : carte Apparence (sélecteur de thème), carte Documents (dropzone + liste), carte Format, barre d'actions, carte progression (étapes), carte résultats (cartes statistiques). La logique (runner moteur câblé, threading, file d'événements, arrêt coopératif, anti double-lancement) est conservée. Aucune logique de détection ici. """ from __future__ import annotations import queue import threading from pathlib import Path 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.processing_runner import ProcessingRunner, default_output_dir _STEPS = ["📖 Extraction", "🧠 Détection", "🔒 Masquage", "📄 PDF final"] class UsageTab(ctk.CTkFrame): def __init__( self, master, runner: ProcessingRunner | None = None, config_provider=None, config_path: Path | None = None, palette: dict | None = None, on_theme_change=None, current_theme: str = theme_mod.DEFAULT_THEME, **kwargs, ): self._p = palette or theme_mod.get_palette(current_theme) super().__init__(master, fg_color=self._p["bg"], **kwargs) self._runner = runner or ProcessingRunner() self._config_provider = config_provider self._config_path = config_path self._on_theme_change = on_theme_change self._current_theme = current_theme self._input_path: Path | None = None self._output_dir: Path | None = None self._stop_event: threading.Event | None = None self._is_running = False self._events: "queue.Queue[tuple]" = queue.Queue() self._build() self.after(120, self._drain_events) # -- construction UI -------------------------------------------------- def _build(self) -> None: p = self._p # Carte Apparence (sélecteur de thème) appearance = ui_kit.Card(self, p, title="🎨 Apparence") appearance.pack(fill="x", padx=14, pady=(14, 7)) row = ctk.CTkFrame(appearance, fg_color="transparent") row.pack(fill="x", padx=16, pady=(0, 14)) for name in theme_mod.theme_names(): ui_kit.pill_button( row, p, theme_mod.THEME_LABELS.get(name, name), command=lambda n=name: self._change_theme(n), active=(name == self._current_theme), ).pack(side="left", padx=(0, 8)) # Carte Documents + dropzone docs = ui_kit.Card(self, p, title="📁 Documents à anonymiser") docs.pack(fill="x", padx=14, pady=7) dz = ctk.CTkFrame( docs, fg_color=p["divider"], border_color=p["card_border"], border_width=2, corner_radius=8 ) dz.pack(fill="x", padx=16, pady=(0, 8)) ctk.CTkLabel(dz, text="⬆️", font=ui_kit.font(30)).pack(pady=(20, 4)) ctk.CTkLabel(dz, text="Choisissez vos fichiers", text_color=p["text"], font=ui_kit.font(14)).pack() ctk.CTkLabel(dz, text="PDF · Word · Images · Texte", text_color=p["text_muted"], font=ui_kit.font(12)).pack(pady=(2, 10)) acts = ctk.CTkFrame(dz, fg_color="transparent") acts.pack(pady=(0, 20)) ui_kit.secondary_button(acts, p, "📄 Fichier", command=self._pick_file).pack(side="left", padx=4) ui_kit.secondary_button(acts, p, "📁 Dossier entier", command=self._pick_folder).pack(side="left", padx=4) self._target_label = ctk.CTkLabel(docs, text="Aucune source sélectionnée", text_color=p["text_muted"], font=ui_kit.font(12), anchor="w") self._target_label.pack(fill="x", padx=16, pady=(0, 14)) # Carte Format de sortie fmt = ui_kit.Card(self, p, title="💾 Format de sortie") fmt.pack(fill="x", padx=14, pady=7) grid = ctk.CTkFrame(fmt, fg_color="transparent") grid.pack(fill="x", padx=16, pady=(0, 14)) for icon, name, sub in [("📄", "PDF anonymisé", "Zones noircies"), ("📝", "Texte .txt", "Mots → [NOM]…")]: fcard = ctk.CTkFrame(grid, fg_color=p["card"], border_color=p["primary"], border_width=2, corner_radius=8) fcard.pack(side="left", expand=True, fill="x", padx=4) ctk.CTkLabel(fcard, text=icon, font=ui_kit.font(20)).pack(pady=(10, 0)) ctk.CTkLabel(fcard, text=name, text_color=p["text"], font=ui_kit.font(13, "bold")).pack() ctk.CTkLabel(fcard, text=sub, text_color=p["text_muted"], font=ui_kit.font(11)).pack(pady=(0, 10)) # Barre d'actions brow = ctk.CTkFrame(self, fg_color="transparent") brow.pack(fill="x", padx=14, pady=7) ui_kit.secondary_button(brow, p, "✖ Effacer", command=self._clear_input).pack(side="right", padx=(8, 0)) self._run_btn = ui_kit.primary_button(brow, p, "▶ Lancer l'anonymisation", command=self._start, large=True) self._run_btn.configure(state="disabled") self._run_btn.pack(side="right") # Carte progression self._psec = ui_kit.Card(self, p, title="⏳ Traitement en cours") self._progress = ctk.CTkProgressBar(self._psec, progress_color=p["primary"]) self._progress.set(0.0) self._progress.pack(fill="x", padx=16, pady=(0, 6)) self._status_label = ctk.CTkLabel(self._psec, text="Prêt.", text_color=p["text_muted"], font=ui_kit.font(12), anchor="w") self._status_label.pack(fill="x", padx=16) steps = ctk.CTkFrame(self._psec, fg_color="transparent") steps.pack(fill="x", padx=16, pady=6) for s in _STEPS: ctk.CTkLabel(steps, text=s, fg_color=p["divider"], text_color=p["text_muted"], corner_radius=99, font=ui_kit.font(11)).pack(side="left", padx=3, ipadx=6, ipady=2) self._log = ctk.CTkTextbox(self._psec, height=110, fg_color=p["divider"], text_color=p["text_dim"], font=ui_kit.font(11), border_color=p["card_border"], border_width=1) self._log.pack(fill="x", padx=16, pady=8) self._log.configure(state="disabled") self._stop_btn = ui_kit.secondary_button(self._psec, p, "⏹ Arrêter", command=self._request_stop) self._stop_btn.configure(state="disabled") self._stop_btn.pack(anchor="e", padx=16, pady=(0, 14)) # masquée tant qu'aucun run # Carte résultats (cartes statistiques) self._rsec = ui_kit.Card(self, p, title="✅ Résultats") self._stats_row = ctk.CTkFrame(self._rsec, fg_color="transparent") self._stats_row.pack(fill="x", padx=16, pady=(0, 14)) self._result_built = False # -- thème ------------------------------------------------------------ def _change_theme(self, name: str) -> None: if self._on_theme_change is not None: self._on_theme_change(name) # -- runner câblé ----------------------------------------------------- def _build_run_runner(self) -> tuple[ProcessingRunner, Path | None]: if self._config_provider is None: return self._runner, self._output_dir from gui_v6.engine_bridge import make_process_fn cfg = self._config_provider() settings = cfg.to_engine_settings(self._config_path) runner = ProcessingRunner(process_fn=make_process_fn(settings)) output_dir = self._output_dir or getattr(cfg, "output_dir", None) return runner, output_dir # -- sélection -------------------------------------------------------- def _pick_file(self) -> None: path = filedialog.askopenfilename(title="Choisir un document") if path: self._set_input(Path(path)) def _pick_folder(self) -> None: path = filedialog.askdirectory(title="Choisir un dossier") if path: self._set_input(Path(path)) def _clear_input(self) -> None: self._input_path = None self._target_label.configure(text="Aucune source sélectionnée") self._run_btn.configure(state="disabled") def _set_input(self, path: Path) -> None: self._input_path = path count = len(self._runner.discover(path)) if path.is_dir(): self._target_label.configure(text=f"📁 {path} · {count} document(s) détecté(s)") else: self._target_label.configure(text=f"📄 {path.name} · sortie {default_output_dir(path)}") self._run_btn.configure(state="normal" if count > 0 else "disabled") # -- exécution -------------------------------------------------------- def _start(self) -> None: if self._input_path is None or self._is_running: return self._is_running = True run_runner, run_output_dir = self._build_run_runner() self._stop_event = threading.Event() self._run_btn.configure(state="disabled") self._psec.pack(fill="x", padx=14, pady=7) self._stop_btn.configure(state="normal") self._progress.set(0.0) self._clear_log() self._set_status("Traitement en cours…") input_path, output_dir, stop = self._input_path, run_output_dir, self._stop_event def work() -> None: try: summary = run_runner.run( input_path, output_dir, on_progress=lambda done, total, name: self._events.put(("progress", done, total, name)), on_log=lambda msg: self._events.put(("log", msg)), stop_event=stop, ) self._events.put(("done", summary)) except Exception as exc: # garde-fou : ne jamais laisser le thread tuer l'UI self._events.put(("error", str(exc))) self._worker = threading.Thread(target=work, daemon=True) self._worker.start() def _request_stop(self) -> None: if self._stop_event is not None: self._stop_event.set() self._set_status("Arrêt demandé…") self._stop_btn.configure(state="disabled") # -- file d'événements worker → UI ------------------------------------ def _drain_events(self) -> None: try: while True: event = self._events.get_nowait() self._handle_event(event) except queue.Empty: pass self.after(120, self._drain_events) def _handle_event(self, event: tuple) -> None: kind = event[0] if kind == "progress": _, done, total, name = event self._progress.set(done / total if total else 0.0) self._set_status(f"{done}/{total} — {name}") elif kind == "log": self._append_log(event[1]) elif kind == "done": self._finish(event[1]) elif kind == "error": self._append_log(f"Erreur : {event[1]}") self._finish(None) def _finish(self, summary) -> None: self._is_running = False self._stop_btn.configure(state="disabled") self._run_btn.configure(state="normal") if summary is None: self._set_status("Terminé avec erreur.") return if summary.stopped: self._set_status(f"Arrêté : {summary.succeeded}/{summary.total} traités.") else: self._progress.set(1.0) self._set_status(f"Terminé : {summary.succeeded} OK, {summary.failed} échec(s) sur {summary.total}.") self._show_results(summary) def _show_results(self, summary) -> None: p = self._p for w in self._stats_row.winfo_children(): w.destroy() cards = [ (str(summary.total), "Documents", p["primary"]), (str(summary.succeeded), "Réussis", p["success"]), (str(summary.failed), "Échecs", p["danger"] if summary.failed else p["text_muted"]), ("OK" if summary.ok else "KO", "Statut", p["success"] if summary.ok else p["warning"]), ] for value, label, color in cards: ui_kit.StatCard(self._stats_row, p, value, label, value_color=color).pack(side="left", expand=True, fill="x", padx=4) self._rsec.pack(fill="x", padx=14, pady=(7, 14)) # -- helpers widgets -------------------------------------------------- def _set_status(self, text: str) -> None: self._status_label.configure(text=text) def _clear_log(self) -> None: self._log.configure(state="normal") self._log.delete("1.0", "end") self._log.configure(state="disabled") def _append_log(self, message: str) -> None: self._log.configure(state="normal") self._log.insert("end", message + "\n") self._log.see("end") self._log.configure(state="disabled")