3acdfbb5c8
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>
347 lines
13 KiB
Python
347 lines
13 KiB
Python
"""
|
|
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)
|