From e4b2272e6136c4ad40de34a8768a681f9bb28ef2 Mon Sep 17 00:00:00 2001 From: Vitali Graf Date: Sat, 27 Dec 2025 20:01:46 +0100 Subject: [PATCH] =?UTF-8?q?UX-Verbesserung:=20Batch-Verarbeitung=20f=C3=BC?= =?UTF-8?q?r=20Drag&Drop=20von=20XML-Dateien?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Checkbox 'Alle XML-Dateien zuordnen' im XmlToXslAssignDialog hinzugefügt - Bei aktivierter Checkbox wird Dialog nur einmal angezeigt und Auswahl auf alle weiteren Dateien angewendet - Einzelne Erfolgsdialoge durch einen zusammenfassenden Dialog ersetzt - Zusammenfassungsdialog zeigt detaillierte Statistiken: • Anzahl neu hinzugefügter Dateien • Anzahl bereits vorhandener Dateien (Hash-Duplikate) • Anzahl bereits zugeordneter Dateien • Liste umbenannter Dateien • Fehlerberichte falls aufgetreten - Deutliche Verbesserung der UX: Bei 30 Dateien nur 1 Dialog statt 30 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/ui/MainWindow.py | 235 +++++++++++++++++++++++++----- src/ui/XmlToXslAssignDialog.py | 11 +- src/ui/XmlToXslAssignDialog.ui | 7 + src/ui/XmlToXslAssignDialog_ui.py | 16 +- 4 files changed, 225 insertions(+), 44 deletions(-) diff --git a/src/ui/MainWindow.py b/src/ui/MainWindow.py index cbaaa3c..921aa07 100644 --- a/src/ui/MainWindow.py +++ b/src/ui/MainWindow.py @@ -2427,9 +2427,8 @@ class MainWindow(QMainWindow): logger.info(f"Drop-Event: {len(xml_files)} XML-Dateien erkannt") - # Verarbeite jede XML-Datei einzeln - for xml_file_path in xml_files: - self._handle_xml_file_drop(xml_file_path) + # Verarbeite alle XML-Dateien mit optionalem "Alle zuordnen" Feature + self._handle_multiple_xml_files_drop(xml_files) event.acceptProposedAction() @@ -2439,9 +2438,171 @@ class MainWindow(QMainWindow): QMessageBox.critical(self, "Fehler", error_msg) event.ignore() + def _handle_multiple_xml_files_drop(self, xml_files: list): + """ + Verarbeitet mehrere XML-Dateien, die per Drag&Drop hinzugefügt wurden. + Unterstützt das "Alle XML-Dateien zuordnen" Feature. + + 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 + + # Variablen für "Alle zuordnen" Feature + apply_to_all = False + cached_selected_nodes = None + + # Statistiken für Zusammenfassung + stats = { + "total": len(xml_files), + "processed": 0, + "new_added": 0, + "existing_added": 0, + "already_assigned": 0, + "cancelled": 0, + "errors": 0, + "error_messages": [], + "renamed_files": [], + } + + # Verarbeite jede XML-Datei + for i, xml_file_path in enumerate(xml_files): + logger.debug(f"Verarbeite XML-Datei {i+1}/{len(xml_files)}: {xml_file_path}") + + # Prüfe ob die Datei existiert + if not xml_file_path.exists(): + stats["errors"] += 1 + stats["error_messages"].append(f"{xml_file_path.name}: Datei existiert nicht") + continue + + # Wenn "Alle zuordnen" aktiv ist und wir bereits eine Auswahl haben + if apply_to_all and cached_selected_nodes: + # Verwende die gecachte Auswahl + result = self._assign_xml_to_xsl_nodes(xml_file_path, cached_selected_nodes) + self._update_stats(stats, result) + continue + + # Zeige den Dialog für die erste Datei oder wenn "Alle zuordnen" nicht aktiv ist + dialog = XmlToXslAssignDialog( + parent=self, xml_file_path=xml_file_path, project_nodes=self.pdf_project.nodes + ) + + if dialog.exec() == XmlToXslAssignDialog.DialogCode.Accepted: + # Hole die ausgewählten XSL-Knoten + selected_xsl_nodes = dialog.get_selected_xsl_nodes() + + if selected_xsl_nodes: + # Verarbeite die Zuordnung + result = self._assign_xml_to_xsl_nodes(xml_file_path, selected_xsl_nodes) + self._update_stats(stats, result) + + # Prüfe ob "Alle zuordnen" aktiviert wurde + if dialog.is_apply_to_all() and i < len(xml_files) - 1: + # Es gibt noch weitere Dateien und User möchte alle zuordnen + apply_to_all = True + cached_selected_nodes = selected_xsl_nodes + logger.info( + f"'Alle zuordnen' aktiviert - {len(xml_files) - i - 1} weitere Datei(en) werden automatisch zugeordnet" + ) + else: + logger.warning("Keine XSL-Knoten ausgewählt") + else: + # Dialog wurde abgebrochen - beende die Verarbeitung + stats["cancelled"] = len(xml_files) - i + logger.debug(f"Dialog abgebrochen - {stats['cancelled']} Datei(en) übersprungen") + break + + # Zeige Zusammenfassungsdialog + self._show_drop_summary_dialog(stats) + + except Exception as e: + error_msg = f"Fehler beim Verarbeiten der XML-Dateien: {str(e)}" + logger.error(error_msg) + QMessageBox.critical(self, "Fehler", error_msg) + + def _update_stats(self, stats: dict, result: dict): + """ + Aktualisiert die Statistiken basierend auf dem Verarbeitungsergebnis. + + Args: + stats: Statistik-Dictionary + result: Ergebnis von _assign_xml_to_xsl_nodes + """ + stats["processed"] += 1 + + status = result.get("status") + if status == "new_added": + stats["new_added"] += 1 + if result.get("renamed_from"): + stats["renamed_files"].append(f"{result['renamed_from']} → {result['new_file']}") + elif status == "existing_added": + stats["existing_added"] += 1 + elif status == "already_assigned": + stats["already_assigned"] += 1 + elif status == "cancelled": + stats["cancelled"] += 1 + elif status == "error": + stats["errors"] += 1 + stats["error_messages"].append(result.get("error_msg", "Unbekannter Fehler")) + + 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(f"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 hinzugefügt: {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(f"\n📝 Umbenannte Dateien:") + for renamed in stats["renamed_files"]: + summary_lines.append(f" • {renamed}") + + if stats["errors"] > 0: + summary_lines.append(f"\n❌ Fehler: {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) + def _handle_xml_file_drop(self, xml_file_path: Path): """ Verarbeitet eine einzelne XML-Datei, die per Drag&Drop hinzugefügt wurde. + DEPRECATED: Diese Methode wird durch _handle_multiple_xml_files_drop ersetzt. Args: xml_file_path: Pfad zur XML-Datei @@ -2489,6 +2650,9 @@ class MainWindow(QMainWindow): Args: xml_file_path: Pfad zur XML-Datei selected_xsl_nodes: Liste der ausgewählten XSL-Knoten + + Returns: + dict: Statistiken über die Verarbeitung """ try: logger.info(f"Ordne XML-Datei '{xml_file_path.name}' zu {len(selected_xsl_nodes)} XSL-Knoten zu") @@ -2504,16 +2668,16 @@ class MainWindow(QMainWindow): if existing_xml: # 3. Hash-Match gefunden: Ordne vorhandene XML-Datei zu logger.info(f"Hash-Duplikat gefunden: {existing_xml.xml} hat gleichen Hash wie {xml_file_path.name}") - self._assign_existing_xml_to_nodes(existing_xml, selected_xsl_nodes) + return self._assign_existing_xml_to_nodes(existing_xml, selected_xsl_nodes) else: # 4. Kein Hash-Match: Verarbeite als neue XML-Datei logger.info(f"Keine Hash-Duplikate gefunden für {xml_file_path.name}, verarbeite als neue Datei") - self._process_new_xml_file(xml_file_path, selected_xsl_nodes, file_hash) + return self._process_new_xml_file(xml_file_path, selected_xsl_nodes, file_hash) except Exception as e: error_msg = f"Fehler beim Zuordnen der XML-Datei: {str(e)}" logger.error(error_msg) - QMessageBox.critical(self, "Fehler", error_msg) + return {"status": "error", "error_msg": error_msg} def _start_xml_hash_calculation(self): """ @@ -2848,6 +3012,9 @@ class MainWindow(QMainWindow): Args: existing_xml: Die bereits vorhandene XML-Datei selected_xsl_nodes: Liste der ausgewählten XSL-Knoten + + Returns: + dict: Statistiken mit 'status', 'added_count', 'existing_file' """ try: added_count = 0 @@ -2872,24 +3039,18 @@ class MainWindow(QMainWindow): # Aktualisiere das TreeWidget self._load_nodes_to_tree() - # Zeige Erfolgsmeldung - QMessageBox.information( - self, - "XML-Datei zugeordnet", - f"Eine XML-Datei mit gleichem Inhalt war bereits im Projekt vorhanden.\n\n" - f"Die vorhandene Datei '{existing_xml.xml.name}' wurde automatisch zu {added_count} XSL-Knoten zugeordnet.", - ) + return { + "status": "existing_added", + "added_count": added_count, + "existing_file": existing_xml.xml.name, + } else: - QMessageBox.information( - self, - "Bereits zugeordnet", - "Die XML-Datei mit gleichem Inhalt ist bereits in allen ausgewählten XSL-Knoten vorhanden.", - ) + return {"status": "already_assigned", "added_count": 0, "existing_file": existing_xml.xml.name} except Exception as e: error_msg = f"Fehler beim Zuordnen der vorhandenen XML-Datei: {str(e)}" logger.error(error_msg) - QMessageBox.critical(self, "Fehler", error_msg) + return {"status": "error", "error_msg": error_msg} def _process_new_xml_file(self, xml_file_path: Path, selected_xsl_nodes: list, file_hash: str | None): """ @@ -2899,13 +3060,15 @@ class MainWindow(QMainWindow): xml_file_path: Pfad zur neuen XML-Datei selected_xsl_nodes: Liste der ausgewählten XSL-Knoten file_hash: Berechneter Hash der Datei + + Returns: + dict: Statistiken mit 'status', 'added_count', 'new_file', 'renamed_from' """ try: # Prüfe ob Projekt verfügbar ist if not self.project or not self.project.project_dir: logger.error("Kein Projekt-Verzeichnis für neue XML-Datei verfügbar") - QMessageBox.critical(self, "Fehler", "Kein Projekt-Verzeichnis verfügbar.") - return + return {"status": "error", "error_msg": "Kein Projekt-Verzeichnis verfügbar."} # Erstelle xml-Ordner im Projekt-Verzeichnis falls er nicht existiert xml_dir = Path(self.project.project_dir) / "xml" @@ -2928,7 +3091,7 @@ class MainWindow(QMainWindow): if not selected_path: # Benutzer hat abgebrochen - return + return {"status": "cancelled", "added_count": 0} target_xml_path = selected_path @@ -2965,27 +3128,23 @@ class MainWindow(QMainWindow): # Aktualisiere das TreeWidget self._load_nodes_to_tree() - # Zeige Erfolgsmeldung - success_msg = ( - f"XML-Datei '{target_xml_path.name}' wurde erfolgreich zu {added_count} XSL-Knoten hinzugefügt." - ) - if target_xml_path.name != xml_file_path.name: - success_msg += ( - f"\n\nDie Datei wurde umbenannt von '{xml_file_path.name}' zu '{target_xml_path.name}'." - ) - - QMessageBox.information(self, "Erfolg", success_msg) + return { + "status": "new_added", + "added_count": added_count, + "new_file": target_xml_path.name, + "renamed_from": xml_file_path.name if target_xml_path.name != xml_file_path.name else None, + } else: - QMessageBox.information( - self, - "Information", - f"XML-Datei '{target_xml_path.name}' war bereits in allen ausgewählten XSL-Knoten vorhanden.", - ) + return { + "status": "already_assigned", + "added_count": 0, + "new_file": target_xml_path.name, + } except Exception as e: error_msg = f"Fehler beim Verarbeiten der neuen XML-Datei: {str(e)}" logger.error(error_msg) - QMessageBox.critical(self, "Fehler", error_msg) + return {"status": "error", "error_msg": error_msg} def _show_filename_selection_dialog(self, original_name: str, alternative_paths: List[Path]) -> Path | None: """ diff --git a/src/ui/XmlToXslAssignDialog.py b/src/ui/XmlToXslAssignDialog.py index 3fd4ae4..f07bd2a 100644 --- a/src/ui/XmlToXslAssignDialog.py +++ b/src/ui/XmlToXslAssignDialog.py @@ -280,12 +280,21 @@ class XmlToXslAssignDialog(QDialog): def get_xml_file_path(self): """ Gibt den Pfad zur XML-Datei zurück. - + Returns: Path: Pfad zur XML-Datei """ return self.xml_file_path + def is_apply_to_all(self): + """ + Prüft, ob die Checkbox 'Alle XML-Dateien' aktiviert ist. + + Returns: + bool: True wenn die Checkbox aktiviert ist, sonst False + """ + return self.ui.alle_xml.isChecked() + def accept(self): """Überschreibt accept() um Validierung durchzuführen.""" selected_nodes = self.get_selected_xsl_nodes() diff --git a/src/ui/XmlToXslAssignDialog.ui b/src/ui/XmlToXslAssignDialog.ui index 6fa1660..b1c318f 100644 --- a/src/ui/XmlToXslAssignDialog.ui +++ b/src/ui/XmlToXslAssignDialog.ui @@ -97,6 +97,13 @@ + + + + Alle XML-Dateien den ausgewählten XSL-Dateien zuordnen + + + diff --git a/src/ui/XmlToXslAssignDialog_ui.py b/src/ui/XmlToXslAssignDialog_ui.py index 504b1fc..b3a7131 100644 --- a/src/ui/XmlToXslAssignDialog_ui.py +++ b/src/ui/XmlToXslAssignDialog_ui.py @@ -3,7 +3,7 @@ ################################################################################ ## Form generated from reading UI file 'XmlToXslAssignDialog.ui' ## -## Created by: Qt User Interface Compiler version 6.9.1 +## Created by: Qt User Interface Compiler version 6.9.2 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ @@ -15,10 +15,10 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, QFont, QFontDatabase, QGradient, QIcon, QImage, QKeySequence, QLinearGradient, QPainter, QPalette, QPixmap, QRadialGradient, QTransform) -from PySide6.QtWidgets import (QAbstractButton, QApplication, QDialog, QDialogButtonBox, - QHBoxLayout, QHeaderView, QLabel, QPushButton, - QSizePolicy, QSpacerItem, QTreeWidget, QTreeWidgetItem, - QVBoxLayout, QWidget) +from PySide6.QtWidgets import (QAbstractButton, QApplication, QCheckBox, QDialog, + QDialogButtonBox, QHBoxLayout, QHeaderView, QLabel, + QPushButton, QSizePolicy, QSpacerItem, QTreeWidget, + QTreeWidgetItem, QVBoxLayout, QWidget) class Ui_XmlToXslAssignDialog(object): def setupUi(self, XmlToXslAssignDialog): @@ -64,6 +64,11 @@ class Ui_XmlToXslAssignDialog(object): self.buttonLayout.addItem(self.horizontalSpacer) + self.alle_xml = QCheckBox(XmlToXslAssignDialog) + self.alle_xml.setObjectName(u"alle_xml") + + self.buttonLayout.addWidget(self.alle_xml) + self.verticalLayout.addLayout(self.buttonLayout) @@ -94,5 +99,6 @@ class Ui_XmlToXslAssignDialog(object): ___qtreewidgetitem.setText(0, QCoreApplication.translate("XmlToXslAssignDialog", u"XSL-Knoten", None)); self.selectAllButton.setText(QCoreApplication.translate("XmlToXslAssignDialog", u"Alle ausw\u00e4hlen", None)) self.deselectAllButton.setText(QCoreApplication.translate("XmlToXslAssignDialog", u"Alle abw\u00e4hlen", None)) + self.alle_xml.setText(QCoreApplication.translate("XmlToXslAssignDialog", u"Alle XML-Dateien den ausgew\u00e4hlten XSL-Dateien zuordnen", None)) # retranslateUi