Feat: XML-Knoten-Bearbeiten-Dialog implementiert (v1.6.0)

Neuer Dialog ermöglicht es, einem XML-Knoten XSL-Zuordnungen hinzuzufügen
oder zu entfernen. XmlToXslAssignDialog wiederverwendet mit edit_mode,
Vorauswahl per preselected_xsl_ids und get_selection_diff(). Beim Entfernen
werden zugehörige PDF-Dateien gelöscht; bei verbleibend leerer Zuordnung
wird das physische Löschen der XML-Datei angeboten.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-18 21:10:34 +02:00
parent 762523d669
commit 9fad317891
7 changed files with 262 additions and 23 deletions
+156 -2
View File
@@ -30,6 +30,7 @@ from PySide6.QtWidgets import (
from conf import TreeNode, XslFile, XmlFile
from ui.TreeNodeEditDialog import TreeNodeEditDialog
from ui.XslFileEditDialog import XslFileEditDialog
from ui.XmlToXslAssignDialog import XmlToXslAssignDialog
class ItemType(Enum):
@@ -1397,9 +1398,162 @@ class TreeManagerMixin:
# Kontextmenü-Aktionen für XmlFile
def _edit_xml_file(self, item):
"""Bearbeitet eine XML-Datei."""
"""
Bearbeitet die Zuordnungen einer XML-Datei zu XSL-Knoten.
Öffnet den XmlToXslAssignDialog im Edit-Modus mit Vorauswahl aller bestehenden Zuordnungen.
"""
logger.debug(f"XmlFile bearbeiten: {item.text(0)}")
# TODO: Dialog zum Bearbeiten der XML-Datei öffnen
try:
if not hasattr(self, "project") or not self.project:
QMessageBox.warning(self, "Warnung", "Kein Projekt geladen. Bitte öffnen Sie zuerst ein Projekt.")
return
if not hasattr(self, "pdf_project") or not self.pdf_project:
QMessageBox.warning(self, "Warnung", "Keine Projekt-Einstellungen geladen.")
return
xml_file_obj = item.data(0, Qt.ItemDataRole.UserRole)
if not xml_file_obj or not isinstance(xml_file_obj, XmlFile):
QMessageBox.warning(self, "Warnung", "Keine gültige XML-Datei gefunden.")
return
xml_path = xml_file_obj.xml
# Alle aktuellen XSL-IDs, an denen diese XML hängt
current_xsl_ids = self._find_xsl_ids_for_xml(xml_path)
# Hash von einer vorhandenen Instanz übernehmen (falls vorhanden)
existing_hash = self._find_existing_xml_hash(xml_path)
dialog = XmlToXslAssignDialog(
parent=self,
xml_file_path=xml_path,
project_nodes=self.pdf_project.nodes,
preselected_xsl_ids=current_xsl_ids,
edit_mode=True,
)
if dialog.exec() != XmlToXslAssignDialog.DialogCode.Accepted:
logger.debug("Bearbeiten abgebrochen")
return
added_nodes, removed_nodes = dialog.get_selection_diff()
if not added_nodes and not removed_nodes:
logger.debug("Keine Änderungen an den XML-Zuordnungen")
return
self._apply_xml_assignment_changes(xml_path, existing_hash, added_nodes, removed_nodes)
# Speichern und Baum neu laden
self._save_project_settings()
self._load_nodes_to_tree()
# Wenn keine Zuordnungen mehr bestehen: Physische Datei löschen anbieten
if not self._find_xsl_ids_for_xml(xml_path):
xml_file_path = Path(self.project.project_dir) / xml_path
if xml_file_path.exists():
reply = QMessageBox.question(
self,
"Physische Datei löschen",
f"Die XML-Datei '{xml_path.name}' ist keinem XSL-Knoten mehr zugeordnet.\n\n"
"Möchten Sie auch die physische Datei aus dem xml-Ordner löschen?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
if reply == QMessageBox.StandardButton.Yes:
try:
xml_file_path.unlink()
logger.info(f"Physische XML-Datei gelöscht: {xml_file_path}")
except Exception as e:
QMessageBox.warning(self, "Warnung", f"Fehler beim Löschen der physischen Datei:\n{str(e)}")
logger.info(
f"XML-Zuordnungen aktualisiert: {len(added_nodes)} hinzugefügt, {len(removed_nodes)} entfernt"
)
except Exception as e:
error_msg = f"Fehler beim Bearbeiten der XML-Zuordnungen: {str(e)}"
logger.exception(error_msg)
QMessageBox.critical(self, "Fehler", error_msg)
def _find_xsl_ids_for_xml(self, xml_path: Path) -> set:
"""
Findet alle XslFile-IDs (tuple), deren `xmls`-Liste den angegebenen Pfad enthält.
Args:
xml_path: Relativer Pfad zur XML-Datei
Returns:
set[tuple]: Set von XslFile-IDs
"""
found_ids: set = set()
if not self.pdf_project or not self.pdf_project.nodes:
return found_ids
def _walk(nodes):
for node in nodes:
if isinstance(node, XslFile):
if any(xml.xml == xml_path for xml in node.xmls):
found_ids.add(node.id)
elif isinstance(node, TreeNode) and node.children:
_walk(node.children)
_walk(self.pdf_project.nodes)
return found_ids
def _find_existing_xml_hash(self, xml_path: Path) -> str | None:
"""
Sucht den blake2b-Hash einer bereits zugeordneten XmlFile-Instanz.
Gibt den ersten gefundenen Hash zurück (oder None, wenn keiner vorhanden).
"""
if not self.pdf_project or not self.pdf_project.nodes:
return None
def _walk(nodes):
for node in nodes:
if isinstance(node, XslFile):
for xml_file in node.xmls:
if xml_file.xml == xml_path and xml_file.hashsum:
return xml_file.hashsum
elif isinstance(node, TreeNode) and node.children:
found = _walk(node.children)
if found:
return found
return None
return _walk(self.pdf_project.nodes)
def _apply_xml_assignment_changes(
self, xml_path: Path, hashsum: str | None, added_nodes: list, removed_nodes: list
):
"""
Wendet Änderungen an den XML-Zuordnungen an:
- Entfernt XmlFile-Instanzen aus `removed_nodes` und löscht zugehörige PDFs (falls Kombination nirgends mehr existiert)
- Fügt XmlFile-Instanzen zu `added_nodes` hinzu (mit übernommenem Hash)
Args:
xml_path: Relativer Pfad zur XML-Datei
hashsum: blake2b-Hash, der für neue Instanzen übernommen wird
added_nodes: Liste der XslFile-Objekte, denen die XML neu zugeordnet wird
removed_nodes: Liste der XslFile-Objekte, aus denen die XML entfernt wird
"""
# 1. Entfernen
for xsl_node in removed_nodes:
xsl_node.xmls = [x for x in xsl_node.xmls if x.xml != xml_path]
# PDFs nur löschen, wenn die Kombination nirgends mehr existiert
if not self._is_xml_xsl_combination_used_elsewhere(xml_path, xsl_node.id, xsl_node):
deleted = self._delete_pdf_files_for_xml_xsl_combination(xml_path, xsl_node.id)
if deleted:
logger.info(f"{deleted} PDF-Datei(en) für '{xml_path.name}' (XSL: {xsl_node.bez}) gelöscht")
# 2. Hinzufügen (Hash übernehmen, Duplikate im gleichen XslFile vermeiden)
for xsl_node in added_nodes:
if any(x.xml == xml_path for x in xsl_node.xmls):
logger.debug(f"XML '{xml_path.name}' bereits an XSL '{xsl_node.bez}' zugeordnet — übersprungen")
continue
xsl_node.xmls.append(XmlFile(xml=xml_path, hashsum=hashsum))
logger.info(f"XML '{xml_path.name}' zu XSL '{xsl_node.bez}' hinzugefügt")
def _delete_xml_file(self, item):
"""