diff --git a/gui_v6/tabs/tab_config.py b/gui_v6/tabs/tab_config.py index 1d60911..b89a725 100644 --- a/gui_v6/tabs/tab_config.py +++ b/gui_v6/tabs/tab_config.py @@ -23,7 +23,6 @@ from manual_masking import ensure_mask_templates_dir, list_mask_templates, mask_ _SUBTABS = [ ("reg", "⚙️ Réglages"), ("pro", "👤 Profils"), - ("msk", "🎭 Masquage"), ("shr", "🔄 Partage"), ("rul", "🛡️ Règles"), ] @@ -38,17 +37,6 @@ _DETECTION_OPTIONS = [ ("N° adhérent mutuelle", "Identifiant local"), ] -_REPLACEMENT_CODES = [ - ("Nom/Prénom", "[NOM]"), - ("Date naissance", "[DATE_NAISSANCE]"), - ("Établissement", "[ETABLISSEMENT]"), - ("Adresse", "[ADRESSE]"), - ("Téléphone", "[TEL]"), - ("N° sécu", "[NIR]"), - ("IPP", "[IPP]"), - ("Email", "[EMAIL]"), -] - _MASK_COLORS = [ ("Noir", "#000000"), ("Bleu nuit", "#1a1a2e"), @@ -68,14 +56,6 @@ _HELP_REGLAGES = ( "• Listes locales : vos termes à toujours masquer ou toujours conserver.\n\n" "Tout fonctionne 100 % en local sur ce poste. Aucun document patient n'est envoyé sur Internet." ) -_HELP_MASQUAGE = ( - "Masquage des documents.\n\n" - "• Couleur, style et marges du masque appliqué sur les PDF.\n" - "• Codes de remplacement affichés à la place des données ([NOM], [DATE_NAISSANCE]…).\n" - "• Masques de zones fixes : ouvrez l'éditeur pour dessiner les zones à masquer " - "(en-têtes, blocs identité) directement sur un PDF modèle, puis enregistrez un modèle réutilisable.\n\n" - "Le traitement reste local ; rien n'est envoyé sur Internet." -) _HELP_PARTAGE = ( "À quoi sert le Partage ?\n\n" "Il permet d'échanger les RÉGLAGES de l'application (listes de termes, règles, " @@ -229,7 +209,6 @@ class ConfigTab(ctk.CTkFrame): builders = { "reg": self._build_reglages, "pro": self._build_profils, - "msk": self._build_masquage, "shr": self._build_partage, "rul": self._build_regles, } @@ -496,7 +475,39 @@ class ConfigTab(ctk.CTkFrame): ctk.CTkLabel(mask, text="Template de masque préféré", text_color=p["text_muted"], font=ui_kit.font(11), anchor="w").pack(fill="x", padx=12, pady=(0, 2)) self._pro_template_var = ctk.StringVar() self._pro_template_entry = ctk.CTkEntry(mask, textvariable=self._pro_template_var, fg_color=p["btn_sec_bg"], border_color=p["btn_sec_border"], text_color=p["text"], height=30) - self._pro_template_entry.pack(fill="x", padx=12, pady=(0, 12)) + self._pro_template_entry.pack(fill="x", padx=12, pady=(0, 6)) + mask_actions = ctk.CTkFrame(mask, fg_color="transparent") + mask_actions.pack(fill="x", padx=12, pady=(0, 6)) + ui_kit.secondary_button(mask_actions, p, "🖊 Ouvrir l'éditeur de masque", command=self._open_full_mask_editor).pack(side="left") + ui_kit.secondary_button(mask_actions, p, "📁 Dossier", command=self._open_templates_dir).pack(side="left", padx=(6, 0)) + + # Apparence du masque (couleur / style / marges) — réglage global appliqué aux PDF. + ctk.CTkLabel(mask, text="Apparence du masque", text_color=p["text_muted"], font=ui_kit.font(11, "bold"), anchor="w").pack(fill="x", padx=12, pady=(6, 2)) + swatches = ctk.CTkFrame(mask, fg_color="transparent") + swatches.pack(fill="x", padx=12, pady=(0, 6)) + self._swatch_buttons: dict[str, ctk.CTkButton] = {} + for _label, color_value in _MASK_COLORS: + btn = ctk.CTkButton( + swatches, text="", width=28, height=24, fg_color=color_value, hover_color=color_value, + border_color=p["primary"] if color_value == self._mask_color else p["card_border"], + border_width=3 if color_value == self._mask_color else 1, corner_radius=6, + command=lambda c=color_value: self._set_mask_color(c), + ) + btn.pack(side="left", padx=(0, 6)) + self._swatch_buttons[color_value] = btn + style_row = ctk.CTkFrame(mask, fg_color="transparent") + style_row.pack(fill="x", padx=12, pady=(0, 6)) + for _label, value, preview in [("Crochets", "brackets", "[NOM]"), ("Étoiles", "stars", "***"), ("Noirci", "blackout", "████")]: + ctk.CTkRadioButton( + style_row, text=preview, variable=self._mask_style_var, value=value, command=self._update_mask_preview, + text_color=p["text"], fg_color=p["primary"], hover_color=p["primary_dim"], font=ui_kit.font(11), + ).pack(side="left", padx=(0, 10)) + self._slider_row(mask, "Marge H", self._mask_margin_x_var, self._on_mask_margin_x) + self._slider_row(mask, "Marge V", self._mask_margin_y_var, self._on_mask_margin_y) + self._mini_toggle( + mask, "Coins arrondis", "", value=self._state.mask_rounded_corners, + variable=self._mask_rounded_var, command=self._on_rounded_corners, + ).pack(fill="x", padx=12, pady=(2, 12)) eng = ui_kit.Card(left, p, title="🧠 Moteurs") eng.pack(fill="x") @@ -683,124 +694,6 @@ class ConfigTab(ctk.CTkFrame): # -- Masquage --------------------------------------------------------- - def _build_masquage(self, parent) -> None: - p = self._p - self._section_intro( - parent, - "Apparence des masques et éditeur de zones fixes à masquer sur vos PDF.", - _HELP_MASQUAGE, - "Le Masquage", - ) - top_cols = self._columns(parent, 3, gap=8, height=300) - - pdf_opts = ui_kit.Card(top_cols[0], p, title="⬛ PDF") - pdf_opts.pack(fill="both", expand=True) - ctk.CTkLabel( - pdf_opts, text="Couleur de masquage", text_color=p["text"], font=ui_kit.font(12, "bold") - ).pack(anchor="w", padx=12, pady=(0, 4)) - swatches = ctk.CTkFrame(pdf_opts, fg_color="transparent") - swatches.pack(fill="x", padx=12, pady=(0, 8)) - self._swatch_buttons: dict[str, ctk.CTkButton] = {} - for label, color_value in _MASK_COLORS: - btn = ctk.CTkButton( - swatches, - text="", - width=30, - height=26, - fg_color=color_value, - hover_color=color_value, - border_color=p["primary"] if color_value == self._mask_color else p["card_border"], - border_width=3 if color_value == self._mask_color else 1, - corner_radius=6, - command=lambda c=color_value: self._set_mask_color(c), - ) - btn.pack(side="left", padx=(0, 6)) - self._swatch_buttons[color_value] = btn - - self._slider_row(pdf_opts, "Marge H", self._mask_margin_x_var, self._on_mask_margin_x) - self._slider_row(pdf_opts, "Marge V", self._mask_margin_y_var, self._on_mask_margin_y) - self._mini_toggle( - pdf_opts, - "Coins arrondis", - "", - value=self._state.mask_rounded_corners, - variable=self._mask_rounded_var, - command=self._on_rounded_corners, - ).pack(fill="x", padx=12, pady=(4, 10)) - - text_opts = ui_kit.Card(top_cols[1], p, title="🏷️ Texte") - text_opts.pack(fill="both", expand=True) - for label, value, preview in [ - ("Crochets", "brackets", "[NOM]"), - ("Étoiles", "stars", "***"), - ("Noirci", "blackout", "████"), - ]: - ctk.CTkRadioButton( - text_opts, - text=f"{label} {preview}", - variable=self._mask_style_var, - value=value, - command=self._update_mask_preview, - text_color=p["text"], - fg_color=p["primary"], - hover_color=p["primary_dim"], - font=ui_kit.font(12), - ).pack(anchor="w", padx=12, pady=2) - self._mask_preview = ctk.CTkLabel( - text_opts, - text="Patient [NOM], né le [DATE_NAISSANCE]", - text_color=p["text_dim"], - fg_color=p["divider"], - corner_radius=6, - font=ui_kit.font(12), - anchor="w", - ) - self._mask_preview.pack(fill="x", padx=12, pady=(8, 10), ipady=7) - - codes = ui_kit.Card(top_cols[2], p, title="🔒 Codes") - codes.pack(fill="both", expand=True) - grid = ctk.CTkFrame(codes, fg_color="transparent") - grid.pack(fill="both", expand=True, padx=12, pady=(0, 10)) - for idx, (label, code) in enumerate(_REPLACEMENT_CODES): - ctk.CTkLabel(grid, text=label, text_color=p["text_muted"], font=ui_kit.font(11), anchor="w").grid( - row=idx, column=0, sticky="w", pady=1 - ) - ctk.CTkLabel(grid, text=code, text_color=p["primary"], font=ui_kit.font(11, "bold"), anchor="w").grid( - row=idx, column=1, sticky="w", padx=(8, 0), pady=1 - ) - grid.grid_columnconfigure(0, weight=1) - - editor = ui_kit.Card(parent, p, title="🏠 Masques de zones fixes") - editor.pack(fill="x", pady=(8, 0)) - ctk.CTkLabel( - editor, - text=( - "Définissez les zones à masquer (en-têtes, blocs identité…) directement sur " - "votre PDF, dans une fenêtre dédiée où le document est affiché en grand et " - "défilable (scroll, zoom, ajuster largeur/page). Les templates enregistrés " - "apparaissent ensuite dans « Template de masque manuel » (onglet Réglages)." - ), - text_color=p["text_muted"], - font=ui_kit.font(12), - justify="left", - wraplength=760, - anchor="w", - ).pack(fill="x", padx=14, pady=(0, 10)) - actions = ctk.CTkFrame(editor, fg_color="transparent") - actions.pack(fill="x", padx=14, pady=(0, 6)) - ui_kit.primary_button( - actions, p, "🖊 Ouvrir l'éditeur de masques", command=self._open_full_mask_editor - ).pack(side="left") - ui_kit.secondary_button( - actions, p, "📁 Dossier des templates", command=self._open_templates_dir - ).pack(side="left", padx=(8, 0)) - ctk.CTkLabel( - editor, - textvariable=self._mask_status_text, - text_color=p["text_muted"], - font=ui_kit.font(11), - anchor="w", - ).pack(fill="x", padx=14, pady=(2, 12)) # -- Partage / Règles ------------------------------------------------- @@ -999,13 +892,22 @@ class ConfigTab(ctk.CTkFrame): messagebox.showerror("Masques PDF", f"Impossible d'ouvrir l'éditeur : {exc}") def _on_mask_template_saved(self, path: Path) -> None: - """Callback déclenché par la fenêtre dédiée après sauvegarde d'un template.""" + """Callback déclenché par la fenêtre dédiée après sauvegarde d'un template. + + Lie le template au profil en cours d'édition (`preferred_manual_mask_template`). + """ self._refresh_manual_mask_templates() try: self._manual_mask_var.set(mask_template_label(path, _app_base_dir())) self._state.manual_mask_template = path except Exception: pass + # Renseigne le champ Template du profil édité (section Profils > Masquage). + if hasattr(self, "_pro_template_var"): + try: + self._pro_template_var.set(str(path)) + except Exception: + pass self._mask_status_text.set(f"Template enregistré : {path.name}") diff --git a/tests/unit/test_gui_v6_profiles.py b/tests/unit/test_gui_v6_profiles.py index ece7329..03a49b8 100644 --- a/tests/unit/test_gui_v6_profiles.py +++ b/tests/unit/test_gui_v6_profiles.py @@ -251,3 +251,25 @@ def test_usage_tab_finish_calls_reporter(ctk_root): assert called.wait(timeout=3.0) # reporter appelé en thread daemon assert captured["summary"] is summary tab.destroy() + + +def test_masquage_moved_into_profils(ctk_root, tmp_path, monkeypatch): + """Le sous-onglet Masquage est retiré ; son contenu utile est dans Profils.""" + from gui_v6.tabs import tab_config + + keys = [k for k, _ in tab_config._SUBTABS] + assert "msk" not in keys # plus de sous-onglet Masquage séparé + + monkeypatch.setattr(tab_config, "_app_base_dir", lambda: tmp_path) + tab = tab_config.ConfigTab(ctk_root) + tab._show_sub("pro") + tab.update_idletasks() + + # apparence du masque relocalisée dans la section Profils > Masquage + assert getattr(tab, "_swatch_buttons", None) + # un template enregistré depuis l'éditeur remplit le champ Template du profil + saved = tmp_path / "config" / "mask_templates" / "depuis_editeur.json" + saved.parent.mkdir(parents=True, exist_ok=True) + tab._on_mask_template_saved(saved) + assert tab._pro_template_var.get().endswith("depuis_editeur.json") + tab.destroy()