Kontextmenü: Neue Aktion "Alle Änderungen übernehmen" für TreeNode und XslFile

Neue Kontextmenü-Funktionalität zum Batch-Akzeptieren von Diff-PDFs:
- Neue Aktion "Alle Änderungen übernehmen" für TreeNode und XslFile
- Wird nur aktiviert, wenn mindestens eine Diff-PDF unter dem Knoten existiert
- Akzeptiert alle Diff-PDFs unter dem ausgewählten Knoten mit einem Klick

Verhalten:
- Verschiebt alle new-PDFs nach ref (alte ref-PDFs werden gelöscht)
- Löscht alle diff-PDFs
- Entfernt Diff-Icons bei allen betroffenen XML-Knoten
- Aktualisiert Diff-PDF-Zähler auf allen übergeordneten Ebenen
- Zeigt Bestätigungsdialog mit Anzahl der zu akzeptierenden Änderungen
- Zeigt Erfolgsmeldung nach Abschluss

Viewer-Behandlung:
- KEINE Diff-PDF wird in den Viewer geladen
- Falls eine akzeptierte Diff-PDF gerade im Viewer angezeigt wird, wird der Viewer geleert
- Kein XML-Knoten wird im TreeWidget ausgewählt

Bugfixes:
- XSL-ID wird korrekt als "2002_1_128" statt "(2002, 1, 128)" formatiert
- Relative Pfade werden für xml_item_map verwendet (statt absolute Pfade)
- Diff-Icons werden jetzt korrekt entfernt

