miscs (Interro02) : horizontal cutting resolution

This commit is contained in:
2026-09-14 22:20:46 +02:00
parent 5080274e8f
commit 0a86403ca6
28 changed files with 1447 additions and 59 deletions
+154
View File
@@ -0,0 +1,154 @@
from __future__ import annotations
import tkinter as tk
from pathlib import Path
from tkinter import messagebox, ttk
from copienator import CliError
from copienator.commands.resolve_manual import get_actual_pdf, parse_instruction_text
from copienator.platform import open_path
from .cut_helper import CutHelper
HELP = """La correction propose x> pour un mauvais label et -> pour une réponse supplémentaire lorsque le PDF cible existe déjà. Vérifiez les deux PDF avant de choisir.
Format : Copie01 Label source OP Label cible|
Conservez Copie suivi du numéro et les labels exacts (sans .pdf). Les espaces dans les labels sont acceptés ; entourez lopérateur despaces.
-> : fusionner dans la cible, conserver la source.
x> : fusionner dans la cible, archiver la source.
-x : remplacer la cible par une copie de la source, conserver la source.
xx : remplacer la cible par une copie de la source, archiver la source.
ss : conserver les deux PDF sans fusion.
sx : conserver la source, archiver la cible, sans fusion.
xs : archiver la source, conserver la cible, sans fusion.
c{43}1> : couper la source à 43 %, conserver le début et ajouter la fin à la cible.
c{43}2x : couper la source à 43 %, conserver la fin et remplacer la cible par le début.
1 conserve le début, 2 conserve la fin ; > fusionne lautre partie avec la cible, x la remplace. Le pourcentage porte sur la hauteur cumulée des pages visibles. Les décimales sont acceptées. Une seule coupe par label source est autorisée.
Cut permet de placer la coupe, avec accrochage entre les pages. Entrée ferme laperçu et affiche la commande à recopier ci-dessus : aucun fichier nest modifié. Une séparation entre pages conserve les pages entières. La source complète est archivée en _old ; les deux labels modifiés sont générés en _new et ajoutés à refaire.json.
Pour les fusions : « Source x> Cible| » place la cible avant la source ; « Source x> |Cible » place la source avant la cible. Même règle avec -> et c{…}1>/c{…}2> (pour la partie transférée) ; sans |, la cible vient en premier.
Vous pouvez changer lopérateur, déplacer |, corriger les labels ou retirer une instruction. Les lignes vides et celles commençant par ### sont ignorées. Retirer/commenter une ligne ne résout pas son conflit.
Enregistrez dans l’éditeur, puis cliquez sur Recharger et enfin sur Exécuter. Seul le fichier enregistré est appliqué. Larchivage utilise le suffixe _old ; les PDF créés utilisent _new. Après succès, manual_resolutions.txt est supprimé et correction.json est mis à jour. Si des PDF sont créés, refaire.json est généré : relancez la correction avec --refaire."""
class ManualResolutionPanel(ttk.Frame):
def __init__(self, parent, get_evaluation):
super().__init__(parent)
self.get_evaluation = get_evaluation
self.pdf_buttons = []
self.cut_buttons = []
actions = ttk.Frame(self)
actions.pack(fill="x")
self.editor_button = ttk.Button(
actions, text="Ouvrir dans un éditeur de texte", command=self.open_editor
)
self.editor_button.pack(side="left")
ttk.Button(actions, text="Recharger", command=self.reload).pack(side="left", padx=6)
self.cut_result = tk.StringVar()
result = ttk.Frame(self)
result.pack(fill="x", pady=4)
ttk.Entry(result, textvariable=self.cut_result, state="readonly").pack(side="left", fill="x", expand=True)
ttk.Button(result, text="Copier la commande Cut", command=self.copy_cut_command).pack(side="left", padx=6)
self.status = ttk.Label(self, wraplength=650)
self.status.pack(fill="x", pady=4)
preview = ttk.Frame(self)
preview.pack(fill="both", expand=True)
self.text = tk.Text(preview, height=10, width=50, wrap="word", state="disabled")
scroll = ttk.Scrollbar(preview, command=self.text.yview)
self.text.configure(yscrollcommand=scroll.set)
scroll.pack(side="right", fill="y")
self.text.pack(fill="both", expand=True)
ttk.Label(self, text=HELP, wraplength=650, justify="left").pack(fill="x", pady=8)
self.reload()
def manual_path(self) -> Path | None:
evaluation = self.get_evaluation()
return evaluation / "manual_resolutions.txt" if evaluation else None
def open_editor(self):
path = self.manual_path()
if path is None or not path.is_file():
self.reload()
return
try:
# .txt is opened in the desktop's associated text editor.
open_path(path)
except (OSError, RuntimeError) as exc:
messagebox.showerror("Ouverture impossible", str(exc))
def open_pdf(self, evaluation, copy_id, label):
path = get_actual_pdf(evaluation / "Copies", copy_id, label)
try:
if not path.is_file():
raise FileNotFoundError(f"PDF introuvable : {path}")
open_path(path)
except (OSError, RuntimeError) as exc:
messagebox.showerror("Ouverture du PDF", str(exc))
def reload(self):
for button in self.pdf_buttons + self.cut_buttons:
button.destroy()
self.pdf_buttons.clear()
self.cut_buttons.clear()
self.cut_result.set("")
self.text.configure(state="normal")
self.text.delete("1.0", "end")
path = self.manual_path()
self.editor_button.configure(state="disabled")
try:
if path is None:
raise FileNotFoundError("Chargez une évaluation.")
content = path.read_text(encoding="utf-8")
except (OSError, UnicodeError) as exc:
self.status.configure(text=f"manual_resolutions.txt indisponible : {exc}")
else:
self.editor_button.configure(state="normal")
malformed = []
for number, raw in enumerate(content.splitlines(keepends=True), 1):
self.text.insert("end", raw.rstrip("\r\n"))
try:
instructions = parse_instruction_text(raw)
except CliError:
malformed.append(str(number))
else:
if instructions:
instruction = instructions[0]
for title, label in (("PDF source", instruction.old_label),
("PDF cible", instruction.new_label)):
button = ttk.Button(
self.text, text=title,
command=lambda label=label, copy_id=instruction.copy_id, root=path.parent: self.open_pdf(root, copy_id, label),
)
self.pdf_buttons.append(button)
self.text.window_create("end", window=button, padx=8)
button = ttk.Button(self.text, text="Cut",
command=lambda item=instruction, root=path.parent: self.open_cut(root, item))
self.cut_buttons.append(button)
self.text.window_create("end", window=button, padx=8)
if raw.endswith("\n"):
self.text.insert("end", "\n")
detail = (" — lignes invalides : " + ", ".join(malformed)) if malformed else (
f"{len(self.pdf_buttons) // 2} instruction(s)"
)
self.status.configure(text=str(path) + detail)
finally:
self.text.configure(state="disabled")
def copy_cut_command(self):
if self.cut_result.get():
self.clipboard_clear()
self.clipboard_append(self.cut_result.get())
def open_cut(self, evaluation, instruction):
source = get_actual_pdf(evaluation / "Copies", instruction.copy_id, instruction.old_label)
def accepted(operator):
target = ("|" + instruction.new_label) if instruction.pipe_first else (instruction.new_label + "|")
self.cut_result.set(f"Copie{instruction.copy_id} {instruction.old_label} {operator} {target}")
try:
initial = (*instruction.cut, instruction.operator[-1]) if instruction.cut else None
CutHelper(self, source, accepted, initial)
except (OSError, RuntimeError, ValueError) as exc:
messagebox.showerror("Ouverture du PDF", str(exc))