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>
This commit is contained in:
@@ -1,11 +1,10 @@
|
||||
"""Onglet « Utilisation » de la GUI V6.
|
||||
"""Onglet « Utilisation » de la GUI V6 (G4 — alignement maquette).
|
||||
|
||||
Sélection fichier/dossier → choix sortie → lancement via le runner (dans un
|
||||
thread) → progression / journal / résumé. Aucun appel réseau au démarrage,
|
||||
aucune logique de détection : tout passe par :class:`ProcessingRunner`.
|
||||
|
||||
Les widgets ne sont créés qu'à l'instanciation (import sûr pour ``--self-test``).
|
||||
La communication thread worker → UI passe par une file drainée via ``after``.
|
||||
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
|
||||
@@ -17,8 +16,12 @@ 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__(
|
||||
@@ -27,28 +30,118 @@ class UsageTab(ctk.CTkFrame):
|
||||
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,
|
||||
):
|
||||
super().__init__(master, **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._worker: threading.Thread | None = None
|
||||
self._is_running = False
|
||||
self._events: "queue.Queue[tuple]" = queue.Queue()
|
||||
|
||||
self._build()
|
||||
self.after(120, self._drain_events)
|
||||
|
||||
def _build_run_runner(self) -> tuple[ProcessingRunner, Path | None]:
|
||||
"""Runner d'exécution + dossier sortie selon la configuration courante.
|
||||
# -- construction UI --------------------------------------------------
|
||||
|
||||
Si une configuration est fournie, câble le moteur réel via engine_bridge ;
|
||||
sinon retombe sur le runner par défaut (process_document direct).
|
||||
"""
|
||||
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
|
||||
@@ -59,47 +152,6 @@ class UsageTab(ctk.CTkFrame):
|
||||
output_dir = self._output_dir or getattr(cfg, "output_dir", None)
|
||||
return runner, output_dir
|
||||
|
||||
# -- construction UI --------------------------------------------------
|
||||
|
||||
def _build(self) -> None:
|
||||
bar = ctk.CTkFrame(self)
|
||||
bar.pack(fill="x", padx=12, pady=(12, 6))
|
||||
ctk.CTkButton(bar, text="Choisir un fichier…", command=self._pick_file).pack(
|
||||
side="left", padx=(0, 8)
|
||||
)
|
||||
ctk.CTkButton(bar, text="Choisir un dossier…", command=self._pick_folder).pack(
|
||||
side="left"
|
||||
)
|
||||
|
||||
self._target_label = ctk.CTkLabel(self, text="Aucune source sélectionnée", anchor="w")
|
||||
self._target_label.pack(fill="x", padx=12, pady=(0, 4))
|
||||
|
||||
out_bar = ctk.CTkFrame(self)
|
||||
out_bar.pack(fill="x", padx=12, pady=(0, 6))
|
||||
ctk.CTkButton(out_bar, text="Dossier de sortie…", command=self._pick_output).pack(
|
||||
side="left", padx=(0, 8)
|
||||
)
|
||||
self._output_label = ctk.CTkLabel(out_bar, text="Sortie : (défaut anonymise/)", anchor="w")
|
||||
self._output_label.pack(side="left")
|
||||
|
||||
action = ctk.CTkFrame(self)
|
||||
action.pack(fill="x", padx=12, pady=(0, 6))
|
||||
self._run_btn = ctk.CTkButton(action, text="Lancer", command=self._start, state="disabled")
|
||||
self._run_btn.pack(side="left", padx=(0, 8))
|
||||
self._stop_btn = ctk.CTkButton(action, text="Arrêter", command=self._request_stop, state="disabled")
|
||||
self._stop_btn.pack(side="left")
|
||||
|
||||
self._progress = ctk.CTkProgressBar(self)
|
||||
self._progress.set(0.0)
|
||||
self._progress.pack(fill="x", padx=12, pady=(6, 4))
|
||||
|
||||
self._status_label = ctk.CTkLabel(self, text="Prêt.", anchor="w")
|
||||
self._status_label.pack(fill="x", padx=12)
|
||||
|
||||
self._log = ctk.CTkTextbox(self, height=180)
|
||||
self._log.pack(fill="both", expand=True, padx=12, pady=(6, 12))
|
||||
self._log.configure(state="disabled")
|
||||
|
||||
# -- sélection --------------------------------------------------------
|
||||
|
||||
def _pick_file(self) -> None:
|
||||
@@ -112,20 +164,18 @@ class UsageTab(ctk.CTkFrame):
|
||||
if path:
|
||||
self._set_input(Path(path))
|
||||
|
||||
def _pick_output(self) -> None:
|
||||
path = filedialog.askdirectory(title="Dossier de sortie")
|
||||
if path:
|
||||
self._output_dir = Path(path)
|
||||
self._output_label.configure(text=f"Sortie : {self._output_dir}")
|
||||
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"Dossier : {path} · {count} document(s) détecté(s)")
|
||||
self._target_label.configure(text=f"📁 {path} · {count} document(s) détecté(s)")
|
||||
else:
|
||||
self._target_label.configure(text=f"Fichier : {path.name}")
|
||||
self._output_label.configure(text=f"Sortie : (défaut {default_output_dir(path)})")
|
||||
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 --------------------------------------------------------
|
||||
@@ -137,6 +187,7 @@ class UsageTab(ctk.CTkFrame):
|
||||
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()
|
||||
@@ -202,9 +253,22 @@ class UsageTab(ctk.CTkFrame):
|
||||
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._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 --------------------------------------------------
|
||||
|
||||
|
||||
Reference in New Issue
Block a user