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 l’opérateur d’espaces. -> : 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 l’autre 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 l’aperçu et affiche la commande à recopier ci-dessus : aucun fichier n’est 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 l’opé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é. L’archivage 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))