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:
2026-01-15 18:23:55 +01:00
parent 5a2da7f264
commit 3acdfbb5c8
10 changed files with 4353 additions and 4066 deletions
+346
View File
@@ -0,0 +1,346 @@
"""
DragDropMixin - Mixin für Drag-and-Drop-Funktionalität.
Dieses Mixin enthält alle Methoden zur Verarbeitung von Drag-and-Drop-Operationen
für das MainWindow.
"""
import logging
from pathlib import Path
from PySide6.QtGui import QDragEnterEvent, QDropEvent
from PySide6.QtWidgets import QMessageBox, QProgressBar
from ui.XmlToXslAssignDialog import XmlToXslAssignDialog
from ui.threads import XmlBatchProcessingThread
logger = logging.getLogger(__name__)
class DragDropMixin:
"""
Mixin für Drag-and-Drop-Funktionalität.
Dieses Mixin wird von MainWindow verwendet und erwartet folgende Attribute:
- self.ui: Das UI-Objekt mit treeWidget
- self.project: Das aktuelle Projekt
- self.pdf_project: Die Projekt-Daten (ProjectData)
- self.batch_processing_thread: Thread für Batch-Verarbeitung
- self.batch_progress_bar: QProgressBar für Batch-Fortschritt
- self._save_project_settings(): Methode zum Speichern der Projekt-Einstellungen
- self._load_nodes_to_tree(): Methode zum Laden der Nodes in den Tree
"""
def _setup_drag_drop(self):
"""Aktiviert Drag&Drop für das TreeWidget."""
try:
# Aktiviere Drag&Drop für das TreeWidget
self.ui.treeWidget.setAcceptDrops(True)
self.ui.treeWidget.setDragDropMode(self.ui.treeWidget.DragDropMode.DropOnly)
# Überschreibe die Drag&Drop-Events
self.ui.treeWidget.dragEnterEvent = self.tree_drag_enter_event
self.ui.treeWidget.dragMoveEvent = self.tree_drag_move_event
self.ui.treeWidget.dropEvent = self.tree_drop_event
logger.debug("Drag&Drop für TreeWidget aktiviert")
except Exception as e:
logger.error(f"Fehler beim Aktivieren von Drag&Drop: {e}")
def tree_drag_enter_event(self, event: QDragEnterEvent):
"""
Wird ausgeführt, wenn ein Drag-Vorgang über das TreeWidget beginnt.
Args:
event: Das Drag-Enter-Event
"""
try:
# Prüfe ob URLs (Dateien) gedraggt werden
if event.mimeData().hasUrls():
urls = event.mimeData().urls()
# Prüfe ob mindestens eine XML-Datei dabei ist
xml_files = [url.toLocalFile() for url in urls if url.toLocalFile().lower().endswith(".xml")]
if xml_files:
event.acceptProposedAction()
logger.debug(f"Drag-Enter akzeptiert: {len(xml_files)} XML-Dateien")
else:
event.ignore()
logger.debug("Drag-Enter ignoriert: Keine XML-Dateien")
else:
event.ignore()
logger.debug("Drag-Enter ignoriert: Keine URLs")
except Exception as e:
logger.error(f"Fehler in tree_drag_enter_event: {e}")
event.ignore()
def tree_drag_move_event(self, event):
"""
Wird ausgeführt, wenn ein Drag-Vorgang über das TreeWidget bewegt wird.
Args:
event: Das Drag-Move-Event
"""
try:
# Prüfe ob URLs (Dateien) gedraggt werden
if event.mimeData().hasUrls():
urls = event.mimeData().urls()
# Prüfe ob mindestens eine XML-Datei dabei ist
xml_files = [url.toLocalFile() for url in urls if url.toLocalFile().lower().endswith(".xml")]
if xml_files:
event.acceptProposedAction()
else:
event.ignore()
else:
event.ignore()
except Exception as e:
logger.error(f"Fehler in tree_drag_move_event: {e}")
event.ignore()
def tree_drop_event(self, event: QDropEvent):
"""
Wird ausgeführt, wenn Dateien auf das TreeWidget gedroppt werden.
Args:
event: Das Drop-Event
"""
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.")
event.ignore()
return
if not hasattr(self, "pdf_project") or not self.pdf_project:
QMessageBox.warning(self, "Warnung", "Keine Projekt-Einstellungen geladen.")
event.ignore()
return
# Hole die URLs aus dem Drop-Event
if not event.mimeData().hasUrls():
event.ignore()
return
urls = event.mimeData().urls()
xml_files = []
# Sammle alle XML-Dateien
for url in urls:
file_path = url.toLocalFile()
if file_path.lower().endswith(".xml"):
xml_files.append(Path(file_path))
if not xml_files:
QMessageBox.information(self, "Information", "Keine XML-Dateien zum Hinzufügen gefunden.")
event.ignore()
return
logger.info(f"Drop-Event: {len(xml_files)} XML-Dateien erkannt")
# Verarbeite alle XML-Dateien mit optionalem "Alle zuordnen" Feature
self._handle_multiple_xml_files_drop(xml_files)
event.acceptProposedAction()
except Exception as e:
error_msg = f"Fehler beim Verarbeiten des Drop-Events: {str(e)}"
logger.error(error_msg)
QMessageBox.critical(self, "Fehler", error_msg)
event.ignore()
def _handle_multiple_xml_files_drop(self, xml_files: list):
"""
Verarbeitet mehrere XML-Dateien asynchron per Drag&Drop.
Zeigt einen Dialog zur Auswahl der XSL-Knoten und startet dann die Batch-Verarbeitung im Hintergrund.
Args:
xml_files: Liste von Pfaden zu XML-Dateien
"""
if not xml_files:
return
try:
# Prüfe ob Projekt-Nodes verfügbar sind
if not self.pdf_project or not self.pdf_project.nodes:
QMessageBox.warning(self, "Warnung", "Keine Projekt-Nodes verfügbar für die Zuordnung.")
return
# Zeige Dialog für die erste Datei
dialog = XmlToXslAssignDialog(parent=self, xml_file_path=xml_files[0], project_nodes=self.pdf_project.nodes)
if dialog.exec() != XmlToXslAssignDialog.DialogCode.Accepted:
logger.debug("Dialog abgebrochen - keine Dateien verarbeitet")
return
# Hole die ausgewählten XSL-Knoten
selected_xsl_nodes = dialog.get_selected_xsl_nodes()
if not selected_xsl_nodes:
logger.warning("Keine XSL-Knoten ausgewählt")
return
# Prüfe ob "Alle zuordnen" aktiviert wurde
apply_to_all = dialog.is_apply_to_all()
# Bestimme welche Dateien verarbeitet werden sollen
files_to_process = xml_files if apply_to_all else [xml_files[0]]
# Stoppe vorherigen Batch-Thread falls noch aktiv
if self.batch_processing_thread and self.batch_processing_thread.isRunning():
self.batch_processing_thread.quit()
self.batch_processing_thread.wait()
# Zusätzliche Sicherheitsprüfung für project_dir
if not self.project or not self.project.project_dir:
QMessageBox.warning(self, "Fehler", "Projekt-Verzeichnis ist nicht verfügbar")
return
# Erstelle und starte neuen Batch-Verarbeitungs-Thread
self.batch_processing_thread = XmlBatchProcessingThread(
xml_files=files_to_process,
selected_xsl_nodes=selected_xsl_nodes,
project_dir=Path(self.project.project_dir),
pdf_project=self.pdf_project,
)
# Verbinde Signale
self.batch_processing_thread.progress_update.connect(self._on_batch_progress_update)
self.batch_processing_thread.processing_finished.connect(self._on_batch_processing_finished)
# Zeige Progressbar
self._show_batch_progress_bar(len(files_to_process))
# Starte Thread
self.batch_processing_thread.start()
logger.info(
f"Batch-Verarbeitung von {len(files_to_process)} Datei(en) gestartet (apply_to_all={apply_to_all})"
)
except Exception as e:
error_msg = f"Fehler beim Starten der Batch-Verarbeitung: {str(e)}"
logger.error(error_msg)
QMessageBox.critical(self, "Fehler", error_msg)
def _show_batch_progress_bar(self, total_files: int):
"""
Zeigt einen Progressbar in der Statusbar für die Batch-Verarbeitung.
Args:
total_files: Gesamtanzahl der zu verarbeitenden Dateien
"""
if self.batch_progress_bar is None:
self.batch_progress_bar = QProgressBar()
self.batch_progress_bar.setMaximumHeight(20)
self.batch_progress_bar.setMaximumWidth(300)
self.batch_progress_bar.setMinimum(0)
self.batch_progress_bar.setMaximum(total_files)
self.batch_progress_bar.setValue(0)
self.batch_progress_bar.setFormat("%v/%m Dateien")
# Füge Progressbar zur Statusbar hinzu
self.statusBar().addPermanentWidget(self.batch_progress_bar)
self.batch_progress_bar.show()
def _hide_batch_progress_bar(self):
"""Versteckt und entfernt den Progressbar aus der Statusbar."""
if self.batch_progress_bar:
self.statusBar().removeWidget(self.batch_progress_bar)
self.batch_progress_bar.hide()
def _on_batch_progress_update(self, current: int, total: int, current_file: str):
"""
Wird aufgerufen wenn der Batch-Thread einen Fortschritt meldet.
Args:
current: Aktuelle Dateinummer
total: Gesamtanzahl der Dateien
current_file: Name der aktuellen Datei
"""
if self.batch_progress_bar:
self.batch_progress_bar.setValue(current)
self.statusBar().showMessage(f"Verarbeite: {current_file} ({current}/{total})")
def _on_batch_processing_finished(self, stats: dict):
"""
Wird aufgerufen wenn die Batch-Verarbeitung abgeschlossen ist.
Args:
stats: Statistik-Dictionary mit Verarbeitungsergebnissen
"""
try:
# Verstecke Progressbar
self._hide_batch_progress_bar()
# Speichere Projekt-Einstellungen
if stats["processed"] > 0:
self._save_project_settings()
# Aktualisiere Tree
self._load_nodes_to_tree()
# Zeige Zusammenfassungsdialog
self._show_drop_summary_dialog(stats)
# Statusbar-Nachricht
self.statusBar().showMessage(
f"Batch-Verarbeitung abgeschlossen: {stats['processed']}/{stats['total']} Dateien", 5000
)
except Exception as e:
logger.error(f"Fehler beim Abschließen der Batch-Verarbeitung: {e}")
def _show_drop_summary_dialog(self, stats: dict):
"""
Zeigt einen Zusammenfassungsdialog über die verarbeiteten XML-Dateien.
Args:
stats: Statistik-Dictionary mit Verarbeitungsergebnissen
"""
# Erstelle Zusammenfassungstext
summary_lines = []
summary_lines.append("Verarbeitung abgeschlossen:\n")
summary_lines.append(f"Gesamt: {stats['total']} Datei(en)")
summary_lines.append(f"Verarbeitet: {stats['processed']} Datei(en)")
if stats["new_added"] > 0:
summary_lines.append(f"Neu hinzugefuegt: {stats['new_added']} Datei(en)")
if stats["existing_added"] > 0:
summary_lines.append(f"Vorhandene zugeordnet: {stats['existing_added']} Datei(en)")
if stats["already_assigned"] > 0:
summary_lines.append(f"Bereits zugeordnet: {stats['already_assigned']} Datei(en)")
if stats["cancelled"] > 0:
summary_lines.append(f"Abgebrochen: {stats['cancelled']} Datei(en)")
if stats["renamed_files"]:
summary_lines.append("\nUmbenannte Dateien:")
for renamed in stats["renamed_files"]:
summary_lines.append(f" - {renamed}")
if stats["errors"] > 0:
summary_lines.append(f"\nFehler: {stats['errors']}")
for error_msg in stats["error_messages"][:5]: # Zeige max. 5 Fehler
summary_lines.append(f" - {error_msg}")
if len(stats["error_messages"]) > 5:
summary_lines.append(f" ... und {len(stats['error_messages']) - 5} weitere Fehler")
summary_text = "\n".join(summary_lines)
# Wähle Icon basierend auf Erfolg
if stats["errors"] > 0:
QMessageBox.warning(self, "Verarbeitung mit Fehlern abgeschlossen", summary_text)
elif stats["cancelled"] > 0:
QMessageBox.information(self, "Verarbeitung abgebrochen", summary_text)
else:
QMessageBox.information(self, "Verarbeitung erfolgreich", summary_text)