feat(gui): éditeur de masques en fenêtre dédiée (GUI V6)

Remplace l'éditeur de masquage encastré dans l'onglet Configuration —
jugé inutilisable par Dom (document trop à l'étroit, non défilable) —
par une fenêtre dédiée où le document est majoritaire et réellement
navigable.

- gui_v6/mask_editor_model.py : couche logique pure (rectangles par
  page, conversions écran↔PDF, hit-test, sérialisation template)
  testable sans display ; réutilise MaskRect/Template de
  pdf_mask_designer → format de template inchangé (compat moteur).
- gui_v6/mask_editor_window.py : MaskEditorWindow (CTkToplevel)
  redimensionnable — canvas + scrollbars H+V câblées + molette (le
  manque qui rendait l'éditeur inutilisable), zoom + ajuster
  largeur/page, navigation pages, rectangles au glisser-déposer,
  sélection (clic) + suppression (Suppr / clic-droit), templates
  JSON/YAML, mode aperçu d'exemple sans PDF.
- tab_config.py : l'onglet Masquage lance la fenêtre dédiée ; retrait
  du canvas encastré et de ~290 lignes de code mort associé.
- tests/unit/test_gui_v6_mask_editor.py : 13 tests logique + 3 smoke
  headless (scrollbars, ajout/sélection/suppression, save/load
  roundtrip, câblage onglet→fenêtre).

Sans nouvelle dépendance. V5, moteur et app_aivanov non touchés.
221 tests unit OK (0 régression), self-test GUI V6 OK.
Verdict Qwen requis avant push/build/diffusion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-15 12:01:13 +02:00
parent 696f6bf27c
commit 13b79db417
4 changed files with 1004 additions and 462 deletions

View File

@@ -8,13 +8,10 @@ Partage/Règles.
from __future__ import annotations
import json
import sys
import webbrowser
from pathlib import Path
from tkinter import filedialog, messagebox
import tkinter as tk
from typing import Any
import customtkinter as ctk
@@ -109,10 +106,6 @@ def _app_base_dir() -> Path:
return Path.cwd()
def _clamp(value: float, low: float, high: float) -> float:
return max(low, min(high, value))
class ConfigTab(ctk.CTkFrame):
def __init__(self, master, state: ConfigState | None = None, palette: dict | None = None, **kwargs):
self._p = palette or theme_mod.get_palette(theme_mod.DEFAULT_THEME)
@@ -131,24 +124,12 @@ class ConfigTab(ctk.CTkFrame):
self._mask_margin_x_var = ctk.IntVar(value=self._state.mask_margin_x)
self._mask_margin_y_var = ctk.IntVar(value=self._state.mask_margin_y)
self._mask_rounded_var = ctk.BooleanVar(value=self._state.mask_rounded_corners)
self._mask_template_name = ctk.StringVar(value="template_masques")
self._mask_dpi = ctk.IntVar(value=200)
self._mask_zoom = 1.0
self._mask_zoom_text = ctk.StringVar(value="100%")
self._mask_status_text = ctk.StringVar(value="Aucun PDF chargé")
self._mask_count_text = ctk.StringVar(value="0 masque")
self._mask_status_text = ctk.StringVar(value="Éditeur de masques en fenêtre dédiée.")
self._mask_doc = None
self._mask_pdf_path: Path | None = None
self._mask_page_index = 0
self._mask_page_size = (595.0, 842.0)
self._mask_rects: list[dict[str, Any]] = []
self._mask_photo = None
self._mask_canvas: tk.Canvas | None = None
self._mask_page_origin = (0.0, 0.0)
self._mask_current_scale = 1.0
self._mask_drag_start: tuple[float, float] | None = None
self._mask_preview_item: int | None = None
# L'édition interactive des masques se fait dans une fenêtre séparée
# (gui_v6.mask_editor_window) ; on garde juste une référence à l'instance
# ouverte pour éviter d'en empiler plusieurs.
self._mask_editor_window = None
self._build()
@@ -212,8 +193,6 @@ class ConfigTab(ctk.CTkFrame):
font=ui_kit.font(13, "bold" if active else "normal"),
)
self._panels[key].tkraise()
if key == "msk":
self.after_idle(self._refresh_mask_canvas)
# -- Réglages ---------------------------------------------------------
@@ -410,109 +389,35 @@ class ConfigTab(ctk.CTkFrame):
editor = ui_kit.Card(parent, p, title="🏠 Masques de zones fixes")
editor.pack(fill="x", pady=(8, 0))
toolbar = ctk.CTkFrame(editor, fg_color="transparent")
toolbar.pack(fill="x", padx=12, pady=(0, 8))
ui_kit.primary_button(toolbar, p, "📄 Ouvrir PDF…", command=self._open_mask_pdf).pack(
side="left", padx=(0, 6)
)
ui_kit.secondary_button(toolbar, p, "↙ Fenêtre complète", command=self._open_full_mask_editor).pack(
side="left", padx=(0, 6)
)
self._toolbar_button(toolbar, "", lambda: self._move_mask_page(-1), width=38).pack(side="left", padx=(0, 4))
self._toolbar_button(toolbar, "", lambda: self._move_mask_page(1), width=38).pack(side="left", padx=(0, 8))
self._toolbar_button(toolbar, "", lambda: self._zoom_mask(-0.15), width=38).pack(side="left", padx=(0, 4))
ctk.CTkLabel(toolbar, textvariable=self._mask_zoom_text, text_color=p["text_muted"], font=ui_kit.font(12), width=46).pack(
side="left"
)
self._toolbar_button(toolbar, "+", lambda: self._zoom_mask(0.15), width=38).pack(side="left", padx=(4, 8))
work = ctk.CTkFrame(editor, fg_color="transparent")
work.pack(fill="x", padx=12, pady=(0, 12))
canvas_wrap = ctk.CTkFrame(
work,
fg_color=p["divider"],
border_color=p["card_border"],
border_width=1,
corner_radius=8,
height=292,
)
canvas_wrap.pack(side="left", fill="both", expand=True, padx=(0, 8))
canvas_wrap.pack_propagate(False)
self._mask_canvas = tk.Canvas(
canvas_wrap,
bg=p["divider"],
highlightthickness=0,
bd=0,
height=288,
cursor="crosshair",
)
self._mask_canvas.pack(fill="both", expand=True, padx=6, pady=6)
self._mask_canvas.bind("<ButtonPress-1>", self._on_mask_canvas_down)
self._mask_canvas.bind("<B1-Motion>", self._on_mask_canvas_drag)
self._mask_canvas.bind("<ButtonRelease-1>", self._on_mask_canvas_up)
self._mask_canvas.bind("<Configure>", lambda _event: self._refresh_mask_canvas())
side = ctk.CTkFrame(
work,
fg_color=p["divider"],
border_color=p["card_border"],
border_width=1,
corner_radius=8,
width=230,
)
side.pack(side="left", fill="y")
ctk.CTkLabel(side, text="Template", text_color=p["text_dim"], font=ui_kit.font(11, "bold"), anchor="w").pack(
fill="x", padx=12, pady=(12, 2)
)
ctk.CTkEntry(
side,
textvariable=self._mask_template_name,
fg_color=p["btn_sec_bg"],
border_color=p["btn_sec_border"],
text_color=p["text"],
height=30,
).pack(fill="x", padx=12, pady=(0, 8))
dpi_row = ctk.CTkFrame(side, fg_color="transparent")
dpi_row.pack(fill="x", padx=12, pady=(0, 8))
ctk.CTkLabel(dpi_row, text="DPI raster", text_color=p["text_muted"], font=ui_kit.font(12)).pack(side="left")
ctk.CTkEntry(
dpi_row,
textvariable=self._mask_dpi,
fg_color=p["btn_sec_bg"],
border_color=p["btn_sec_border"],
text_color=p["text"],
width=62,
height=28,
justify="center",
).pack(side="right")
ui_kit.secondary_button(side, p, "💾 Sauver JSON", command=self._save_mask_template).pack(
fill="x", padx=12, pady=(0, 6)
)
ui_kit.secondary_button(side, p, "📁 Charger", command=self._load_mask_template).pack(
fill="x", padx=12, pady=(0, 6)
)
ui_kit.secondary_button(side, p, "👁 Prévisualiser", command=self._preview_mask_template).pack(
fill="x", padx=12, pady=(0, 6)
)
ui_kit.primary_button(side, p, "▶ Appliquer", command=self._apply_mask_template_selection).pack(
fill="x", padx=12, pady=(0, 10)
)
clear_row = ctk.CTkFrame(side, fg_color="transparent")
clear_row.pack(fill="x", padx=12, pady=(0, 8))
ui_kit.secondary_button(clear_row, p, "Page", command=self._clear_mask_page).pack(side="left", fill="x", expand=True, padx=(0, 4))
ui_kit.secondary_button(clear_row, p, "Tout", command=self._clear_all_masks).pack(side="left", fill="x", expand=True, padx=(4, 0))
ctk.CTkLabel(side, textvariable=self._mask_status_text, text_color=p["text_muted"], font=ui_kit.font(11), wraplength=190, justify="left").pack(
fill="x", padx=12, pady=(4, 4)
)
ctk.CTkLabel(side, textvariable=self._mask_count_text, text_color=p["primary"], font=ui_kit.font(13, "bold")).pack(
fill="x", padx=12, pady=(0, 12)
)
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 -------------------------------------------------
@@ -625,7 +530,6 @@ class ConfigTab(ctk.CTkFrame):
border_color=p["primary"] if value == color else p["card_border"],
border_width=3 if value == color else 1,
)
self._refresh_mask_canvas()
def _on_mask_margin_x(self, value: float) -> None:
self._state.mask_margin_x = int(round(value))
@@ -651,337 +555,63 @@ class ConfigTab(ctk.CTkFrame):
# -- éditeur masques --------------------------------------------------
def _open_mask_pdf(self) -> None:
path = filedialog.askopenfilename(title="PDF modèle", filetypes=[("PDF", "*.pdf")])
if not path:
return
try:
import fitz
doc = fitz.open(path)
if len(doc) == 0:
raise ValueError("PDF vide")
if self._mask_doc is not None:
try:
self._mask_doc.close()
except Exception:
pass
self._mask_doc = doc
self._mask_pdf_path = Path(path)
self._mask_page_index = 0
self._mask_page_size = (float(doc[0].rect.width), float(doc[0].rect.height))
self._mask_rects.clear()
self._mask_template_name.set(f"{self._mask_pdf_path.stem}_template")
self._mask_status_text.set(f"{self._mask_pdf_path.name} — page 1/{len(doc)}")
self._update_mask_count()
self._refresh_mask_canvas()
except Exception as exc:
messagebox.showerror("Masques PDF", f"Impossible d'ouvrir le PDF : {exc}")
def _open_full_mask_editor(self) -> None:
try:
from pdf_mask_designer import MaskDesignerApp
win = tk.Toplevel(self)
MaskDesignerApp(win, templates_dir=ensure_mask_templates_dir(_app_base_dir()))
except Exception as exc:
messagebox.showerror("Masques PDF", f"Impossible d'ouvrir l'éditeur complet : {exc}")
def _move_mask_page(self, delta: int) -> None:
if self._mask_doc is None:
return
self._mask_page_index = int(_clamp(self._mask_page_index + delta, 0, len(self._mask_doc) - 1))
page = self._mask_doc[self._mask_page_index]
self._mask_page_size = (float(page.rect.width), float(page.rect.height))
self._mask_status_text.set(f"{self._mask_pdf_path.name if self._mask_pdf_path else 'PDF'} — page {self._mask_page_index + 1}/{len(self._mask_doc)}")
self._update_mask_count()
self._refresh_mask_canvas()
def _zoom_mask(self, delta: float) -> None:
self._mask_zoom = _clamp(self._mask_zoom + delta, 0.55, 2.5)
self._mask_zoom_text.set(f"{round(self._mask_zoom * 100)}%")
self._refresh_mask_canvas()
def _clear_mask_page(self) -> None:
self._mask_rects = [m for m in self._mask_rects if int(m.get("page", 0)) != self._mask_page_index]
self._mask_status_text.set(f"Masques page {self._mask_page_index + 1} supprimés.")
self._update_mask_count()
self._refresh_mask_canvas()
def _clear_all_masks(self) -> None:
self._mask_rects.clear()
self._mask_status_text.set("Tous les masques ont été supprimés.")
self._update_mask_count()
self._refresh_mask_canvas()
def _preview_mask_template(self) -> None:
count = self._current_page_mask_count()
self._mask_status_text.set(f"Prévisualisation : {count} masque(s) sur la page courante.")
self._refresh_mask_canvas()
def _apply_mask_template_selection(self) -> None:
path = self._save_mask_template(silent=True)
if path is None:
return
self._state.manual_mask_template = path
self._manual_mask_required_var.set(True)
self._on_manual_mask_required()
self._refresh_manual_mask_templates()
self._manual_mask_var.set(mask_template_label(path, _app_base_dir()))
self._mask_status_text.set(f"Template actif : {path.name}")
def _save_mask_template(self, silent: bool = False) -> Path | None:
tpl = self._current_mask_template_payload()
if not tpl["masks"]:
self._mask_status_text.set("Dessinez au moins un masque avant de sauvegarder.")
if not silent:
messagebox.showwarning("Masques PDF", "Aucun masque défini.")
return None
initial_dir = ensure_mask_templates_dir(_app_base_dir())
default_name = f"{tpl['name'] or 'template_masques'}.json"
if silent:
path = initial_dir / default_name
else:
selected = filedialog.asksaveasfilename(
title="Sauver le template",
defaultextension=".json",
filetypes=[("JSON", "*.json")],
initialdir=str(initial_dir),
initialfile=default_name,
)
if not selected:
return None
path = Path(selected)
try:
path.write_text(json.dumps(tpl, ensure_ascii=False, indent=2), encoding="utf-8")
self._mask_status_text.set(f"Template sauvegardé : {path.name}")
self._refresh_manual_mask_templates()
return path
except Exception as exc:
if not silent:
messagebox.showerror("Masques PDF", f"Impossible d'écrire le template : {exc}")
self._mask_status_text.set("Échec sauvegarde template.")
return None
def _load_mask_template(self) -> None:
initial_dir = ensure_mask_templates_dir(_app_base_dir())
selected = filedialog.askopenfilename(
title="Charger un template",
filetypes=[("Templates", "*.json *.yml *.yaml")],
initialdir=str(initial_dir),
)
if not selected:
return
try:
payload = self._read_mask_template(Path(selected))
self._mask_template_name.set(str(payload.get("name") or Path(selected).stem))
ps = payload.get("page_size") or {}
self._mask_page_size = (float(ps.get("width", 595)), float(ps.get("height", 842)))
self._mask_rects = [
{
"page": int(m.get("page", 0)),
"x0": float(m.get("x0", 0)),
"y0": float(m.get("y0", 0)),
"x1": float(m.get("x1", 0)),
"y1": float(m.get("y1", 0)),
"label": str(m.get("label", "MASK")),
}
for m in payload.get("masks", [])
]
self._state.manual_mask_template = Path(selected)
self._refresh_manual_mask_templates()
self._manual_mask_var.set(mask_template_label(Path(selected), _app_base_dir()))
self._mask_status_text.set(f"Template chargé : {Path(selected).name}")
self._update_mask_count()
self._refresh_mask_canvas()
except Exception as exc:
messagebox.showerror("Masques PDF", f"Template invalide : {exc}")
def _read_mask_template(self, path: Path) -> dict[str, Any]:
if path.suffix.lower() in {".yml", ".yaml"}:
import yaml
return yaml.safe_load(path.read_text(encoding="utf-8")) or {}
return json.loads(path.read_text(encoding="utf-8"))
def _current_mask_template_payload(self) -> dict[str, Any]:
width, height = self._mask_page_size
return {
"version": 1,
"name": self._mask_template_name.get().strip() or "template_masques",
"page_size": {"width": width, "height": height},
"masks": [
{
"page": int(m["page"]),
"x0": round(float(m["x0"]), 2),
"y0": round(float(m["y0"]), 2),
"x1": round(float(m["x1"]), 2),
"y1": round(float(m["y1"]), 2),
"label": str(m.get("label", "MASK")),
}
for m in self._mask_rects
],
}
def _refresh_mask_canvas(self) -> None:
canvas = self._mask_canvas
if canvas is None:
return
canvas.delete("all")
p = self._p
width = max(360, canvas.winfo_width() or 420)
height = max(260, canvas.winfo_height() or 318)
page_w, page_h = self._mask_page_size
fit_scale = min((width - 26) / page_w, (height - 26) / page_h)
scale = max(0.1, fit_scale * self._mask_zoom)
page_px_w = page_w * scale
page_px_h = page_h * scale
origin_x = max(12, (width - page_px_w) / 2)
origin_y = 12
self._mask_page_origin = (origin_x, origin_y)
self._mask_current_scale = scale
if self._mask_doc is not None:
existing = self._mask_editor_window
if existing is not None:
try:
from PIL import Image, ImageTk
import fitz
page = self._mask_doc[self._mask_page_index]
pix = page.get_pixmap(matrix=fitz.Matrix(scale, scale), annots=False)
img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
self._mask_photo = ImageTk.PhotoImage(img)
canvas.create_image(origin_x, origin_y, image=self._mask_photo, anchor="nw")
if existing.winfo_exists():
existing.lift()
existing.focus_force()
return
except Exception:
self._draw_sample_page(canvas, origin_x, origin_y, page_px_w, page_px_h)
else:
self._draw_sample_page(canvas, origin_x, origin_y, page_px_w, page_px_h)
pass
try:
from gui_v6.mask_editor_window import MaskEditorWindow
self._draw_mask_rectangles(canvas)
if self._mask_doc is None:
canvas.create_text(
width / 2,
height - 18,
text="Ouvrez un PDF modèle, ou dessinez directement sur l'aperçu d'exemple.",
fill=p["text_muted"],
font=("TkDefaultFont", 9),
active = self._state.manual_mask_template
initial_template = active if (active and Path(active).exists()) else None
win = MaskEditorWindow(
self.winfo_toplevel(),
templates_dir=ensure_mask_templates_dir(_app_base_dir()),
initial_template=initial_template,
on_template_saved=self._on_mask_template_saved,
)
self._mask_editor_window = win
self._mask_status_text.set("Éditeur de masques ouvert.")
except Exception as exc:
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."""
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
self._mask_status_text.set(f"Template enregistré : {path.name}")
def _draw_sample_page(self, canvas: tk.Canvas, x: float, y: float, w: float, h: float) -> None:
p = self._p
canvas.create_rectangle(x, y, x + w, y + h, fill="#f8fafc", outline=p["card_border"], width=1)
header_h = min(54, h * 0.16)
canvas.create_rectangle(x, y, x + w, y + header_h, fill="#e5e7eb", outline="")
scale = self._mask_current_scale
canvas.create_text(x + 18 * scale, y + 35 * scale, text="EN-TÊTE ÉTABLISSEMENT [LOGO]", fill="#6b7280", anchor="w", font=("TkDefaultFont", 10, "bold"))
canvas.create_text(x + 18 * scale, y + 72 * scale, text="Service de cardiologie | Tel : 05.59.XX.XX.XX", fill="#6b7280", anchor="w", font=("TkDefaultFont", 8))
for idx, line in enumerate(
[
"Patient : Dupont Jean Né le : 12/03/1955",
"IPP : 1234567 NDA : 8901234",
"Motif : Insuffisance cardiaque décompensée.",
"Traitement : FUROSEMIDE 40mg, BISOPROLOL 5mg.",
"Signé : Dr Martin RPPS 12345678",
]
):
canvas.create_text(x + 28 * scale, y + (128 + idx * 32) * scale, text=line, fill="#111827", anchor="w", font=("TkDefaultFont", 8))
def _draw_mask_rectangles(self, canvas: tk.Canvas) -> None:
x0, y0 = self._mask_page_origin
scale = self._mask_current_scale
for idx, mask in enumerate(self._mask_rects):
if int(mask.get("page", 0)) != self._mask_page_index:
continue
rx0 = x0 + float(mask["x0"]) * scale
ry0 = y0 + float(mask["y0"]) * scale
rx1 = x0 + float(mask["x1"]) * scale
ry1 = y0 + float(mask["y1"]) * scale
canvas.create_rectangle(rx0, ry0, rx1, ry1, fill=self._mask_color, outline=self._p["primary"], width=2, tags=(f"mask-{idx}",))
canvas.create_text((rx0 + rx1) / 2, (ry0 + ry1) / 2, text="×", fill="#ffffff", font=("TkDefaultFont", 10, "bold"))
def _on_mask_canvas_down(self, event) -> None:
canvas = self._mask_canvas
if canvas is None:
return
point = self._canvas_to_pdf(event.x, event.y)
if point is None:
return
hit = self._mask_at(point)
if hit is not None:
del self._mask_rects[hit]
self._mask_status_text.set("Masque supprimé.")
self._update_mask_count()
self._refresh_mask_canvas()
return
self._mask_drag_start = point
x, y = event.x, event.y
self._mask_preview_item = canvas.create_rectangle(x, y, x, y, outline=self._p["primary"], width=2, dash=(4, 2))
def _on_mask_canvas_drag(self, event) -> None:
canvas = self._mask_canvas
if canvas is None or self._mask_drag_start is None or self._mask_preview_item is None:
return
start = self._pdf_to_canvas(self._mask_drag_start)
if start is None:
return
canvas.coords(self._mask_preview_item, start[0], start[1], event.x, event.y)
def _on_mask_canvas_up(self, event) -> None:
canvas = self._mask_canvas
if canvas is None or self._mask_drag_start is None:
return
end = self._canvas_to_pdf(event.x, event.y)
if self._mask_preview_item is not None:
canvas.delete(self._mask_preview_item)
self._mask_preview_item = None
start = self._mask_drag_start
self._mask_drag_start = None
if end is None:
return
x0, y0 = start
x1, y1 = end
rx0, rx1 = sorted([x0, x1])
ry0, ry1 = sorted([y0, y1])
if (rx1 - rx0) < 5 or (ry1 - ry0) < 5:
return
self._mask_rects.append(
{"page": self._mask_page_index, "x0": rx0, "y0": ry0, "x1": rx1, "y1": ry1, "label": "MASK"}
)
self._mask_status_text.set(f"Masque ajouté page {self._mask_page_index + 1}.")
self._update_mask_count()
self._refresh_mask_canvas()
def _canvas_to_pdf(self, x: float, y: float) -> tuple[float, float] | None:
ox, oy = self._mask_page_origin
scale = self._mask_current_scale
page_w, page_h = self._mask_page_size
px = (x - ox) / scale
py = (y - oy) / scale
if px < 0 or py < 0 or px > page_w or py > page_h:
return None
return px, py
def _pdf_to_canvas(self, point: tuple[float, float]) -> tuple[float, float] | None:
ox, oy = self._mask_page_origin
scale = self._mask_current_scale
return ox + point[0] * scale, oy + point[1] * scale
def _mask_at(self, point: tuple[float, float]) -> int | None:
px, py = point
for idx in range(len(self._mask_rects) - 1, -1, -1):
mask = self._mask_rects[idx]
if int(mask.get("page", 0)) != self._mask_page_index:
continue
if float(mask["x0"]) <= px <= float(mask["x1"]) and float(mask["y0"]) <= py <= float(mask["y1"]):
return idx
return None
def _current_page_mask_count(self) -> int:
return sum(1 for m in self._mask_rects if int(m.get("page", 0)) == self._mask_page_index)
def _update_mask_count(self) -> None:
page_count = self._current_page_mask_count()
total = len(self._mask_rects)
self._mask_count_text.set(f"{page_count} masque(s) page · {total} total")
# -- helpers UI -------------------------------------------------------
@@ -1074,23 +704,6 @@ class ConfigTab(ctk.CTkFrame):
slider.set(variable.get())
slider.pack(side="right", padx=(8, 4))
def _toolbar_button(self, parent, text: str, command, width: int = 42):
p = self._p
return ctk.CTkButton(
parent,
text=text,
command=command,
fg_color=p["btn_sec_bg"],
hover_color=p["card_border"],
text_color=p["text"],
border_color=p["btn_sec_border"],
border_width=1,
corner_radius=8,
height=32,
width=width,
font=ui_kit.font(13, "bold"),
)
def _rule_row(self, parent, values: tuple[str, str, str, str]) -> None:
p = self._p
label, rule_type, target, status = values