Refactor: MainWindow in 7 Mixins aufgeteilt (80% Code-Reduktion)
MainWindow.py von 5025 auf 983 Zeilen reduziert durch Extraktion in: - TreeManagerMixin: Baumstruktur-Verwaltung (~1136 Zeilen) - PdfViewerMixin: PDF-Anzeige und Rendering - WorkerPoolMixin: Saxon/FOP Worker-Pool-Verwaltung - DatabaseMixin: PostgreSQL-Operationen - DragDropMixin: Drag-and-Drop für XML-Dateien - HashCalculationMixin: blake2b Hash-Berechnung - TransformationMixin: XSL-Transformationen Zusätzlich Thread-Klassen in threads.py ausgelagert. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,680 @@
|
||||
"""
|
||||
TransformationMixin - Mixin für XSL-Transformationen.
|
||||
|
||||
Dieses Mixin enthält alle Methoden zur Durchführung und Verwaltung von
|
||||
XSL-Transformationen für das MainWindow.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QMessageBox, QProgressBar, QTreeWidgetItem
|
||||
|
||||
from conf import app_settings, TreeNode, XslFile, XmlFile
|
||||
from transform import TransformationJob
|
||||
from ui.threads import TransformationThread
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TransformationMixin:
|
||||
"""
|
||||
Mixin für XSL-Transformationen.
|
||||
|
||||
Dieses Mixin wird von MainWindow verwendet und erwartet folgende Attribute:
|
||||
- self.project: Das aktuelle Projekt
|
||||
- self.ui: Das UI-Objekt mit treeWidget, statusBar
|
||||
- self.transformation_thread: Thread für Transformationen
|
||||
- self.transformation_progress_bar: QProgressBar für Transformation-Fortschritt
|
||||
- self.xml_item_map: Mapping von xml_path|xsl_id zu TreeWidgetItems
|
||||
- self.last_saxon_metrics: Gecachte Saxon-Worker-Pool-Metriken
|
||||
- self.last_fop_metrics: Gecachte FOP-Worker-Pool-Metriken
|
||||
|
||||
Erwartet folgende Methoden von anderen Mixins:
|
||||
- self._initialize_saxon_worker_pool(): Von WorkerPoolMixin
|
||||
- self._initialize_fop_worker_pool(): Von WorkerPoolMixin
|
||||
- self._shutdown_saxon_worker_pool(): Von WorkerPoolMixin
|
||||
- self._shutdown_fop_worker_pool(): Von WorkerPoolMixin
|
||||
- self._create_centered_progress_bar(): Von TreeManagerMixin
|
||||
- self._create_centered_diff_icon(): Von TreeManagerMixin
|
||||
- self._collect_parent_params(): Von TreeManagerMixin
|
||||
- self._update_diff_icons_for_existing_pdfs(): Von MainWindow
|
||||
"""
|
||||
|
||||
def _show_transformation_progress_bar(self, total_jobs: int):
|
||||
"""
|
||||
Zeigt einen Progressbar in der Statusbar für Transformationen.
|
||||
|
||||
Args:
|
||||
total_jobs: Gesamtanzahl der Transformations-Jobs
|
||||
"""
|
||||
if self.transformation_progress_bar is None:
|
||||
self.transformation_progress_bar = QProgressBar()
|
||||
self.transformation_progress_bar.setMaximumHeight(20)
|
||||
self.transformation_progress_bar.setMaximumWidth(300)
|
||||
|
||||
self.transformation_progress_bar.setMinimum(0)
|
||||
self.transformation_progress_bar.setMaximum(total_jobs)
|
||||
self.transformation_progress_bar.setValue(0)
|
||||
self.transformation_progress_bar.setFormat("%v/%m Jobs")
|
||||
|
||||
# Füge Progressbar zur Statusbar hinzu
|
||||
self.statusBar().addPermanentWidget(self.transformation_progress_bar)
|
||||
self.transformation_progress_bar.show()
|
||||
|
||||
def _hide_transformation_progress_bar(self):
|
||||
"""Versteckt und entfernt den Transformation-Progressbar aus der Statusbar."""
|
||||
if self.transformation_progress_bar:
|
||||
self.statusBar().removeWidget(self.transformation_progress_bar)
|
||||
self.transformation_progress_bar.hide()
|
||||
|
||||
def _update_transformation_progress(self):
|
||||
"""Aktualisiert den Transformation-Progressbar um einen Schritt."""
|
||||
if self.transformation_progress_bar:
|
||||
current_value = self.transformation_progress_bar.value()
|
||||
self.transformation_progress_bar.setValue(current_value + 1)
|
||||
|
||||
def _transform_xml_file(self, item: QTreeWidgetItem, force: bool = False):
|
||||
"""
|
||||
Transformiert eine einzelne XML-Datei.
|
||||
|
||||
Args:
|
||||
item: Das TreeWidgetItem der XML-Datei
|
||||
force: Wenn True, wird Transformation auch bei aktuellem Output durchgeführt
|
||||
"""
|
||||
try:
|
||||
# Hole XslFile vom Parent-Item
|
||||
parent_item = item.parent()
|
||||
if not parent_item:
|
||||
logger.error("XML-Datei hat kein Parent-Item (XslFile)")
|
||||
QMessageBox.warning(self, "Fehler", "XML-Datei hat keine zugeordnete XSL-Datei")
|
||||
return
|
||||
|
||||
xsl_file_obj = parent_item.data(0, Qt.ItemDataRole.UserRole)
|
||||
if not isinstance(xsl_file_obj, XslFile):
|
||||
logger.error(f"Parent-Item ist kein XslFile: {type(xsl_file_obj)}")
|
||||
QMessageBox.warning(self, "Fehler", "Konnte XSL-Datei nicht ermitteln")
|
||||
return
|
||||
|
||||
# Hole XmlFile-Objekt
|
||||
xml_file_obj = item.data(0, Qt.ItemDataRole.UserRole)
|
||||
if not isinstance(xml_file_obj, XmlFile):
|
||||
logger.error(f"Item ist kein XmlFile: {type(xml_file_obj)}")
|
||||
QMessageBox.warning(self, "Fehler", "Konnte XML-Datei nicht ermitteln")
|
||||
return
|
||||
|
||||
# Erstelle TransformationJob mit TreeWidgetItem-Kontext für Parameter-Sammlung
|
||||
job = self._create_transformation_job(xsl_file_obj, xml_file_obj, parent_item)
|
||||
if not job:
|
||||
return
|
||||
|
||||
# Starte Transformation in separatem Thread
|
||||
self._start_transformation([job], force=force)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Transformieren der XML-Datei: {e}")
|
||||
QMessageBox.critical(self, "Fehler", f"Fehler beim Transformieren: {str(e)}")
|
||||
|
||||
def _transform_xsl_file(self, item: QTreeWidgetItem, force: bool = False):
|
||||
"""
|
||||
Transformiert alle XML-Dateien einer XSL-Datei.
|
||||
|
||||
Args:
|
||||
item: Das TreeWidgetItem der XSL-Datei
|
||||
force: Wenn True, wird Transformation auch bei aktuellem Output durchgeführt
|
||||
"""
|
||||
try:
|
||||
# Hole XslFile-Objekt
|
||||
xsl_file_obj = item.data(0, Qt.ItemDataRole.UserRole)
|
||||
if not isinstance(xsl_file_obj, XslFile):
|
||||
logger.error(f"Item ist kein XslFile: {type(xsl_file_obj)}")
|
||||
QMessageBox.warning(self, "Fehler", "Konnte XSL-Datei nicht ermitteln")
|
||||
return
|
||||
|
||||
# Prüfe ob XML-Dateien vorhanden sind
|
||||
if not xsl_file_obj.xmls:
|
||||
QMessageBox.information(self, "Info", "Keine XML-Dateien zugeordnet")
|
||||
return
|
||||
|
||||
# Erstelle TransformationJobs für alle XML-Dateien
|
||||
jobs = []
|
||||
for xml_file_obj in xsl_file_obj.xmls:
|
||||
# Übergebe das XslFile-TreeWidgetItem für Parameter-Sammlung
|
||||
job = self._create_transformation_job(xsl_file_obj, xml_file_obj, item)
|
||||
if job:
|
||||
jobs.append(job)
|
||||
|
||||
if not jobs:
|
||||
QMessageBox.warning(self, "Fehler", "Konnte keine Transformations-Jobs erstellen")
|
||||
return
|
||||
|
||||
# Starte Transformation in separatem Thread
|
||||
self._start_transformation(jobs, force=force)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Transformieren der XSL-Datei: {e}")
|
||||
QMessageBox.critical(self, "Fehler", f"Fehler beim Transformieren: {str(e)}")
|
||||
|
||||
def _count_diff_pdfs_under_node(self, node: TreeNode | XslFile, node_item: QTreeWidgetItem) -> int:
|
||||
"""
|
||||
Zählt die Anzahl der existierenden Diff-PDFs unter einem Knoten.
|
||||
|
||||
Args:
|
||||
node: TreeNode oder XslFile Objekt
|
||||
node_item: Das TreeWidgetItem des Knotens
|
||||
|
||||
Returns:
|
||||
int: Anzahl der existierenden Diff-PDF-Dateien
|
||||
"""
|
||||
count = 0
|
||||
|
||||
if isinstance(node, XslFile):
|
||||
# Für XslFile: Zähle Diff-PDFs für jede XML-Datei
|
||||
if not self.project:
|
||||
return 0
|
||||
|
||||
diff_dir = self.project.project_dir / "diff"
|
||||
xsl_id_str = "_".join(str(x) for x in node.id) if node.id else ""
|
||||
|
||||
for xml_file_obj in node.xmls:
|
||||
xml_stem = xml_file_obj.xml.stem
|
||||
pdf_basename = f"{xml_stem}_xsl_{xsl_id_str}.pdf"
|
||||
diff_pdf_path = diff_dir / pdf_basename
|
||||
|
||||
if diff_pdf_path.exists():
|
||||
count += 1
|
||||
|
||||
elif isinstance(node, TreeNode):
|
||||
# Für TreeNode: Rekursiv alle Kinder durchgehen
|
||||
for i in range(node_item.childCount()):
|
||||
child_item = node_item.child(i)
|
||||
child_node = child_item.data(0, Qt.ItemDataRole.UserRole)
|
||||
|
||||
if isinstance(child_node, (XslFile, TreeNode)):
|
||||
count += self._count_diff_pdfs_under_node(child_node, child_item)
|
||||
|
||||
return count
|
||||
|
||||
def _update_diff_pdf_counts_recursive(self, tree_item: QTreeWidgetItem):
|
||||
"""
|
||||
Aktualisiert rekursiv die Diff-PDF-Anzahl in Spalte 2 für alle TreeNode und XslFile Items.
|
||||
|
||||
Args:
|
||||
tree_item: Das TreeWidgetItem (kann Root oder beliebiger Knoten sein)
|
||||
"""
|
||||
node = tree_item.data(0, Qt.ItemDataRole.UserRole)
|
||||
|
||||
# Aktualisiere nur für TreeNode und XslFile, nicht für XmlFile
|
||||
if isinstance(node, (TreeNode, XslFile)):
|
||||
count = self._count_diff_pdfs_under_node(node, tree_item)
|
||||
tree_item.setText(2, str(count) if count > 0 else "")
|
||||
|
||||
# Rekursiv für alle Kinder
|
||||
for i in range(tree_item.childCount()):
|
||||
child_item = tree_item.child(i)
|
||||
self._update_diff_pdf_counts_recursive(child_item)
|
||||
|
||||
def _update_all_diff_pdf_counts(self):
|
||||
"""
|
||||
Aktualisiert die Diff-PDF-Anzahl für alle Knoten im TreeWidget.
|
||||
"""
|
||||
root = self.ui.treeWidget.invisibleRootItem()
|
||||
for i in range(root.childCount()):
|
||||
self._update_diff_pdf_counts_recursive(root.child(i))
|
||||
|
||||
def _has_xml_files_recursive(self, node: TreeNode) -> bool:
|
||||
"""
|
||||
Prüft rekursiv, ob unter einem TreeNode mindestens eine XML-Datei vorhanden ist.
|
||||
|
||||
Args:
|
||||
node: Der TreeNode
|
||||
|
||||
Returns:
|
||||
bool: True wenn mindestens eine XML-Datei gefunden wurde
|
||||
"""
|
||||
if not hasattr(node, "children") or not node.children:
|
||||
return False
|
||||
|
||||
for child in node.children:
|
||||
if isinstance(child, XslFile):
|
||||
if child.xmls:
|
||||
return True
|
||||
elif isinstance(child, TreeNode):
|
||||
if self._has_xml_files_recursive(child):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _collect_all_xsl_xml_pairs_recursive(
|
||||
self, tree_node: TreeNode, tree_item: QTreeWidgetItem
|
||||
) -> list[tuple[XslFile, XmlFile, QTreeWidgetItem]]:
|
||||
"""
|
||||
Sammelt rekursiv alle (XslFile, XmlFile, XslFileItem) Tupel unter einem TreeNode.
|
||||
|
||||
Args:
|
||||
tree_node: Der TreeNode
|
||||
tree_item: Das TreeWidgetItem des TreeNode
|
||||
|
||||
Returns:
|
||||
list: Liste von (XslFile, XmlFile, XslFileItem) Tupeln
|
||||
"""
|
||||
pairs = []
|
||||
|
||||
if not hasattr(tree_node, "children") or not tree_node.children:
|
||||
return pairs
|
||||
|
||||
# Durchlaufe alle Kinder des TreeNode
|
||||
for i in range(tree_item.childCount()):
|
||||
child_item = tree_item.child(i)
|
||||
child_node = child_item.data(0, Qt.ItemDataRole.UserRole)
|
||||
|
||||
if isinstance(child_node, XslFile):
|
||||
# XslFile gefunden - sammle alle XML-Dateien
|
||||
for xml_file_obj in child_node.xmls:
|
||||
pairs.append((child_node, xml_file_obj, child_item))
|
||||
|
||||
elif isinstance(child_node, TreeNode):
|
||||
# Rekursiv in Unterknoten suchen
|
||||
pairs.extend(self._collect_all_xsl_xml_pairs_recursive(child_node, child_item))
|
||||
|
||||
return pairs
|
||||
|
||||
def _transform_tree_node(self, item: QTreeWidgetItem, force: bool = False):
|
||||
"""
|
||||
Transformiert alle XML-Dateien unter einem TreeNode (rekursiv).
|
||||
|
||||
Args:
|
||||
item: Das TreeWidgetItem des TreeNode
|
||||
force: Wenn True, wird Transformation auch bei aktuellem Output durchgeführt
|
||||
"""
|
||||
try:
|
||||
# Hole TreeNode-Objekt
|
||||
tree_node_obj = item.data(0, Qt.ItemDataRole.UserRole)
|
||||
if not isinstance(tree_node_obj, TreeNode):
|
||||
logger.error(f"Item ist kein TreeNode: {type(tree_node_obj)}")
|
||||
QMessageBox.warning(self, "Fehler", "Konnte TreeNode nicht ermitteln")
|
||||
return
|
||||
|
||||
# Prüfe ob XML-Dateien vorhanden sind
|
||||
if not self._has_xml_files_recursive(tree_node_obj):
|
||||
QMessageBox.information(self, "Info", "Keine XML-Dateien unter diesem Knoten gefunden")
|
||||
return
|
||||
|
||||
# Sammle alle XSL/XML-Paare rekursiv
|
||||
xsl_xml_pairs = self._collect_all_xsl_xml_pairs_recursive(tree_node_obj, item)
|
||||
|
||||
if not xsl_xml_pairs:
|
||||
QMessageBox.information(self, "Info", "Keine XML-Dateien gefunden")
|
||||
return
|
||||
|
||||
# Erstelle TransformationJobs für alle XML-Dateien
|
||||
jobs = []
|
||||
for xsl_file_obj, xml_file_obj, xsl_file_item in xsl_xml_pairs:
|
||||
# Übergebe das XslFile-TreeWidgetItem für Parameter-Sammlung
|
||||
job = self._create_transformation_job(xsl_file_obj, xml_file_obj, xsl_file_item)
|
||||
if job:
|
||||
jobs.append(job)
|
||||
|
||||
if not jobs:
|
||||
QMessageBox.warning(self, "Fehler", "Konnte keine Transformations-Jobs erstellen")
|
||||
return
|
||||
|
||||
logger.info(f"Starte Transformation für {len(jobs)} XML-Dateien unter TreeNode '{tree_node_obj.bez}'")
|
||||
|
||||
# Starte Transformation in separatem Thread
|
||||
self._start_transformation(jobs, force=force)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Transformieren des TreeNode: {e}")
|
||||
QMessageBox.critical(self, "Fehler", f"Fehler beim Transformieren: {str(e)}")
|
||||
|
||||
def _create_transformation_job(
|
||||
self, xsl_file_obj: XslFile, xml_file_obj: XmlFile, xsl_file_item: QTreeWidgetItem | None = None
|
||||
) -> TransformationJob | None:
|
||||
"""
|
||||
Erstellt einen TransformationJob für eine XML/XSL-Kombination.
|
||||
|
||||
Args:
|
||||
xsl_file_obj: Das XslFile-Objekt
|
||||
xml_file_obj: Das XmlFile-Objekt
|
||||
xsl_file_item: Optional das TreeWidgetItem des XslFile für hierarchische Parameter-Sammlung
|
||||
|
||||
Returns:
|
||||
TransformationJob oder None bei Fehler
|
||||
"""
|
||||
try:
|
||||
if not self.project:
|
||||
QMessageBox.warning(self, "Fehler", "Kein Projekt geöffnet")
|
||||
return None
|
||||
|
||||
# Hole Tool-Konfigurationen aus app_settings
|
||||
java_vm = next((jvm for jvm in app_settings.java_vms if jvm.id == self.project.java_vm_id), None)
|
||||
saxon_jar = next((sj for sj in app_settings.saxon_jars if sj.id == self.project.saxon_jar_id), None)
|
||||
apache_fop = next((af for af in app_settings.apache_fops if af.id == self.project.apache_fop_id), None)
|
||||
diff_pdf = next((dp for dp in app_settings.diff_pdfs if dp.id == self.project.diff_pdf_id), None)
|
||||
xsl_dir = next((xd for xd in app_settings.xsl_dirs if xd.id == self.project.xsl_dir_id), None)
|
||||
|
||||
# Prüfe ob alle Konfigurationen vorhanden sind
|
||||
if not all([java_vm, saxon_jar, apache_fop, diff_pdf, xsl_dir]):
|
||||
missing = []
|
||||
if not java_vm:
|
||||
missing.append("Java VM")
|
||||
if not saxon_jar:
|
||||
missing.append("Saxon JAR")
|
||||
if not apache_fop:
|
||||
missing.append("Apache FOP")
|
||||
if not diff_pdf:
|
||||
missing.append("diff-pdf")
|
||||
if not xsl_dir:
|
||||
missing.append("XSL-Verzeichnis")
|
||||
|
||||
QMessageBox.warning(
|
||||
self, "Fehlende Konfiguration", f"Folgende Konfigurationen fehlen: {', '.join(missing)}"
|
||||
)
|
||||
return None
|
||||
|
||||
# Zusätzliche Sicherheitsprüfung für path_to_binary_file Attribute
|
||||
if java_vm is None or not hasattr(java_vm, "path_to_binary_file") or java_vm.path_to_binary_file is None:
|
||||
QMessageBox.warning(self, "Konfigurationsfehler", "Java VM Pfad ist nicht konfiguriert")
|
||||
return None
|
||||
|
||||
if saxon_jar is None or not hasattr(saxon_jar, "path_to_jar_file") or saxon_jar.path_to_jar_file is None:
|
||||
QMessageBox.warning(self, "Konfigurationsfehler", "Saxon JAR Pfad ist nicht konfiguriert")
|
||||
return None
|
||||
|
||||
if apache_fop is None or not hasattr(apache_fop, "path_to_dir") or apache_fop.path_to_dir is None:
|
||||
QMessageBox.warning(self, "Konfigurationsfehler", "Apache FOP Pfad ist nicht konfiguriert")
|
||||
return None
|
||||
|
||||
if diff_pdf is None or not hasattr(diff_pdf, "path_to_binary_file") or diff_pdf.path_to_binary_file is None:
|
||||
QMessageBox.warning(self, "Konfigurationsfehler", "diff-pdf Pfad ist nicht konfiguriert")
|
||||
return None
|
||||
|
||||
if xsl_dir is None or not hasattr(xsl_dir, "path_to_root_dir") or xsl_dir.path_to_root_dir is None:
|
||||
QMessageBox.warning(self, "Konfigurationsfehler", "XSL-Verzeichnis Pfad ist nicht konfiguriert")
|
||||
return None
|
||||
|
||||
# Erstelle absoluten Pfad zur XSL-Datei
|
||||
xsl_file_abs = xsl_dir.path_to_root_dir / xsl_file_obj.xsl_file
|
||||
|
||||
# Sammle XSLT-Parameter hierarchisch (TreeNode-Eltern → XslFile)
|
||||
xslt_params = {}
|
||||
|
||||
# 1. Sammle Parameter von übergeordneten TreeNodes (falls TreeWidgetItem verfügbar)
|
||||
if xsl_file_item is not None:
|
||||
parent_params = self._collect_parent_params(xsl_file_item)
|
||||
xslt_params.update(parent_params)
|
||||
logger.debug(f"Hierarchische Parameter gesammelt: {parent_params}")
|
||||
else:
|
||||
logger.warning(
|
||||
"Kein TreeWidgetItem-Kontext verfügbar - "
|
||||
"übergeordnete TreeNode-Parameter werden nicht berücksichtigt"
|
||||
)
|
||||
|
||||
# 2. Überschreibe mit XslFile-eigenen Parametern (höchste Priorität)
|
||||
xslt_params.update(xsl_file_obj.xslt_params)
|
||||
|
||||
logger.info(f"Finale XSLT-Parameter für {xml_file_obj.xml} mit {xsl_file_obj.bez}: {xslt_params}")
|
||||
|
||||
# Erstelle TransformationJob
|
||||
job = TransformationJob(
|
||||
project_dir=self.project.project_dir,
|
||||
xml_file=xml_file_obj.xml,
|
||||
xsl_file=xsl_file_abs,
|
||||
xslt_params=xslt_params,
|
||||
java_vm_path=java_vm.path_to_binary_file,
|
||||
saxon_jar_path=saxon_jar.path_to_jar_file,
|
||||
apache_fop_dir=apache_fop.path_to_dir,
|
||||
diff_pdf_path=diff_pdf.path_to_binary_file,
|
||||
diff_pdf_params=diff_pdf.default_params,
|
||||
xsl_id=xsl_file_obj.id,
|
||||
fop_config_dir=self.project.fop_config_dir,
|
||||
)
|
||||
|
||||
return job
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Erstellen des TransformationJobs: {e}")
|
||||
QMessageBox.critical(self, "Fehler", f"Fehler beim Erstellen des Jobs: {str(e)}")
|
||||
return None
|
||||
|
||||
def _start_transformation(self, jobs: list[TransformationJob], force: bool = False):
|
||||
"""
|
||||
Startet die Transformation in einem separaten Thread.
|
||||
|
||||
Args:
|
||||
jobs: Liste der TransformationJobs
|
||||
force: Wenn True, werden alle Jobs ausgeführt (ignoriert Up-to-Date)
|
||||
"""
|
||||
try:
|
||||
# Prüfe ob bereits ein Thread läuft
|
||||
if self.transformation_thread and self.transformation_thread.isRunning():
|
||||
QMessageBox.warning(self, "Warnung", "Es läuft bereits eine Transformation")
|
||||
return
|
||||
|
||||
# Erstelle und konfiguriere Thread
|
||||
self.transformation_thread = TransformationThread(jobs, force=force, max_workers=app_settings.max_workers)
|
||||
|
||||
# Verbinde Signale
|
||||
self.transformation_thread.job_started.connect(self._on_transformation_job_started)
|
||||
self.transformation_thread.job_finished.connect(self._on_transformation_job_finished)
|
||||
self.transformation_thread.job_error.connect(self._on_transformation_job_error)
|
||||
self.transformation_thread.all_jobs_finished.connect(self._on_all_transformations_finished)
|
||||
|
||||
# Zeige Progressbar
|
||||
self._show_transformation_progress_bar(len(jobs))
|
||||
|
||||
# Initialisiere Worker-Pools (lazy loading - nur wenn benötigt)
|
||||
self._initialize_saxon_worker_pool()
|
||||
self._initialize_fop_worker_pool()
|
||||
|
||||
# Erfasse RAM-Verbrauch vor Transformation
|
||||
import transform
|
||||
|
||||
if transform._saxon_worker_pool:
|
||||
transform._saxon_worker_pool.capture_ram_before_transform()
|
||||
if transform._fop_worker_pool:
|
||||
transform._fop_worker_pool.capture_ram_before_transform()
|
||||
|
||||
# Starte Thread
|
||||
self.transformation_thread.start()
|
||||
|
||||
logger.info(f"Transformation von {len(jobs)} Job(s) gestartet (force={force})")
|
||||
self.statusBar().showMessage(f"Transformation von {len(jobs)} Job(s) gestartet...")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Starten der Transformation: {e}")
|
||||
QMessageBox.critical(self, "Fehler", f"Fehler beim Starten: {str(e)}")
|
||||
|
||||
def _expand_tree_item_parents(self, item: QTreeWidgetItem):
|
||||
"""
|
||||
Öffnet alle Eltern-Knoten eines Tree-Items rekursiv.
|
||||
|
||||
Args:
|
||||
item: Das Tree-Item, dessen Eltern geöffnet werden sollen
|
||||
"""
|
||||
if item is None:
|
||||
return
|
||||
|
||||
# Rekursiv alle Eltern öffnen
|
||||
parent = item.parent()
|
||||
while parent is not None:
|
||||
parent.setExpanded(True)
|
||||
parent = parent.parent()
|
||||
|
||||
def _on_transformation_job_started(self, xml_file_name: str, xsl_id_str: str):
|
||||
"""
|
||||
Signal-Handler: Ein Job wurde gestartet.
|
||||
|
||||
Args:
|
||||
xml_file_name: Name der XML-Datei
|
||||
xsl_id_str: XSL-ID als String (z.B. "2002_1_128")
|
||||
"""
|
||||
logger.info(f"Transformation gestartet: {xml_file_name} (XSL-ID: {xsl_id_str})")
|
||||
self.statusBar().showMessage(f"Transformiere: {xml_file_name}")
|
||||
|
||||
# Progress Bar anzeigen
|
||||
map_key = f"{xml_file_name}|{xsl_id_str}"
|
||||
if map_key not in self.xml_item_map and self.xml_item_map:
|
||||
# Zeige erste Keys zur Diagnose
|
||||
list(self.xml_item_map.keys())[:3]
|
||||
logger.info(f"Suche TreeWidget-Item für: '{map_key}'")
|
||||
logger.info(f"Map hat {len(self.xml_item_map)} Einträge")
|
||||
tree_item = self.xml_item_map.get(map_key)
|
||||
if tree_item:
|
||||
# Öffne alle Eltern-Knoten, damit der Benutzer den Fortschritt sehen kann
|
||||
self._expand_tree_item_parents(tree_item)
|
||||
|
||||
# Scrolle zum Item, damit es sichtbar ist
|
||||
self.ui.treeWidget.scrollToItem(tree_item)
|
||||
|
||||
# Entferne vorhandenes Widget (falls Icon vorhanden)
|
||||
self.ui.treeWidget.removeItemWidget(tree_item, 2)
|
||||
|
||||
# Erstelle und setze Progress Bar
|
||||
progress_widget, progress_bar = self._create_centered_progress_bar()
|
||||
self.ui.treeWidget.setItemWidget(tree_item, 2, progress_widget)
|
||||
|
||||
logger.debug(f"Progress Bar für {xml_file_name} gesetzt und Eltern-Knoten geöffnet")
|
||||
else:
|
||||
logger.warning(f"Kein TreeWidget-Item für {xml_file_name} gefunden")
|
||||
|
||||
def _on_transformation_job_finished(self, result: dict):
|
||||
"""
|
||||
Signal-Handler: Ein Job wurde abgeschlossen.
|
||||
|
||||
Args:
|
||||
result: Ergebnis-Dictionary
|
||||
"""
|
||||
# Aktualisiere Transformation-Progressbar
|
||||
self._update_transformation_progress()
|
||||
|
||||
xml_file = result.get("xml_file", "?")
|
||||
success = result.get("success", False)
|
||||
duration = result.get("duration", 0)
|
||||
|
||||
if success:
|
||||
logger.info(f"Transformation erfolgreich: {xml_file} ({duration:.2f}s)")
|
||||
pdfs_identical = result.get("pdfs_identical", False)
|
||||
if pdfs_identical:
|
||||
self.statusBar().showMessage(f"✓ {xml_file} - PDFs identisch ({duration:.2f}s)", 3000)
|
||||
else:
|
||||
self.statusBar().showMessage(f"⚠ {xml_file} - Unterschiede gefunden ({duration:.2f}s)", 3000)
|
||||
else:
|
||||
logger.error(f"Transformation fehlgeschlagen: {xml_file}")
|
||||
# Zeige Fehlerdetails an
|
||||
steps = result.get("steps", {})
|
||||
error_msgs = []
|
||||
for step_name, step_info in steps.items():
|
||||
if not step_info.get("success", True):
|
||||
error_msgs.append(f"{step_name}: {step_info.get('message', 'Unbekannter Fehler')}")
|
||||
|
||||
error_text = "\n".join(error_msgs) if error_msgs else "Unbekannter Fehler"
|
||||
QMessageBox.critical(
|
||||
self, "Transformation fehlgeschlagen", f"XML-Datei: {xml_file}\n\nFehler:\n{error_text}"
|
||||
)
|
||||
|
||||
# Update Widget in Spalte 2: Entferne Progress Bar, zeige Icon wenn Diff-PDF existiert
|
||||
xml_file_str = result.get("xml_file", "")
|
||||
xsl_id = result.get("xsl_id", None)
|
||||
xsl_id_str = "_".join(str(x) for x in xsl_id) if xsl_id else ""
|
||||
map_key = f"{xml_file_str}|{xsl_id_str}"
|
||||
diff_pdf_str = result.get("diff_pdf", None)
|
||||
tree_item = self.xml_item_map.get(map_key)
|
||||
|
||||
if tree_item:
|
||||
# Entferne Progress Bar
|
||||
self.ui.treeWidget.removeItemWidget(tree_item, 2)
|
||||
|
||||
# Wenn Diff-PDF existiert, zeige Icon
|
||||
if diff_pdf_str and Path(diff_pdf_str).exists():
|
||||
xml_file_path = Path(xml_file_str)
|
||||
icon_widget = self._create_centered_diff_icon(xml_file_path, xsl_id_str)
|
||||
self.ui.treeWidget.setItemWidget(tree_item, 2, icon_widget)
|
||||
logger.debug(f"Diff-Icon für {xml_file_str} gesetzt")
|
||||
else:
|
||||
logger.debug(f"Keine Diff-PDF für {xml_file_str}, kein Icon gesetzt")
|
||||
|
||||
def _on_transformation_job_error(self, xml_file_name: str, xsl_id_str: str, error_message: str):
|
||||
"""
|
||||
Signal-Handler: Ein Job ist mit einem Fehler abgebrochen.
|
||||
|
||||
Args:
|
||||
xml_file_name: Name der XML-Datei
|
||||
xsl_id_str: XSL-ID als String
|
||||
error_message: Fehlermeldung
|
||||
"""
|
||||
# Aktualisiere Transformation-Progressbar
|
||||
self._update_transformation_progress()
|
||||
|
||||
logger.error(f"Transformation-Fehler bei {xml_file_name} (XSL-ID: {xsl_id_str}): {error_message}")
|
||||
QMessageBox.critical(self, "Fehler", f"Fehler bei {xml_file_name}:\n{error_message}")
|
||||
|
||||
# Entferne Progress Bar bei Fehler
|
||||
map_key = f"{xml_file_name}|{xsl_id_str}"
|
||||
tree_item = self.xml_item_map.get(map_key)
|
||||
if tree_item:
|
||||
self.ui.treeWidget.removeItemWidget(tree_item, 2)
|
||||
logger.debug(f"Progress Bar für {map_key} entfernt (Fehler)")
|
||||
|
||||
def _on_all_transformations_finished(self, successful_count: int, total_count: int, total_duration: float):
|
||||
"""
|
||||
Signal-Handler: Alle Jobs wurden abgeschlossen.
|
||||
|
||||
Args:
|
||||
successful_count: Anzahl erfolgreicher Jobs
|
||||
total_count: Gesamtanzahl der Jobs
|
||||
total_duration: Gesamtdauer aller Transformationen in Sekunden
|
||||
"""
|
||||
logger.info(
|
||||
f"Alle Transformationen abgeschlossen: {successful_count}/{total_count} erfolgreich ({total_duration:.2f}s)"
|
||||
)
|
||||
|
||||
# Erfasse RAM-Verbrauch nach Transformation
|
||||
import transform
|
||||
|
||||
if transform._saxon_worker_pool:
|
||||
transform._saxon_worker_pool.capture_ram_after_transform()
|
||||
# Speichere Metriken vor Shutdown (für späteren Zugriff im Dialog)
|
||||
self.last_saxon_metrics = deepcopy(transform._saxon_worker_pool.metrics)
|
||||
logger.debug("Saxon Worker-Pool Metriken gespeichert")
|
||||
|
||||
if transform._fop_worker_pool:
|
||||
transform._fop_worker_pool.capture_ram_after_transform()
|
||||
# Speichere Metriken vor Shutdown (für späteren Zugriff im Dialog)
|
||||
self.last_fop_metrics = deepcopy(transform._fop_worker_pool.metrics)
|
||||
logger.debug("FOP Worker-Pool Metriken gespeichert")
|
||||
|
||||
# Beende Worker-Pools (RAM-Optimierung - Pools werden bei nächster Transformation neu gestartet)
|
||||
self._shutdown_saxon_worker_pool()
|
||||
self._shutdown_fop_worker_pool()
|
||||
|
||||
# Verstecke Transformation-Progressbar
|
||||
self._hide_transformation_progress_bar()
|
||||
|
||||
# Aktualisiere Diff-PDF-Anzahl und Icons in allen Knoten
|
||||
self._update_all_diff_pdf_counts()
|
||||
self._update_diff_icons_for_existing_pdfs()
|
||||
|
||||
# Formatiere Dauer für Anzeige
|
||||
duration_str = f"{total_duration:.2f}s"
|
||||
|
||||
if successful_count == total_count:
|
||||
self.statusBar().showMessage(f"✓ Alle {total_count} Transformationen erfolgreich ({duration_str})", 5000)
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"Abgeschlossen",
|
||||
f"Alle {total_count} Transformationen wurden erfolgreich abgeschlossen.\n\nGesamtdauer: {duration_str}",
|
||||
)
|
||||
else:
|
||||
failed_count = total_count - successful_count
|
||||
self.statusBar().showMessage(
|
||||
f"⚠ {successful_count}/{total_count} erfolgreich, {failed_count} fehlgeschlagen ({duration_str})", 5000
|
||||
)
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"Abgeschlossen mit Fehlern",
|
||||
f"{successful_count} von {total_count} Transformationen erfolgreich\n{failed_count} fehlgeschlagen\n\nGesamtdauer: {duration_str}",
|
||||
)
|
||||
Reference in New Issue
Block a user