Files
Copies/copienator_gui/annotation_modifications.py

158 lines
8.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import tkinter as tk
from pathlib import Path
from tempfile import TemporaryDirectory
from tkinter import messagebox, ttk
from copienator import CliError, utils
from copienator.annotation_modifications import (
DRAFT,
baseline,
body_image,
edited_bodies,
normalize_copy,
read_instructions,
validate_instructions,
)
from copienator.commands.resolve_manual import ManualInstruction
from copienator.platform import open_path
from .cut_helper import CutHelper
from .manual_resolution import ManualResolutionPanel
HELP = """Ajoutez une copie, un label source et un label cible. Source, Cible et Cut montrent uniquement le corps de la réponse avec ses annotations conservées, sans note, commentaire global, contexte, énoncé ou corrigé. Les en-têtes restent associés à leur label ; les scores ne changent pas.
Format : Copie43 Label source OP Label cible|
-> ajoute la source à la cible ; x> la déplace ; -x copie en remplaçant la cible ; xx déplace en remplaçant. ss conserve les deux ; sx vide la cible ; xs vide la source.
c{43}1> conserve les premiers 43 % de la source et ajoute le reste à la cible ; c{43}2x conserve la fin et remplace la cible par le début. Le pourcentage porte uniquement sur le corps de la réponse. |Cible place le fragment avant la cible ; Cible| le place après.
Une instruction par source. Une cible ne peut pas également être source. Plusieurs sources peuvent alimenter une cible dans l’ordre du fichier. Une cible sans image est vide.
Cut affiche une commande à recopier dans l’éditeur. Enregistrez puis Recharger. Aperçu du résultat montre les corps après toutes les instructions enregistrées.
Make modifications applique uniquement le fichier enregistré. Chaque application repart des images originales relues : modifier ou retirer une ligne puis appliquer remplace ou annule son effet. Les lignes ### sont des commentaires ; conservez la ligne ### Source: BGnot ou Bnot. Le fichier est conservé. Une nouvelle lecture des annotations réapplique les dernières instructions appliquées, jamais un brouillon. Relancez ensuite la préparation de A Rendre (sans l’option limitée à answers) pour actualiser tous les livrables."""
def annotation_mode_for_reader(reader):
if reader.get("last_run_variant", reader.get("variant")) == "standard":
return "Bnot"
values = reader.get("last_run_values", reader.get("values", {}))
if values.get("refaire") and values.get("annotation_dir") == "Bnot":
return "Bnot"
return "BGnot"
class AnnotationModificationsPanel(ManualResolutionPanel):
def __init__(self, parent, get_evaluation, get_mode):
self.get_mode = get_mode
self.previews = TemporaryDirectory(prefix="copienator-body-preview-")
super().__init__(parent, get_evaluation, help_text=HELP, show_provenance=False)
self.bind("<Destroy>", self._cleanup, add="+")
form = ttk.Frame(self)
form.pack(fill="x", before=self.editor_button.master, pady=5)
self.copy_var, self.source_var, self.target_var = (tk.StringVar() for _ in range(3))
ttk.Label(form, text="Copie").grid(row=0, column=0)
ttk.Entry(form, textvariable=self.copy_var, width=9).grid(row=1, column=0, padx=3)
self.label_choices = []
for column, title, variable in ((1, "Label source", self.source_var), (2, "Label cible", self.target_var)):
ttk.Label(form, text=title).grid(row=0, column=column)
combo = ttk.Combobox(form, textvariable=variable, width=22)
combo.grid(row=1, column=column, padx=3)
self.label_choices.append(combo)
ttk.Button(form, text="Ajouter", command=self.add_instruction).grid(row=1, column=3, padx=5)
ttk.Button(self.editor_button.master, text="Aperçu du résultat", command=self.preview_result).pack(side="left")
self.reload()
def _cleanup(self, event):
if event.widget is self:
self.previews.cleanup()
def manual_path(self):
evaluation = self.get_evaluation()
return evaluation / DRAFT if evaluation else None
def _read(self):
path = self.manual_path()
if path is None:
raise CliError("Chargez une évaluation.")
text = path.read_text(encoding="utf-8") if path.exists() else f"### Source: {self.get_mode()}\n"
return path, text, read_instructions(text, self.get_mode())
def reload(self):
super().reload()
self.editor_button.configure(state="normal" if self.manual_path() else "disabled")
try:
path, _, instructions = self._read()
validate_instructions(path.parent, self.get_mode(), instructions)
for combo in getattr(self, "label_choices", []):
combo.configure(values=utils.read_all_labels(path.parent))
except (CliError, OSError, ValueError, KeyError) as exc:
self.status.configure(text=str(exc))
def open_editor(self):
try:
path = self.manual_path()
if path is None:
raise CliError("Chargez une évaluation.")
if not path.exists():
path.write_text(f"### Source: {self.get_mode()}\n", encoding="utf-8")
open_path(path)
except (CliError, OSError, ValueError, RuntimeError) as exc:
messagebox.showerror("Ouverture du fichier", str(exc))
def add_instruction(self):
try:
path, text, instructions = self._read()
copy_id = normalize_copy(path.parent, self.copy_var.get())
source, target = self.source_var.get().strip(), self.target_var.get().strip()
item = ManualInstruction(copy_id, source, "->", target, False)
validate_instructions(path.parent, self.get_mode(), [*instructions, item])
text = text.rstrip() + f"\nCopie{copy_id} {source} -> {target}|\n"
path.write_text(text, encoding="utf-8")
self.reload()
except (CliError, OSError, ValueError, KeyError) as exc:
messagebox.showerror("Instruction invalide", str(exc))
def _show(self, image, name):
path = Path(self.previews.name) / f"{name}.png"
image.save(path)
open_path(path)
def open_pdf(self, evaluation, copy_id, label):
try:
self._show(body_image(evaluation / self.get_mode() / f"Copie{copy_id}", label),
f"Copie{copy_id} - {label} - corps")
except (CliError, OSError, ValueError, RuntimeError, KeyError) as exc:
messagebox.showinfo("Aperçu du corps annoté", str(exc))
def open_cut(self, evaluation, instruction):
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:
source = body_image(evaluation / self.get_mode() / f"Copie{instruction.copy_id}", instruction.old_label)
initial = (*instruction.cut, instruction.operator[-1]) if instruction.cut else None
CutHelper(self, source, accepted, initial)
except (CliError, OSError, ValueError, RuntimeError, KeyError) as exc:
messagebox.showerror("Coupe du corps annoté", str(exc))
def preview_result(self):
try:
path, _, instructions = self._read()
validate_instructions(path.parent, self.get_mode(), instructions)
# Preview just the requested copy, avoiding a window for every copy.
copy_id = normalize_copy(path.parent, self.copy_var.get())
selected = [i for i in instructions if i.copy_id == copy_id]
if not selected:
raise CliError("Aucune instruction pour cette copie. Saisissez son numéro dans Copie.")
directory = path.parent / self.get_mode() / f"Copie{copy_id}"
_, bodies, changed = edited_bodies(directory, baseline(directory), selected)
for label in sorted(changed):
if label in bodies:
self._show(bodies[label], f"Copie{copy_id} - {label} - résultat")
emptied = sorted(changed - bodies.keys())
if emptied:
messagebox.showinfo("Réponses vidées", ", ".join(emptied))
except (CliError, OSError, ValueError, RuntimeError, KeyError) as exc:
messagebox.showerror("Aperçu du résultat", str(exc))