""" 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)