Feature: Löschen-Funktion für TreeNode-Knoten im Baum implementiert

Rekursive Löschung von TreeNodes mit PDF-Bereinigung, automatischer
physischer Löschung nicht mehr verwendeter XML-Dateien und korrekter
"anderswo verwendet"-Prüfung durch vorheriges Entfernen aus dem
Datenmodell.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-08 21:08:23 +01:00
parent 52180e38ce
commit b985e1eeee
+157 -2
View File
@@ -794,10 +794,165 @@ class TreeManagerMixin:
logger.error(error_msg) logger.error(error_msg)
QMessageBox.critical(self, "Fehler", error_msg) QMessageBox.critical(self, "Fehler", error_msg)
def _collect_xsl_files_recursive(self, node):
"""
Sammelt rekursiv alle XslFile-Objekte unter einem TreeNode.
Args:
node: Der TreeNode, dessen XslFiles gesammelt werden sollen
Returns:
list[XslFile]: Liste aller XslFiles im Teilbaum
"""
xsl_files = []
for child in node.children:
if isinstance(child, XslFile):
xsl_files.append(child)
elif isinstance(child, TreeNode):
xsl_files.extend(self._collect_xsl_files_recursive(child))
return xsl_files
def _count_sub_nodes_recursive(self, node):
"""
Zählt rekursiv alle untergeordneten TreeNodes.
Args:
node: Der TreeNode, dessen Unterknoten gezählt werden sollen
Returns:
int: Anzahl der untergeordneten TreeNodes
"""
count = 0
for child in node.children:
if isinstance(child, TreeNode):
count += 1 + self._count_sub_nodes_recursive(child)
return count
def _delete_tree_node(self, item): def _delete_tree_node(self, item):
"""Löscht einen TreeNode.""" """
Löscht einen TreeNode und alle untergeordneten Elemente.
Die XSL-Dateien werden nur aus dem Baum entfernt, nicht physisch gelöscht.
Zugehörige PDF-Dateien werden automatisch bereinigt.
XML-Dateien, die nirgends mehr verwendet werden, können optional physisch gelöscht werden.
Args:
item: Das TreeWidgetItem des TreeNodes
"""
logger.debug(f"TreeNode löschen: {item.text(0)}") logger.debug(f"TreeNode löschen: {item.text(0)}")
# TODO: Bestätigungsdialog und Löschung implementieren
try:
# Prüfe ob ein Projekt geladen ist
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
# Hole das TreeNode-Objekt aus dem TreeWidgetItem
tree_node_obj = item.data(0, Qt.ItemDataRole.UserRole)
if not tree_node_obj or not isinstance(tree_node_obj, TreeNode):
QMessageBox.warning(self, "Warnung", "Kein gültiger Baumknoten gefunden.")
return
# Sammle alle XslFiles im Teilbaum vor der Löschung
xsl_files = self._collect_xsl_files_recursive(tree_node_obj)
sub_node_count = self._count_sub_nodes_recursive(tree_node_obj)
# Bestätigungsdialog
details = []
if sub_node_count > 0:
details.append(f"{sub_node_count} Unterknoten")
if xsl_files:
details.append(f"{len(xsl_files)} XSL-Datei(en)")
detail_text = f"\n\nEnthaltene Elemente: {', '.join(details)}." if details else ""
reply = QMessageBox.question(
self,
"Knoten löschen",
f"Möchten Sie den Knoten '{tree_node_obj.bez}' und alle untergeordneten "
f"Elemente aus dem Baum entfernen?\n\n"
f"XSL-Dateien werden nicht physisch gelöscht.{detail_text}",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
if reply != QMessageBox.StandardButton.Yes:
logger.debug("Löschung abgebrochen")
return
# Ermittle den Eltern-Kontext und entferne den TreeNode aus dem Datenmodell
# WICHTIG: Zuerst entfernen, damit _is_xml_xsl_combination_used_elsewhere()
# und _is_xml_file_used_elsewhere() den gelöschten Teilbaum nicht mehr sehen
parent_item = item.parent()
if parent_item:
parent_node = parent_item.data(0, Qt.ItemDataRole.UserRole)
if not parent_node or not isinstance(parent_node, TreeNode):
QMessageBox.warning(self, "Warnung", "Kein gültiger Eltern-Knoten gefunden.")
return
children_before = len(parent_node.children)
parent_node.children = [child for child in parent_node.children if child is not tree_node_obj]
if len(parent_node.children) == children_before:
QMessageBox.warning(self, "Warnung", "Knoten konnte nicht aus dem Eltern-Knoten entfernt werden.")
return
else:
# Top-Level-Knoten
nodes_before = len(self.pdf_project.nodes)
self.pdf_project.nodes = [node for node in self.pdf_project.nodes if node is not tree_node_obj]
if len(self.pdf_project.nodes) == nodes_before:
QMessageBox.warning(self, "Warnung", "Knoten konnte nicht aus dem Projekt entfernt werden.")
return
# PDF-Bereinigung für alle gesammelten XslFiles
total_deleted_pdfs = 0
for xsl_file_obj in xsl_files:
xsl_id = xsl_file_obj.id
for xml_file_obj in xsl_file_obj.xmls:
is_combination_used = self._is_xml_xsl_combination_used_elsewhere(
xml_file_obj.xml, xsl_id, xsl_file_obj
)
if not is_combination_used:
deleted_pdfs = self._delete_pdf_files_for_xml_xsl_combination(xml_file_obj.xml, xsl_id)
total_deleted_pdfs += deleted_pdfs
if total_deleted_pdfs > 0:
logger.info(f"{total_deleted_pdfs} PDF-Datei(en) für Knoten '{tree_node_obj.bez}' gelöscht")
# Sammle XML-Dateien, die nirgends mehr verwendet werden
unused_xml_files = []
seen_xml_paths = set()
for xsl_file_obj in xsl_files:
for xml_file_obj in xsl_file_obj.xmls:
xml_path_str = str(xml_file_obj.xml)
if xml_path_str in seen_xml_paths:
continue
seen_xml_paths.add(xml_path_str)
xml_file_path = Path(self.project.project_dir) / xml_file_obj.xml
if xml_file_path.exists():
is_used = self._is_xml_file_used_elsewhere(xml_file_obj.xml, xsl_file_obj)
if not is_used:
unused_xml_files.append((xml_file_obj, xml_file_path))
# Nicht mehr verwendete XML-Dateien physisch löschen
for xml_file_obj, xml_file_path in unused_xml_files:
try:
xml_file_path.unlink()
logger.info(f"Physische XML-Datei gelöscht: {xml_file_path}")
except Exception as e:
logger.warning(f"Konnte XML-Datei nicht löschen: {xml_file_path} - {e}")
# Speichere und aktualisiere
self._save_project_settings()
self._load_nodes_to_tree()
logger.info(f"TreeNode '{tree_node_obj.bez}' erfolgreich entfernt")
except Exception as e:
error_msg = f"Fehler beim Löschen des Knotens: {str(e)}"
logger.error(error_msg)
QMessageBox.critical(self, "Fehler", error_msg)
# Kontextmenü-Aktionen für XslFile # Kontextmenü-Aktionen für XslFile
def _add_xml_file_to_xsl(self, parent_item): def _add_xml_file_to_xsl(self, parent_item):