Implementierungsdetails:
- _collect_all_diff_pdfs_under_node(): Sammelt alle Diff-PDFs unter TreeNode oder XslFile
- _accept_single_diff_pdf(): Akzeptiert eine einzelne Diff-PDF ohne Viewer-Update
- _accept_all_changes_under_node(): Handler für Batch-Accept-Operation
- Kontextmenü-Integration in _create_context_menu_for_type()
- Logging-Konfiguration in main.py hinzugefügt (DEBUG-Level)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-16 21:38:59 +01:00
parent 091096270a
commit c27c649142
2 changed files with 232 additions and 0 deletions
+8
View File
@@ -1,4 +1,5 @@
import sys
import logging
from PySide6.QtWidgets import QApplication
@@ -9,6 +10,13 @@ from conf import app_settings
def main():
"""Haupteinstiegspunkt der Anwendung."""
# Logging konfigurieren
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%H:%M:%S'
)
# QApplication-Instanz erstellen
app = QApplication(sys.argv)
+224
View File
@@ -989,6 +989,18 @@ class MainWindow(QMainWindow):
menu.addSeparator()
# Aktion "Alle Änderungen übernehmen" (nur aktiv wenn Diff-PDFs vorhanden)
diff_pdfs = self._collect_all_diff_pdfs_under_node(tree_node_obj, item) if tree_node_obj else []
has_diff_pdfs = len(diff_pdfs) > 0
action_accept_all = QAction("Alle Änderungen übernehmen", self)
action_accept_all.setIcon(QIcon(QIcon.fromTheme("emblem-default")))
action_accept_all.triggered.connect(lambda: self._accept_all_changes_under_node(item))
action_accept_all.setEnabled(has_diff_pdfs)
menu.addAction(action_accept_all)
menu.addSeparator()
action_edit = QAction("Bearbeiten", self)
action_edit.setIcon(QIcon(QIcon.fromTheme(QIcon.ThemeIcon.DocumentProperties)))
action_edit.triggered.connect(lambda: self._edit_tree_node(item))
@@ -1026,6 +1038,18 @@ class MainWindow(QMainWindow):
menu.addSeparator()
# Aktion "Alle Änderungen übernehmen" (nur aktiv wenn Diff-PDFs vorhanden)
diff_pdfs = self._collect_all_diff_pdfs_under_node(xsl_file_obj, item) if xsl_file_obj else []
has_diff_pdfs = len(diff_pdfs) > 0
action_accept_all = QAction("Alle Änderungen übernehmen", self)
action_accept_all.setIcon(QIcon(QIcon.fromTheme("emblem-default")))
action_accept_all.triggered.connect(lambda: self._accept_all_changes_under_node(item))
action_accept_all.setEnabled(has_diff_pdfs)
menu.addAction(action_accept_all)
menu.addSeparator()
action_edit = QAction("Bearbeiten", self)
action_edit.setIcon(QIcon(QIcon.fromTheme(QIcon.ThemeIcon.DocumentProperties)))
action_edit.triggered.connect(lambda: self._edit_xsl_file(item))
@@ -3643,6 +3667,74 @@ class MainWindow(QMainWindow):
f"{successful_count} von {total_count} Transformationen erfolgreich\n{failed_count} fehlgeschlagen",
)
def _collect_all_diff_pdfs_under_node(
self, node_obj, item: QTreeWidgetItem
) -> list[tuple[Path, str, Path, Path, Path]]:
"""
Sammelt alle Diff-PDFs unter einem TreeNode oder XslFile.
Args:
node_obj: TreeNode oder XslFile Objekt
item: Das TreeWidgetItem des Knotens
Returns:
list[tuple]: Liste von (xml_file_path, xsl_id_str, diff_pdf_path, ref_pdf_path, new_pdf_path)
"""
diff_pdfs = []
if not self.project:
return diff_pdfs
diff_dir = self.project.project_dir / "diff"
ref_dir = self.project.project_dir / "ref"
new_dir = self.project.project_dir / "new"
if not diff_dir.exists():
return diff_pdfs
if isinstance(node_obj, TreeNode):
# Sammle alle XSL/XML-Paare rekursiv
xsl_xml_pairs = self._collect_all_xsl_xml_pairs_recursive(node_obj, item)
for xsl_file_obj, xml_file_obj, xsl_file_item in xsl_xml_pairs:
# Baue XML-Pfad auf
xml_file_path = self.project.project_dir / xml_file_obj.xml
# Baue PDF-Namen
xml_stem = xml_file_path.stem
xsl_id_str = "_".join(str(x) for x in xsl_file_obj.id) if xsl_file_obj.id else ""
pdf_basename = f"{xml_stem}_xsl_{xsl_id_str}.pdf"
diff_pdf_path = diff_dir / pdf_basename
ref_pdf_path = ref_dir / pdf_basename
new_pdf_path = new_dir / pdf_basename
# Prüfe ob Diff-PDF existiert
if diff_pdf_path.exists():
diff_pdfs.append((xml_file_path, xsl_id_str, diff_pdf_path, ref_pdf_path, new_pdf_path))
elif isinstance(node_obj, XslFile):
# Sammle alle XML-Dateien dieser XslFile
xsl_id_str = "_".join(str(x) for x in node_obj.id) if node_obj.id else ""
for xml_file_obj in node_obj.xmls:
# Baue XML-Pfad auf
xml_file_path = self.project.project_dir / xml_file_obj.xml
# Baue PDF-Namen
xml_stem = xml_file_path.stem
pdf_basename = f"{xml_stem}_xsl_{xsl_id_str}.pdf"
diff_pdf_path = diff_dir / pdf_basename
ref_pdf_path = ref_dir / pdf_basename
new_pdf_path = new_dir / pdf_basename
# Prüfe ob Diff-PDF existiert
if diff_pdf_path.exists():
diff_pdfs.append((xml_file_path, xsl_id_str, diff_pdf_path, ref_pdf_path, new_pdf_path))
return diff_pdfs
def _find_next_diff_pdf(self) -> tuple[Path, str] | None:
"""
Findet die nächste Diff-PDF im Projekt.
@@ -3680,6 +3772,138 @@ class MainWindow(QMainWindow):
return None
def _accept_single_diff_pdf(
self, xml_file_path: Path, xsl_id_str: str, diff_pdf_path: Path, ref_pdf_path: Path, new_pdf_path: Path
) -> bool:
"""
Akzeptiert eine einzelne Diff-PDF ohne Viewer-Update.
Args:
xml_file_path: Pfad zur XML-Datei
xsl_id_str: XSL-ID als String
diff_pdf_path: Pfad zur Diff-PDF
ref_pdf_path: Pfad zur Ref-PDF
new_pdf_path: Pfad zur New-PDF
Returns:
bool: True wenn erfolgreich, False bei Fehler
"""
try:
pdf_basename = diff_pdf_path.name
# Prüfe ob new-PDF existiert
if not new_pdf_path.exists():
logger.warning(f"New-PDF nicht gefunden: {pdf_basename}")
return False
# Lösche alte ref-PDF falls vorhanden
if ref_pdf_path.exists():
ref_pdf_path.unlink()
logger.debug(f"Alte Ref-PDF gelöscht: {ref_pdf_path}")
# Verschiebe new-PDF nach ref
shutil.move(str(new_pdf_path), str(ref_pdf_path))
logger.debug(f"New-PDF verschoben nach Ref: {new_pdf_path} -> {ref_pdf_path}")
# Lösche diff-PDF
if diff_pdf_path.exists():
diff_pdf_path.unlink()
logger.debug(f"Diff-PDF gelöscht: {diff_pdf_path}")
# Diff-Icon beim XML-Knoten entfernen
# WICHTIG: xml_item_map verwendet relative Pfade, nicht absolute!
xml_relative_path = xml_file_path.relative_to(self.project.project_dir)
map_key = f"{xml_relative_path}|{xsl_id_str}"
if map_key in self.xml_item_map:
tree_item = self.xml_item_map[map_key]
# Entferne Icon-Widget aus Spalte 2
tree_item.setData(2, Qt.ItemDataRole.UserRole, None)
self.ui.treeWidget.setItemWidget(tree_item, 2, None)
logger.info(f"Änderungen akzeptiert für: {pdf_basename}")
return True
except Exception as e:
logger.error(f"Fehler beim Akzeptieren von {pdf_basename}: {e}")
return False
def _accept_all_changes_under_node(self, item: QTreeWidgetItem):
"""
Akzeptiert alle Diff-PDFs unter einem TreeNode oder XslFile.
Leert den Viewer, falls eine der akzeptierten PDFs gerade angezeigt wird.
Args:
item: Das TreeWidgetItem des TreeNode oder XslFile
"""
try:
# Hole Node-Objekt
node_obj = item.data(0, Qt.ItemDataRole.UserRole)
if not node_obj:
logger.warning("Kein Node-Objekt gefunden")
return
# Sammle alle Diff-PDFs unter diesem Knoten
diff_pdfs = self._collect_all_diff_pdfs_under_node(node_obj, item)
if not diff_pdfs:
QMessageBox.information(self, "Info", "Keine Diff-PDFs unter diesem Knoten gefunden")
return
# Frage Benutzer um Bestätigung
node_name = node_obj.bez if hasattr(node_obj, "bez") else "Unbekannt"
reply = QMessageBox.question(
self,
"Alle Änderungen übernehmen",
f"Möchten Sie wirklich alle {len(diff_pdfs)} Änderungen unter '{node_name}' übernehmen?\n\n"
f"Dies verschiebt alle new-PDFs nach ref und löscht die diff-PDFs.",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
if reply != QMessageBox.StandardButton.Yes:
return
# Merke, ob eine der zu akzeptierenden PDFs gerade im Viewer angezeigt wird
viewer_needs_clearing = False
if self.current_diff_xml_path and self.current_diff_xsl_id:
for xml_file_path, xsl_id_str, diff_pdf_path, ref_pdf_path, new_pdf_path in diff_pdfs:
if xml_file_path == self.current_diff_xml_path and xsl_id_str == self.current_diff_xsl_id:
viewer_needs_clearing = True
break
# Akzeptiere alle Diff-PDFs
successful_count = 0
for xml_file_path, xsl_id_str, diff_pdf_path, ref_pdf_path, new_pdf_path in diff_pdfs:
if self._accept_single_diff_pdf(xml_file_path, xsl_id_str, diff_pdf_path, ref_pdf_path, new_pdf_path):
successful_count += 1
# Aktualisiere Diff-PDF-Anzahl auf übergeordneten Ebenen
self._update_all_diff_pdf_counts()
# Leere Viewer falls nötig
if viewer_needs_clearing:
self._clear_pdf_viewer()
# Zeige Erfolgsmeldung
if successful_count == len(diff_pdfs):
logger.info(f"Alle {successful_count} Änderungen erfolgreich übernommen")
QMessageBox.information(
self, "Erfolg", f"Alle {successful_count} Änderungen wurden erfolgreich übernommen"
)
else:
failed_count = len(diff_pdfs) - successful_count
logger.warning(f"{successful_count}/{len(diff_pdfs)} Änderungen übernommen, {failed_count} fehlgeschlagen")
QMessageBox.warning(
self,
"Teilweise erfolgreich",
f"{successful_count} von {len(diff_pdfs)} Änderungen übernommen\n{failed_count} fehlgeschlagen",
)
except Exception as e:
logger.error(f"Fehler beim Übernehmen aller Änderungen: {e}")
QMessageBox.critical(self, "Fehler", f"Fehler beim Übernehmen der Änderungen:\n{str(e)}")
def _clear_pdf_viewer(self):
"""Leert den PDF-Viewer und alle Thumbnails."""
# Entferne Widgets aus Layouts