feat(gui): onglet Profils éditable (création/modification/persistance)

Retour Dom : remplacer la page vitrine par un vrai éditeur de profils.

- gui_v6/profile_editor.py : couche logique (build_profile_spec,
  profile_is_editable runtime vs defaut, list_profile_choices, slug_for_copy,
  save/set_default/delete) au-dessus de profile_defaults — persistance dans
  config/profiles.yml.
- gui_v6/editable_list.py : EditableTermList (tableau scrollable de termes,
  ajout/suppression, pas de pastilles) — reste lisible à 50+ termes.
- tab_config : sous-onglet « 👤 Profils » réintroduit comme éditeur — menu
  déroulant « Profil à modifier », boutons Nouveau / Dupliquer / Enregistrer /
  Annuler / Définir par défaut, sections Identité, Masquage (require_manual_mask,
  template), Moteurs (force_disable_vlm), Mots (à masquer/conserver/ignorer
  éditables), Règles « à venir ». Profils défaut = lecture seule (dupliquer
  pour modifier). Confirmation non bloquante (pas de modale).
- Réglages : bouton « ✏️ Modifier le profil… » → ouvre Profils sur le profil
  actif. Pas de pastilles inline.

Persiste : label, description, require_manual_mask, force_disable_vlm,
preferred_manual_mask_template, param_lists (3 listes). 260 tests unit OK
(0 régression), self-test OK, nav 5 sous-onglets + thème OK. Préserve
1bbe70a/d30f7b7. Aucun build/push sans GO Dom.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-15 23:09:01 +02:00
parent 1bbe70a911
commit 72841ed7b3
5 changed files with 623 additions and 4 deletions

101
gui_v6/editable_list.py Normal file
View File

@@ -0,0 +1,101 @@
"""Liste de termes éditable et scrollable (pas de pastilles).
Utilisée dans l'éditeur de profils pour « mots à masquer / à conserver / à
ignorer ». Reste lisible avec 50+ termes (zone scrollable + ajout/suppression).
"""
from __future__ import annotations
import customtkinter as ctk
from gui_v6 import ui_kit
class EditableTermList(ctk.CTkFrame):
def __init__(self, master, palette: dict, *, title: str, initial=None, height: int = 150, **kwargs):
super().__init__(master, fg_color="transparent", **kwargs)
self._palette = palette
self._title = title
self._terms: list[str] = [str(t) for t in (initial or [])]
self._editable = True
self._build(height)
self._render()
# -- API testable --------------------------------------------------------
def terms(self) -> list[str]:
return list(self._terms)
def set_terms(self, terms) -> None:
self._terms = [str(t) for t in (terms or [])]
self._render()
def set_editable(self, editable: bool) -> None:
self._editable = bool(editable)
state = "normal" if self._editable else "disabled"
self._entry.configure(state=state)
self._add_btn.configure(state=state)
self._render()
def add_term(self, term: str) -> bool:
term = str(term).strip()
if not term or term in self._terms:
return False
self._terms.append(term)
self._render()
return True
def remove_term(self, term: str) -> None:
if term in self._terms:
self._terms.remove(term)
self._render()
# -- UI ------------------------------------------------------------------
def _build(self, height: int) -> None:
p = self._palette
ctk.CTkLabel(self, text=self._title, text_color=p["text"], font=ui_kit.font(12, "bold"), anchor="w").pack(
fill="x", pady=(0, 2)
)
row = ctk.CTkFrame(self, fg_color="transparent")
row.pack(fill="x", pady=(0, 4))
self._entry = ctk.CTkEntry(
row, placeholder_text="Ajouter un terme…", fg_color=p["btn_sec_bg"],
border_color=p["btn_sec_border"], text_color=p["text"], height=28,
)
self._entry.pack(side="left", fill="x", expand=True, padx=(0, 6))
self._entry.bind("<Return>", lambda _e: self._on_add())
self._add_btn = ui_kit.secondary_button(row, p, "+ Ajouter", command=self._on_add)
self._add_btn.pack(side="right")
self._list = ctk.CTkScrollableFrame(self, fg_color=p["divider"], height=height)
self._list.pack(fill="both", expand=True)
self._count = ctk.CTkLabel(self, text="", text_color=p["text_muted"], font=ui_kit.font(10), anchor="w")
self._count.pack(fill="x", pady=(2, 0))
def _on_add(self) -> None:
if not self._editable:
return
if self.add_term(self._entry.get()):
self._entry.delete(0, "end")
def _render(self) -> None:
p = self._palette
for child in self._list.winfo_children():
child.destroy()
if not self._terms:
ctk.CTkLabel(self._list, text="Aucun terme.", text_color=p["text_muted"], font=ui_kit.font(11)).pack(
anchor="w", padx=8, pady=6
)
for term in self._terms:
line = ctk.CTkFrame(self._list, fg_color="transparent")
line.pack(fill="x", pady=1)
ctk.CTkLabel(line, text=term, text_color=p["text"], font=ui_kit.font(12), anchor="w").pack(
side="left", fill="x", expand=True, padx=(6, 4)
)
btn = ctk.CTkButton(
line, text="×", width=26, height=24, corner_radius=6,
fg_color=p["btn_sec_bg"], hover_color=p["card_border"], text_color=p["text"],
command=lambda t=term: self.remove_term(t),
)
if not self._editable:
btn.configure(state="disabled")
btn.pack(side="right", padx=(0, 4))
self._count.configure(text=f"{len(self._terms)} terme(s)")