From 629485f5e421b1f6314447b51d812dcd5bed65e4 Mon Sep 17 00:00:00 2001 From: Vitali Graf Date: Sat, 13 Dec 2025 21:06:40 +0100 Subject: [PATCH] Progress Bar und Diff-PDF-Icon im TreeWidget implementiert MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neue Features: - Progress Bar in Spalte 2 während XML-Transformationen - Diff-PDF-Icon erscheint nach Transformation bei vorhandener Diff-PDF - Doppelklick auf Icon öffnet Diff-PDF mit System-Viewer - Initial-Laden von Icons für existierende Diff-PDFs beim Projektstart Technische Implementierung: - XML-Item-Mapping mit eindeutigem Key-Format: "xml_path|xsl_id" - Unterstützt mehrfache Verwendung derselben XML bei verschiedenen XSL-Dateien - TransformationThread-Signale erweitert um XSL-ID-Parameter - Widget-Factory-Methoden für zentrierte Progress Bar und klickbare Icons - Result-Dictionary in transform.py enthält jetzt xsl_id UI-Anpassungen: - TreeWidget Spaltenanzahl von 2 auf 3 erhöht - setItemWidget() für dynamische Widget-Verwaltung in Spalte 2 Dateien: - src/ui/MainWindow.py: Hauptimplementierung mit Signal-Handlern - src/transform.py: xsl_id im Result-Dictionary - src/ui/MainWinddow.ui: Spalte 3 hinzugefügt - src/ui/MainWinddow_ui.py: Auto-generiert aus UI-Datei 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/transform.py | 28 +- src/ui/MainWinddow.ui | 17 +- src/ui/MainWinddow_ui.py | 601 +++++++++--------- src/ui/MainWindow.py | 1276 ++++++++++++++++++++++---------------- 4 files changed, 1084 insertions(+), 838 deletions(-) diff --git a/src/transform.py b/src/transform.py index 9e4a000..77beb2d 100644 --- a/src/transform.py +++ b/src/transform.py @@ -34,7 +34,7 @@ class TransformationJob: apache_fop_dir: Path, diff_pdf_path: Path, diff_pdf_params: list[str], - xsl_id: tuple | None = None + xsl_id: tuple | None = None, ): """ Initialisiert einen Transformations-Job. @@ -92,6 +92,7 @@ class TransformationJob: # Apache FOP Binaries (plattformabhängig) import sys + if sys.platform == "win32": self.fop_cmd = self.apache_fop_dir / "fop.cmd" else: @@ -158,11 +159,13 @@ class TransformationJob: # Sammle alle JAR-Dateien im Saxon-Verzeichnis für den Classpath import glob + saxon_dir = self.saxon_jar_path.parent all_jars = glob.glob(str(saxon_dir / "*.jar")) # Verwende alle JARs im Classpath (getrennt durch : auf Linux/Mac, ; auf Windows) import sys + classpath_separator = ";" if sys.platform == "win32" else ":" classpath = classpath_separator.join(all_jars) @@ -187,14 +190,16 @@ class TransformationJob: cmd_line, capture_output=True, text=True, - timeout=120 # 2 Minuten Timeout + timeout=120, # 2 Minuten Timeout ) if result.returncode == 0: logger.info(f"Saxon-Transformation erfolgreich: {self.xml_file.name}") return True, "Erfolgreich" else: - error_msg = f"Saxon-Fehler (Exit {result.returncode}):\nStdOut: {result.stdout}\nStdErr: {result.stderr}" + error_msg = ( + f"Saxon-Fehler (Exit {result.returncode}):\nStdOut: {result.stdout}\nStdErr: {result.stderr}" + ) logger.error(error_msg) return False, error_msg @@ -230,10 +235,13 @@ class TransformationJob: # Apache FOP Kommandozeile cmd_line = [ str(self.fop_cmd), - "-c", str(self.fop_conf) if self.fop_conf.exists() else "", + "-c", + str(self.fop_conf) if self.fop_conf.exists() else "", "-r", - "-fo", str(self.temp_fo), - "-pdf", str(self.new_pdf), + "-fo", + str(self.temp_fo), + "-pdf", + str(self.new_pdf), ] # Entferne leere Config-Parameter wenn fop.xconf nicht existiert @@ -248,7 +256,7 @@ class TransformationJob: cmd_line, capture_output=True, text=True, - timeout=180 # 3 Minuten Timeout + timeout=180, # 3 Minuten Timeout ) # Temporäre FO-Datei löschen @@ -264,6 +272,7 @@ class TransformationJob: if not self.ref_pdf.exists(): try: import shutil + shutil.copy2(self.new_pdf, self.ref_pdf) logger.info(f"Ref-PDF erstellt: {self.ref_pdf}") except Exception as e: @@ -320,7 +329,7 @@ class TransformationJob: cmd_compare, capture_output=True, text=True, - timeout=60 # 1 Minute Timeout + timeout=60, # 1 Minute Timeout ) if result.returncode == 0: @@ -355,7 +364,7 @@ class TransformationJob: cmd_diff, capture_output=True, text=True, - timeout=90 # 1.5 Minuten Timeout + timeout=90, # 1.5 Minuten Timeout ) if result_diff.returncode == 0 or self.diff_pdf.exists(): @@ -392,6 +401,7 @@ class TransformationJob: result = { "success": False, "xml_file": str(self.xml_file), + "xsl_id": self.xsl_id, "steps": {}, "duration": None, "new_pdf": str(self.new_pdf) if self.new_pdf.exists() else None, diff --git a/src/ui/MainWinddow.ui b/src/ui/MainWinddow.ui index 13e7c6e..c8ee7c9 100644 --- a/src/ui/MainWinddow.ui +++ b/src/ui/MainWinddow.ui @@ -64,7 +64,7 @@ - 2 + 3 true @@ -82,6 +82,11 @@ 2 + + + 3 + + @@ -171,8 +176,8 @@ 0 0 - 54 - 718 + 68 + 728 @@ -349,8 +354,8 @@ 0 0 - 649 - 690 + 625 + 700 @@ -396,7 +401,7 @@ 0 0 1263 - 33 + 22 diff --git a/src/ui/MainWinddow_ui.py b/src/ui/MainWinddow_ui.py index 07faf67..05c4215 100644 --- a/src/ui/MainWinddow_ui.py +++ b/src/ui/MainWinddow_ui.py @@ -1,300 +1,301 @@ -# -*- coding: utf-8 -*- - -################################################################################ -## Form generated from reading UI file 'MainWinddow.ui' -## -## Created by: Qt User Interface Compiler version 6.9.1 -## -## WARNING! All changes made in this file will be lost when recompiling UI file! -################################################################################ - -from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, - QMetaObject, QObject, QPoint, QRect, - QSize, QTime, QUrl, Qt) -from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient, - QCursor, QFont, QFontDatabase, QGradient, - QIcon, QImage, QKeySequence, QLinearGradient, - QPainter, QPalette, QPixmap, QRadialGradient, - QTransform) -from PySide6.QtWidgets import (QApplication, QFrame, QHBoxLayout, QHeaderView, - QLabel, QMainWindow, QMenu, QMenuBar, - QPushButton, QScrollArea, QSizePolicy, QSlider, - QSpacerItem, QSplitter, QStatusBar, QTreeWidget, - QTreeWidgetItem, QVBoxLayout, QWidget) - -class Ui_MainWindow(object): - def setupUi(self, MainWindow): - if not MainWindow.objectName(): - MainWindow.setObjectName(u"MainWindow") - MainWindow.resize(1263, 774) - self.actionNeu = QAction(MainWindow) - self.actionNeu.setObjectName(u"actionNeu") - icon = QIcon(QIcon.fromTheme(u"folder-new")) - self.actionNeu.setIcon(icon) - self.action_ffnen = QAction(MainWindow) - self.action_ffnen.setObjectName(u"action_ffnen") - icon1 = QIcon(QIcon.fromTheme(u"folder-open")) - self.action_ffnen.setIcon(icon1) - self.actionBeenden = QAction(MainWindow) - self.actionBeenden.setObjectName(u"actionBeenden") - icon2 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.ApplicationExit)) - self.actionBeenden.setIcon(icon2) - self.actionEinstellungen = QAction(MainWindow) - self.actionEinstellungen.setObjectName(u"actionEinstellungen") - icon3 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.DocumentProperties)) - self.actionEinstellungen.setIcon(icon3) - self.actionVorhandene_Projekte = QAction(MainWindow) - self.actionVorhandene_Projekte.setObjectName(u"actionVorhandene_Projekte") - self.actionVorhandene_Projekte.setEnabled(False) - self.centralwidget = QWidget(MainWindow) - self.centralwidget.setObjectName(u"centralwidget") - self.horizontalLayout = QHBoxLayout(self.centralwidget) - self.horizontalLayout.setObjectName(u"horizontalLayout") - self.horizontalLayout.setContentsMargins(0, 0, 0, 0) - self.splitter = QSplitter(self.centralwidget) - self.splitter.setObjectName(u"splitter") - self.splitter.setOrientation(Qt.Orientation.Horizontal) - self.frame = QFrame(self.splitter) - self.frame.setObjectName(u"frame") - sizePolicy = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.frame.sizePolicy().hasHeightForWidth()) - self.frame.setSizePolicy(sizePolicy) - self.frame.setMinimumSize(QSize(200, 0)) - self.frame.setFrameShape(QFrame.Shape.StyledPanel) - self.frame.setFrameShadow(QFrame.Shadow.Raised) - self.verticalLayout = QVBoxLayout(self.frame) - self.verticalLayout.setObjectName(u"verticalLayout") - self.verticalLayout.setContentsMargins(-1, -1, -1, 0) - self.treeWidget = QTreeWidget(self.frame) - __qtreewidgetitem = QTreeWidgetItem() - __qtreewidgetitem.setText(1, u"2"); - __qtreewidgetitem.setText(0, u"1"); - self.treeWidget.setHeaderItem(__qtreewidgetitem) - self.treeWidget.setObjectName(u"treeWidget") - sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) - sizePolicy1.setHorizontalStretch(0) - sizePolicy1.setVerticalStretch(0) - sizePolicy1.setHeightForWidth(self.treeWidget.sizePolicy().hasHeightForWidth()) - self.treeWidget.setSizePolicy(sizePolicy1) - self.treeWidget.setColumnCount(2) - self.treeWidget.header().setHighlightSections(True) - self.treeWidget.header().setStretchLastSection(True) - - self.verticalLayout.addWidget(self.treeWidget) - - self.frame_2 = QFrame(self.frame) - self.frame_2.setObjectName(u"frame_2") - self.frame_2.setFrameShadow(QFrame.Shadow.Raised) - self.horizontalLayout_2 = QHBoxLayout(self.frame_2) - self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") - self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0) - self.pushButton = QPushButton(self.frame_2) - self.pushButton.setObjectName(u"pushButton") - self.pushButton.setLayoutDirection(Qt.LayoutDirection.LeftToRight) - icon4 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.MediaPlaybackStart)) - self.pushButton.setIcon(icon4) - - self.horizontalLayout_2.addWidget(self.pushButton) - - self.pushButton_2 = QPushButton(self.frame_2) - self.pushButton_2.setObjectName(u"pushButton_2") - self.pushButton_2.setAutoFillBackground(False) - icon5 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.MediaSeekForward)) - self.pushButton_2.setIcon(icon5) - - self.horizontalLayout_2.addWidget(self.pushButton_2) - - self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) - - self.horizontalLayout_2.addItem(self.horizontalSpacer) - - self.pB_lade_aus_fn2 = QPushButton(self.frame_2) - self.pB_lade_aus_fn2.setObjectName(u"pB_lade_aus_fn2") - icon6 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.GoDown)) - self.pB_lade_aus_fn2.setIcon(icon6) - - self.horizontalLayout_2.addWidget(self.pB_lade_aus_fn2) - - - self.verticalLayout.addWidget(self.frame_2) - - self.splitter.addWidget(self.frame) - self.scrollArea = QScrollArea(self.splitter) - self.scrollArea.setObjectName(u"scrollArea") - sizePolicy2 = QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum) - sizePolicy2.setHorizontalStretch(0) - sizePolicy2.setVerticalStretch(0) - sizePolicy2.setHeightForWidth(self.scrollArea.sizePolicy().hasHeightForWidth()) - self.scrollArea.setSizePolicy(sizePolicy2) - self.scrollArea.setWidgetResizable(True) - self.scrollAreaWidgetContents = QWidget() - self.scrollAreaWidgetContents.setObjectName(u"scrollAreaWidgetContents") - self.scrollAreaWidgetContents.setGeometry(QRect(0, 0, 54, 718)) - self.verticalLayout_2 = QVBoxLayout(self.scrollAreaWidgetContents) - self.verticalLayout_2.setObjectName(u"verticalLayout_2") - self.label = QLabel(self.scrollAreaWidgetContents) - self.label.setObjectName(u"label") - - self.verticalLayout_2.addWidget(self.label) - - self.label_2 = QLabel(self.scrollAreaWidgetContents) - self.label_2.setObjectName(u"label_2") - - self.verticalLayout_2.addWidget(self.label_2) - - self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) - - self.verticalLayout_2.addItem(self.verticalSpacer) - - self.scrollArea.setWidget(self.scrollAreaWidgetContents) - self.splitter.addWidget(self.scrollArea) - self.frame_3 = QFrame(self.splitter) - self.frame_3.setObjectName(u"frame_3") - self.frame_3.setFrameShape(QFrame.Shape.NoFrame) - self.frame_3.setFrameShadow(QFrame.Shadow.Raised) - self.verticalLayout_4 = QVBoxLayout(self.frame_3) - self.verticalLayout_4.setObjectName(u"verticalLayout_4") - self.verticalLayout_4.setContentsMargins(0, 0, 0, 0) - self.frame_4 = QFrame(self.frame_3) - self.frame_4.setObjectName(u"frame_4") - self.frame_4.setFrameShape(QFrame.Shape.StyledPanel) - self.frame_4.setFrameShadow(QFrame.Shadow.Raised) - self.horizontalLayout_3 = QHBoxLayout(self.frame_4) - self.horizontalLayout_3.setObjectName(u"horizontalLayout_3") - self.horizontalLayout_3.setContentsMargins(0, 0, 0, 0) - self.horizontalSpacer_4 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) - - self.horizontalLayout_3.addItem(self.horizontalSpacer_4) - - self.label_6 = QLabel(self.frame_4) - self.label_6.setObjectName(u"label_6") - - self.horizontalLayout_3.addWidget(self.label_6) - - self.alpha = QSlider(self.frame_4) - self.alpha.setObjectName(u"alpha") - self.alpha.setMinimum(-100) - self.alpha.setMaximum(100) - self.alpha.setOrientation(Qt.Orientation.Horizontal) - - self.horizontalLayout_3.addWidget(self.alpha) - - self.label_7 = QLabel(self.frame_4) - self.label_7.setObjectName(u"label_7") - - self.horizontalLayout_3.addWidget(self.label_7) - - self.horizontalSpacer_2 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) - - self.horizontalLayout_3.addItem(self.horizontalSpacer_2) - - self.label_5 = QLabel(self.frame_4) - self.label_5.setObjectName(u"label_5") - - self.horizontalLayout_3.addWidget(self.label_5) - - self.zoom = QSlider(self.frame_4) - self.zoom.setObjectName(u"zoom") - self.zoom.setMinimum(25) - self.zoom.setMaximum(300) - self.zoom.setValue(100) - self.zoom.setOrientation(Qt.Orientation.Horizontal) - - self.horizontalLayout_3.addWidget(self.zoom) - - self.horizontalSpacer_5 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) - - self.horizontalLayout_3.addItem(self.horizontalSpacer_5) - - - self.verticalLayout_4.addWidget(self.frame_4) - - self.scrollArea_2 = QScrollArea(self.frame_3) - self.scrollArea_2.setObjectName(u"scrollArea_2") - self.scrollArea_2.setWidgetResizable(True) - self.scrollAreaWidgetContents_2 = QWidget() - self.scrollAreaWidgetContents_2.setObjectName(u"scrollAreaWidgetContents_2") - self.scrollAreaWidgetContents_2.setGeometry(QRect(0, 0, 649, 690)) - self.verticalLayout_3 = QVBoxLayout(self.scrollAreaWidgetContents_2) - self.verticalLayout_3.setObjectName(u"verticalLayout_3") - self.verticalLayout_3.setContentsMargins(0, 0, 0, 0) - self.label_3 = QLabel(self.scrollAreaWidgetContents_2) - self.label_3.setObjectName(u"label_3") - - self.verticalLayout_3.addWidget(self.label_3) - - self.label_4 = QLabel(self.scrollAreaWidgetContents_2) - self.label_4.setObjectName(u"label_4") - - self.verticalLayout_3.addWidget(self.label_4) - - self.scrollArea_2.setWidget(self.scrollAreaWidgetContents_2) - - self.verticalLayout_4.addWidget(self.scrollArea_2) - - self.splitter.addWidget(self.frame_3) - - self.horizontalLayout.addWidget(self.splitter) - - MainWindow.setCentralWidget(self.centralwidget) - self.menubar = QMenuBar(MainWindow) - self.menubar.setObjectName(u"menubar") - self.menubar.setGeometry(QRect(0, 0, 1263, 33)) - self.menuProjekt = QMenu(self.menubar) - self.menuProjekt.setObjectName(u"menuProjekt") - self.menuThema = QMenu(self.menubar) - self.menuThema.setObjectName(u"menuThema") - MainWindow.setMenuBar(self.menubar) - self.statusbar = QStatusBar(MainWindow) - self.statusbar.setObjectName(u"statusbar") - MainWindow.setStatusBar(self.statusbar) - - self.menubar.addAction(self.menuProjekt.menuAction()) - self.menubar.addAction(self.menuThema.menuAction()) - self.menuProjekt.addAction(self.actionNeu) - self.menuProjekt.addAction(self.action_ffnen) - self.menuProjekt.addSeparator() - self.menuProjekt.addAction(self.actionVorhandene_Projekte) - self.menuProjekt.addSeparator() - self.menuProjekt.addAction(self.actionEinstellungen) - self.menuProjekt.addSeparator() - self.menuProjekt.addAction(self.actionBeenden) - - self.retranslateUi(MainWindow) - self.actionBeenden.triggered.connect(MainWindow.close) - - QMetaObject.connectSlotsByName(MainWindow) - # setupUi - - def retranslateUi(self, MainWindow): - MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"DocuMentor", None)) - self.actionNeu.setText(QCoreApplication.translate("MainWindow", u"Neu ...", None)) -#if QT_CONFIG(shortcut) - self.actionNeu.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+N", None)) -#endif // QT_CONFIG(shortcut) - self.action_ffnen.setText(QCoreApplication.translate("MainWindow", u"\u00d6ffnen ...", None)) -#if QT_CONFIG(shortcut) - self.action_ffnen.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+O", None)) -#endif // QT_CONFIG(shortcut) - self.actionBeenden.setText(QCoreApplication.translate("MainWindow", u"Beenden", None)) - self.actionEinstellungen.setText(QCoreApplication.translate("MainWindow", u"Einstellungen ...", None)) -#if QT_CONFIG(shortcut) - self.actionEinstellungen.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+S", None)) -#endif // QT_CONFIG(shortcut) - self.actionVorhandene_Projekte.setText(QCoreApplication.translate("MainWindow", u"Vorhandene Projekte", None)) - self.pushButton.setText(QCoreApplication.translate("MainWindow", u"nur ge\u00e4nderte generieren", None)) - self.pushButton_2.setText(QCoreApplication.translate("MainWindow", u"Alle generieren", None)) - self.pB_lade_aus_fn2.setText(QCoreApplication.translate("MainWindow", u"lade aus FN2", None)) - self.label.setText("") - self.label_2.setText("") - self.label_6.setText(QCoreApplication.translate("MainWindow", u"Vorher (Referenz)", None)) - self.label_7.setText(QCoreApplication.translate("MainWindow", u"Nachher (Neu)", None)) - self.label_5.setText(QCoreApplication.translate("MainWindow", u"Zoom", None)) - self.label_3.setText(QCoreApplication.translate("MainWindow", u"TextLabel", None)) - self.label_4.setText(QCoreApplication.translate("MainWindow", u"TextLabel", None)) - self.menuProjekt.setTitle(QCoreApplication.translate("MainWindow", u"Projekt", None)) - self.menuThema.setTitle(QCoreApplication.translate("MainWindow", u"Thema", None)) - # retranslateUi - +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'MainWinddow.ui' +## +## Created by: Qt User Interface Compiler version 6.9.2 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, + QMetaObject, QObject, QPoint, QRect, + QSize, QTime, QUrl, Qt) +from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient, + QCursor, QFont, QFontDatabase, QGradient, + QIcon, QImage, QKeySequence, QLinearGradient, + QPainter, QPalette, QPixmap, QRadialGradient, + QTransform) +from PySide6.QtWidgets import (QApplication, QFrame, QHBoxLayout, QHeaderView, + QLabel, QMainWindow, QMenu, QMenuBar, + QPushButton, QScrollArea, QSizePolicy, QSlider, + QSpacerItem, QSplitter, QStatusBar, QTreeWidget, + QTreeWidgetItem, QVBoxLayout, QWidget) + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + if not MainWindow.objectName(): + MainWindow.setObjectName(u"MainWindow") + MainWindow.resize(1263, 774) + self.actionNeu = QAction(MainWindow) + self.actionNeu.setObjectName(u"actionNeu") + icon = QIcon(QIcon.fromTheme(u"folder-new")) + self.actionNeu.setIcon(icon) + self.action_ffnen = QAction(MainWindow) + self.action_ffnen.setObjectName(u"action_ffnen") + icon1 = QIcon(QIcon.fromTheme(u"folder-open")) + self.action_ffnen.setIcon(icon1) + self.actionBeenden = QAction(MainWindow) + self.actionBeenden.setObjectName(u"actionBeenden") + icon2 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.ApplicationExit)) + self.actionBeenden.setIcon(icon2) + self.actionEinstellungen = QAction(MainWindow) + self.actionEinstellungen.setObjectName(u"actionEinstellungen") + icon3 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.DocumentProperties)) + self.actionEinstellungen.setIcon(icon3) + self.actionVorhandene_Projekte = QAction(MainWindow) + self.actionVorhandene_Projekte.setObjectName(u"actionVorhandene_Projekte") + self.actionVorhandene_Projekte.setEnabled(False) + self.centralwidget = QWidget(MainWindow) + self.centralwidget.setObjectName(u"centralwidget") + self.horizontalLayout = QHBoxLayout(self.centralwidget) + self.horizontalLayout.setObjectName(u"horizontalLayout") + self.horizontalLayout.setContentsMargins(0, 0, 0, 0) + self.splitter = QSplitter(self.centralwidget) + self.splitter.setObjectName(u"splitter") + self.splitter.setOrientation(Qt.Orientation.Horizontal) + self.frame = QFrame(self.splitter) + self.frame.setObjectName(u"frame") + sizePolicy = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.frame.sizePolicy().hasHeightForWidth()) + self.frame.setSizePolicy(sizePolicy) + self.frame.setMinimumSize(QSize(200, 0)) + self.frame.setFrameShape(QFrame.Shape.StyledPanel) + self.frame.setFrameShadow(QFrame.Shadow.Raised) + self.verticalLayout = QVBoxLayout(self.frame) + self.verticalLayout.setObjectName(u"verticalLayout") + self.verticalLayout.setContentsMargins(-1, -1, -1, 0) + self.treeWidget = QTreeWidget(self.frame) + __qtreewidgetitem = QTreeWidgetItem() + __qtreewidgetitem.setText(2, u"3"); + __qtreewidgetitem.setText(1, u"2"); + __qtreewidgetitem.setText(0, u"1"); + self.treeWidget.setHeaderItem(__qtreewidgetitem) + self.treeWidget.setObjectName(u"treeWidget") + sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + sizePolicy1.setHorizontalStretch(0) + sizePolicy1.setVerticalStretch(0) + sizePolicy1.setHeightForWidth(self.treeWidget.sizePolicy().hasHeightForWidth()) + self.treeWidget.setSizePolicy(sizePolicy1) + self.treeWidget.setColumnCount(3) + self.treeWidget.header().setHighlightSections(True) + self.treeWidget.header().setStretchLastSection(True) + + self.verticalLayout.addWidget(self.treeWidget) + + self.frame_2 = QFrame(self.frame) + self.frame_2.setObjectName(u"frame_2") + self.frame_2.setFrameShadow(QFrame.Shadow.Raised) + self.horizontalLayout_2 = QHBoxLayout(self.frame_2) + self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") + self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0) + self.pushButton = QPushButton(self.frame_2) + self.pushButton.setObjectName(u"pushButton") + self.pushButton.setLayoutDirection(Qt.LayoutDirection.LeftToRight) + icon4 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.MediaPlaybackStart)) + self.pushButton.setIcon(icon4) + + self.horizontalLayout_2.addWidget(self.pushButton) + + self.pushButton_2 = QPushButton(self.frame_2) + self.pushButton_2.setObjectName(u"pushButton_2") + self.pushButton_2.setAutoFillBackground(False) + icon5 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.MediaSeekForward)) + self.pushButton_2.setIcon(icon5) + + self.horizontalLayout_2.addWidget(self.pushButton_2) + + self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) + + self.horizontalLayout_2.addItem(self.horizontalSpacer) + + self.pB_lade_aus_fn2 = QPushButton(self.frame_2) + self.pB_lade_aus_fn2.setObjectName(u"pB_lade_aus_fn2") + icon6 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.GoDown)) + self.pB_lade_aus_fn2.setIcon(icon6) + + self.horizontalLayout_2.addWidget(self.pB_lade_aus_fn2) + + + self.verticalLayout.addWidget(self.frame_2) + + self.splitter.addWidget(self.frame) + self.scrollArea = QScrollArea(self.splitter) + self.scrollArea.setObjectName(u"scrollArea") + sizePolicy2 = QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum) + sizePolicy2.setHorizontalStretch(0) + sizePolicy2.setVerticalStretch(0) + sizePolicy2.setHeightForWidth(self.scrollArea.sizePolicy().hasHeightForWidth()) + self.scrollArea.setSizePolicy(sizePolicy2) + self.scrollArea.setWidgetResizable(True) + self.scrollAreaWidgetContents = QWidget() + self.scrollAreaWidgetContents.setObjectName(u"scrollAreaWidgetContents") + self.scrollAreaWidgetContents.setGeometry(QRect(0, 0, 68, 728)) + self.verticalLayout_2 = QVBoxLayout(self.scrollAreaWidgetContents) + self.verticalLayout_2.setObjectName(u"verticalLayout_2") + self.label = QLabel(self.scrollAreaWidgetContents) + self.label.setObjectName(u"label") + + self.verticalLayout_2.addWidget(self.label) + + self.label_2 = QLabel(self.scrollAreaWidgetContents) + self.label_2.setObjectName(u"label_2") + + self.verticalLayout_2.addWidget(self.label_2) + + self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) + + self.verticalLayout_2.addItem(self.verticalSpacer) + + self.scrollArea.setWidget(self.scrollAreaWidgetContents) + self.splitter.addWidget(self.scrollArea) + self.frame_3 = QFrame(self.splitter) + self.frame_3.setObjectName(u"frame_3") + self.frame_3.setFrameShape(QFrame.Shape.NoFrame) + self.frame_3.setFrameShadow(QFrame.Shadow.Raised) + self.verticalLayout_4 = QVBoxLayout(self.frame_3) + self.verticalLayout_4.setObjectName(u"verticalLayout_4") + self.verticalLayout_4.setContentsMargins(0, 0, 0, 0) + self.frame_4 = QFrame(self.frame_3) + self.frame_4.setObjectName(u"frame_4") + self.frame_4.setFrameShape(QFrame.Shape.StyledPanel) + self.frame_4.setFrameShadow(QFrame.Shadow.Raised) + self.horizontalLayout_3 = QHBoxLayout(self.frame_4) + self.horizontalLayout_3.setObjectName(u"horizontalLayout_3") + self.horizontalLayout_3.setContentsMargins(0, 0, 0, 0) + self.horizontalSpacer_4 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) + + self.horizontalLayout_3.addItem(self.horizontalSpacer_4) + + self.label_6 = QLabel(self.frame_4) + self.label_6.setObjectName(u"label_6") + + self.horizontalLayout_3.addWidget(self.label_6) + + self.alpha = QSlider(self.frame_4) + self.alpha.setObjectName(u"alpha") + self.alpha.setMinimum(-100) + self.alpha.setMaximum(100) + self.alpha.setOrientation(Qt.Orientation.Horizontal) + + self.horizontalLayout_3.addWidget(self.alpha) + + self.label_7 = QLabel(self.frame_4) + self.label_7.setObjectName(u"label_7") + + self.horizontalLayout_3.addWidget(self.label_7) + + self.horizontalSpacer_2 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) + + self.horizontalLayout_3.addItem(self.horizontalSpacer_2) + + self.label_5 = QLabel(self.frame_4) + self.label_5.setObjectName(u"label_5") + + self.horizontalLayout_3.addWidget(self.label_5) + + self.zoom = QSlider(self.frame_4) + self.zoom.setObjectName(u"zoom") + self.zoom.setMinimum(25) + self.zoom.setMaximum(300) + self.zoom.setValue(100) + self.zoom.setOrientation(Qt.Orientation.Horizontal) + + self.horizontalLayout_3.addWidget(self.zoom) + + self.horizontalSpacer_5 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) + + self.horizontalLayout_3.addItem(self.horizontalSpacer_5) + + + self.verticalLayout_4.addWidget(self.frame_4) + + self.scrollArea_2 = QScrollArea(self.frame_3) + self.scrollArea_2.setObjectName(u"scrollArea_2") + self.scrollArea_2.setWidgetResizable(True) + self.scrollAreaWidgetContents_2 = QWidget() + self.scrollAreaWidgetContents_2.setObjectName(u"scrollAreaWidgetContents_2") + self.scrollAreaWidgetContents_2.setGeometry(QRect(0, 0, 625, 700)) + self.verticalLayout_3 = QVBoxLayout(self.scrollAreaWidgetContents_2) + self.verticalLayout_3.setObjectName(u"verticalLayout_3") + self.verticalLayout_3.setContentsMargins(0, 0, 0, 0) + self.label_3 = QLabel(self.scrollAreaWidgetContents_2) + self.label_3.setObjectName(u"label_3") + + self.verticalLayout_3.addWidget(self.label_3) + + self.label_4 = QLabel(self.scrollAreaWidgetContents_2) + self.label_4.setObjectName(u"label_4") + + self.verticalLayout_3.addWidget(self.label_4) + + self.scrollArea_2.setWidget(self.scrollAreaWidgetContents_2) + + self.verticalLayout_4.addWidget(self.scrollArea_2) + + self.splitter.addWidget(self.frame_3) + + self.horizontalLayout.addWidget(self.splitter) + + MainWindow.setCentralWidget(self.centralwidget) + self.menubar = QMenuBar(MainWindow) + self.menubar.setObjectName(u"menubar") + self.menubar.setGeometry(QRect(0, 0, 1263, 22)) + self.menuProjekt = QMenu(self.menubar) + self.menuProjekt.setObjectName(u"menuProjekt") + self.menuThema = QMenu(self.menubar) + self.menuThema.setObjectName(u"menuThema") + MainWindow.setMenuBar(self.menubar) + self.statusbar = QStatusBar(MainWindow) + self.statusbar.setObjectName(u"statusbar") + MainWindow.setStatusBar(self.statusbar) + + self.menubar.addAction(self.menuProjekt.menuAction()) + self.menubar.addAction(self.menuThema.menuAction()) + self.menuProjekt.addAction(self.actionNeu) + self.menuProjekt.addAction(self.action_ffnen) + self.menuProjekt.addSeparator() + self.menuProjekt.addAction(self.actionVorhandene_Projekte) + self.menuProjekt.addSeparator() + self.menuProjekt.addAction(self.actionEinstellungen) + self.menuProjekt.addSeparator() + self.menuProjekt.addAction(self.actionBeenden) + + self.retranslateUi(MainWindow) + self.actionBeenden.triggered.connect(MainWindow.close) + + QMetaObject.connectSlotsByName(MainWindow) + # setupUi + + def retranslateUi(self, MainWindow): + MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"DocuMentor", None)) + self.actionNeu.setText(QCoreApplication.translate("MainWindow", u"Neu ...", None)) +#if QT_CONFIG(shortcut) + self.actionNeu.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+N", None)) +#endif // QT_CONFIG(shortcut) + self.action_ffnen.setText(QCoreApplication.translate("MainWindow", u"\u00d6ffnen ...", None)) +#if QT_CONFIG(shortcut) + self.action_ffnen.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+O", None)) +#endif // QT_CONFIG(shortcut) + self.actionBeenden.setText(QCoreApplication.translate("MainWindow", u"Beenden", None)) + self.actionEinstellungen.setText(QCoreApplication.translate("MainWindow", u"Einstellungen ...", None)) +#if QT_CONFIG(shortcut) + self.actionEinstellungen.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+S", None)) +#endif // QT_CONFIG(shortcut) + self.actionVorhandene_Projekte.setText(QCoreApplication.translate("MainWindow", u"Vorhandene Projekte", None)) + self.pushButton.setText(QCoreApplication.translate("MainWindow", u"nur ge\u00e4nderte generieren", None)) + self.pushButton_2.setText(QCoreApplication.translate("MainWindow", u"Alle generieren", None)) + self.pB_lade_aus_fn2.setText(QCoreApplication.translate("MainWindow", u"lade aus FN2", None)) + self.label.setText("") + self.label_2.setText("") + self.label_6.setText(QCoreApplication.translate("MainWindow", u"Vorher (Referenz)", None)) + self.label_7.setText(QCoreApplication.translate("MainWindow", u"Nachher (Neu)", None)) + self.label_5.setText(QCoreApplication.translate("MainWindow", u"Zoom", None)) + self.label_3.setText(QCoreApplication.translate("MainWindow", u"TextLabel", None)) + self.label_4.setText(QCoreApplication.translate("MainWindow", u"TextLabel", None)) + self.menuProjekt.setTitle(QCoreApplication.translate("MainWindow", u"Projekt", None)) + self.menuThema.setTitle(QCoreApplication.translate("MainWindow", u"Thema", None)) + # retranslateUi + diff --git a/src/ui/MainWindow.py b/src/ui/MainWindow.py index 945002f..64c182b 100644 --- a/src/ui/MainWindow.py +++ b/src/ui/MainWindow.py @@ -9,7 +9,19 @@ from typing import List from PySide6.QtCore import Qt, QSize, QThread, Signal from PySide6.QtGui import QCursor, QPixmap, QPainter, QAction, QIcon, QDragEnterEvent, QDropEvent -from PySide6.QtWidgets import QLabel, QMainWindow, QApplication, QStyleFactory, QMenu, QTreeWidgetItem, QMessageBox, QFileDialog +from PySide6.QtWidgets import ( + QLabel, + QMainWindow, + QApplication, + QStyleFactory, + QMenu, + QTreeWidgetItem, + QMessageBox, + QFileDialog, + QWidget, + QHBoxLayout, + QProgressBar, +) from PySide6.QtPdf import QPdfDocument from ui.MainWinddow_ui import Ui_MainWindow @@ -30,16 +42,16 @@ class XmlHashCalculatorThread(QThread): """ Thread für die asynchrone Berechnung von blake2b-Hash-Werten für XML-Dateien. """ - + # Signale für die Kommunikation mit dem Haupt-Thread hash_calculated = Signal(object, str) # xml_file_object, hash_value calculation_finished = Signal(int, int) # processed_count, total_count error_occurred = Signal(str, str) # xml_file_path, error_message - + def __init__(self, project_dir: Path, xml_files: List[XmlFile]): """ Initialisiert den Hash-Berechnungs-Thread. - + Args: project_dir: Pfad zum Projekt-Verzeichnis xml_files: Liste der XmlFile-Objekte für die Hash-Berechnung @@ -48,13 +60,13 @@ class XmlHashCalculatorThread(QThread): self.project_dir = project_dir self.xml_files = xml_files self.processed_count = 0 - + def run(self): """ Führt die Hash-Berechnung für alle XML-Dateien aus. """ logger.info(f"Starte Hash-Berechnung für {len(self.xml_files)} XML-Dateien") - + for xml_file in self.xml_files: try: # Prüfe ob hashsum bereits vorhanden ist @@ -62,35 +74,35 @@ class XmlHashCalculatorThread(QThread): logger.debug(f"Hash bereits vorhanden für {xml_file.xml}: {xml_file.hashsum}") self.processed_count += 1 continue - + # Berechne Hash für die XML-Datei xml_file_path = self.project_dir / xml_file.xml hash_value = self._calculate_blake2b_hash(xml_file_path) - + if hash_value: # Sende Signal mit berechnetem Hash self.hash_calculated.emit(xml_file, hash_value) logger.debug(f"Hash berechnet für {xml_file.xml}: {hash_value}") - + self.processed_count += 1 - + except Exception as e: error_msg = f"Fehler bei Hash-Berechnung für {xml_file.xml}: {str(e)}" logger.error(error_msg) self.error_occurred.emit(str(xml_file.xml), error_msg) self.processed_count += 1 - + # Sende Abschluss-Signal self.calculation_finished.emit(self.processed_count, len(self.xml_files)) logger.info(f"Hash-Berechnung abgeschlossen: {self.processed_count}/{len(self.xml_files)} verarbeitet") - + def _calculate_blake2b_hash(self, file_path: Path) -> str | None: """ Berechnet den blake2b-Hash einer XML-Datei. - + Args: file_path: Pfad zur XML-Datei - + Returns: str: Hash-Wert mit "blake2b:" Präfix oder None bei Fehler """ @@ -98,16 +110,16 @@ class XmlHashCalculatorThread(QThread): if not file_path.exists(): logger.warning(f"XML-Datei nicht gefunden: {file_path}") return None - + # Datei binär lesen und Hash berechnen - with open(file_path, 'rb') as f: + with open(file_path, "rb") as f: file_content = f.read() hash_obj = hashlib.blake2b(file_content) hash_hex = hash_obj.hexdigest() - + # Präfix hinzufügen return f"blake2b:{hash_hex}" - + except Exception as e: logger.error(f"Fehler beim Berechnen des Hash für {file_path}: {e}") return None @@ -119,9 +131,9 @@ class TransformationThread(QThread): """ # Signale für die Kommunikation mit dem Haupt-Thread - job_started = Signal(str) # xml_file_name + job_started = Signal(str, str) # xml_file_name, xsl_id_str job_finished = Signal(dict) # result_dict - job_error = Signal(str, str) # xml_file_name, error_message + job_error = Signal(str, str, str) # xml_file_name, xsl_id_str, error_message all_jobs_finished = Signal(int, int) # successful_count, total_count def __init__(self, jobs: list[TransformationJob], force: bool = False): @@ -145,8 +157,9 @@ class TransformationThread(QThread): for job in self.jobs: try: - # Sende Start-Signal - self.job_started.emit(str(job.xml_file)) + # Sende Start-Signal mit XSL-ID + xsl_id_str = "_".join(str(x) for x in job.xsl_id) if job.xsl_id else "" + self.job_started.emit(str(job.xml_file), xsl_id_str) # Führe Transformations-Pipeline aus result = job.run_full_pipeline(force=self.force) @@ -160,7 +173,8 @@ class TransformationThread(QThread): except Exception as e: error_msg = f"Unerwarteter Fehler bei Transformation: {str(e)}" logger.error(error_msg) - self.job_error.emit(str(job.xml_file), error_msg) + xsl_id_str = "_".join(str(x) for x in job.xsl_id) if job.xsl_id else "" + self.job_error.emit(str(job.xml_file), xsl_id_str, error_msg) # Sende Abschluss-Signal für alle Jobs self.all_jobs_finished.emit(self.successful_count, len(self.jobs)) @@ -172,7 +186,7 @@ class MainWindow(QMainWindow): """ Konstruktor für die MainWindow-Klasse. Verwendet PySide6.QtPdf für optimale Performance. - + Args: parent: Übergeordnetes Widget, falls vorhanden """ @@ -187,7 +201,7 @@ class MainWindow(QMainWindow): # PDF-Dokumente für späteres On-Demand-Rendering speichern self.pdf_documents = {} # {pdf_filename: {'diff': QPdfDocument, 'ref': QPdfDocument, 'new': QPdfDocument}} - + # Aktueller Zoom-Faktor self.current_zoom = 100 # 100% @@ -206,43 +220,46 @@ class MainWindow(QMainWindow): self.last_drag_position = None self.drag_threshold = 3 # Mindestbewegung in Pixeln vor dem Scrollen self.scroll_sensitivity = 0.7 # Reduzierte Empfindlichkeit für sanfteres Scrollen - + # Das aktuelle Projekt (Project) aus app_settings self.project = None - + # Das aktuelle ProjectData self.pdf_project = None - + # Hash-Berechnungs-Thread self.hash_calculator_thread = None # Transformations-Thread self.transformation_thread = None + # Mapping: xml_file_path_str → QTreeWidgetItem (für Progress Bar und Icon Updates) + self.xml_item_map = {} + # Theme-Menü initialisieren self._setup_theme_menu() - + # Vorhandene Projekte-Menü initialisieren self._setup_projects_menu() - - # - if (theme := app_settings.theme): + + # + if theme := app_settings.theme: self.change_theme(theme) else: - self.change_theme('Fusion') + self.change_theme("Fusion") # Bilder laden self._load_images() # Signale und Slots verbinden self._connect_signals() - + # Kontextmenü für TreeWidget einrichten self._setup_tree_context_menu() - + # TreeWidget Styling für größeren vertikalen Abstand self._setup_tree_widget_styling() - + # Drag&Drop für TreeWidget aktivieren self._setup_drag_drop() @@ -278,47 +295,45 @@ class MainWindow(QMainWindow): self.ui.actionVorhandene_Projekte.setEnabled(False) self.ui.actionVorhandene_Projekte.setText("Vorhandene Projekte (keine vorhanden)") return - + # Projekte vorhanden - Menü aktivieren und Untermenü erstellen self.ui.actionVorhandene_Projekte.setEnabled(True) self.ui.actionVorhandene_Projekte.setText("Vorhandene Projekte") - + # Erstelle ein Untermenü für die Projekte projects_menu = QMenu(self) - + # Füge jedes Projekt als Menü-Eintrag hinzu for project in app_settings.pdf_projects: project_action = QAction(project.name, self) project_action.setToolTip(f"Projekt-Ordner: {project.project_dir}") - + # Verbinde die Aktion mit der Projekt-Öffnen-Funktion - project_action.triggered.connect( - lambda checked, proj=project: self.open_existing_project(proj) - ) - + project_action.triggered.connect(lambda checked, proj=project: self.open_existing_project(proj)) + projects_menu.addAction(project_action) - + # Setze das Untermenü für die Aktion self.ui.actionVorhandene_Projekte.setMenu(projects_menu) - + print(f"Projekte-Menü initialisiert mit {len(app_settings.pdf_projects)} Projekten") def open_existing_project(self, project: Project): """ Öffnet ein vorhandenes Projekt. - + Args: project: Das zu öffnende PdfProject-Objekt """ print(f"Öffne Projekt: {project.name}") print(f"Projekt-Ordner: {project.project_dir}") - + self.project = project - + try: # Prüfe ob project.yaml existiert und nicht leer ist - project_yaml_path = Path(project.project_dir) / 'project.yaml' - + project_yaml_path = Path(project.project_dir) / "project.yaml" + if project_yaml_path.exists() and project_yaml_path.stat().st_size > 0: # Versuche die Projekt-Einstellungen zu laden self.pdf_project = ProjectData.readSettings(project_dir=project.project_dir) @@ -327,17 +342,20 @@ class MainWindow(QMainWindow): # Erstelle Standard-Projekt-Einstellungen wenn Datei leer oder nicht vorhanden print("project.yaml ist leer oder nicht vorhanden, erstelle Standard-Einstellungen") self.pdf_project = ProjectData() - + # Speichere die Standard-Einstellungen in die project.yaml self.pdf_project.writeSettings(project_dir=project.project_dir) print(f"Standard-Projekt-Einstellungen in {project_yaml_path} gespeichert") - + # Lade die Nodes in das TreeWidget self._load_nodes_to_tree() - + # Starte Hash-Berechnung für alle XML-Dateien self._start_xml_hash_calculation() - + + # Setze Icons für bereits existierende Diff-PDFs + self._update_diff_icons_for_existing_pdfs() + except Exception as e: print(f"Fehler beim Laden des Projekts '{project.name}': {e}") # Fallback: Erstelle Standard-Einstellungen @@ -436,18 +454,16 @@ class MainWindow(QMainWindow): new_doc.load(new_pdf_path) # Warten bis PDFs geladen sind - if (diff_doc.status() != QPdfDocument.Status.Ready or - ref_doc.status() != QPdfDocument.Status.Ready or - new_doc.status() != QPdfDocument.Status.Ready): + if ( + diff_doc.status() != QPdfDocument.Status.Ready + or ref_doc.status() != QPdfDocument.Status.Ready + or new_doc.status() != QPdfDocument.Status.Ready + ): print(f"Fehler beim Laden der PDFs für {pdf_filename}") continue # PDF-Dokumente für später speichern - self.pdf_documents[pdf_filename] = { - 'diff': diff_doc, - 'ref': ref_doc, - 'new': new_doc - } + self.pdf_documents[pdf_filename] = {"diff": diff_doc, "ref": ref_doc, "new": new_doc} print(f"PDFs geladen: {pdf_filename}") print(f" diff: {diff_doc.pageCount()} Seiten") @@ -464,14 +480,15 @@ class MainWindow(QMainWindow): for page_num in range(max_pages): # Nur diff-Seite für Thumbnail rendern page_size = diff_doc.pagePointSize(page_num) - + # Skalierung für Thumbnail (entspricht ca. Matrix(1.0, 1.0) in PyMuPDF) scale_factor = 200.0 / page_size.width() # 200 Pixel Breite für Thumbnail - + # Seite rendern - page_image = diff_doc.render(page_num, QSize(int(page_size.width() * scale_factor), - int(page_size.height() * scale_factor))) - + page_image = diff_doc.render( + page_num, QSize(int(page_size.width() * scale_factor), int(page_size.height() * scale_factor)) + ) + diff_pixmap = QPixmap.fromImage(page_image) # Thumbnail erstellen und zur linken Spalte hinzufügen @@ -488,10 +505,7 @@ class MainWindow(QMainWindow): self.ui.verticalLayout_2.addWidget(thumbnail_info) # Beziehung zwischen Thumbnail und Seitennummer speichern - self.thumbnail_to_page[thumbnail] = { - 'pdf_filename': pdf_filename, - 'page_num': page_num - } + self.thumbnail_to_page[thumbnail] = {"pdf_filename": pdf_filename, "page_num": page_num} # Click-Event für das Thumbnail einrichten thumbnail.mousePressEvent = lambda event, t=thumbnail: self.on_thumbnail_clicked(event, t) @@ -519,7 +533,9 @@ class MainWindow(QMainWindow): # Drag-to-Scroll Events für das große Bild einrichten self.fullsize_label.mousePressEvent = lambda event: self.on_fullsize_mouse_press(event, self.fullsize_label) self.fullsize_label.mouseMoveEvent = lambda event: self.on_fullsize_mouse_move(event, self.fullsize_label) - self.fullsize_label.mouseReleaseEvent = lambda event: self.on_fullsize_mouse_release(event, self.fullsize_label) + self.fullsize_label.mouseReleaseEvent = lambda event: self.on_fullsize_mouse_release( + event, self.fullsize_label + ) # Zeige die erste Seite initial an if self.current_pdf: @@ -529,13 +545,13 @@ class MainWindow(QMainWindow): """ Rendert und zeigt eine spezifische Seite in der Vollansicht an. Cached die gerenderten Pixmaps für bessere Performance. - + Args: pdf_filename: Name der PDF-Datei page_num: Seitennummer (0-basiert) """ print(f"Rendere Seite {page_num + 1} von {pdf_filename}") - + if pdf_filename not in self.pdf_documents: print(f"PDF-Dokument {pdf_filename} nicht gefunden") return @@ -544,26 +560,25 @@ class MainWindow(QMainWindow): try: docs = self.pdf_documents[pdf_filename] - + # Diff-Seite laden (bestimmt die Abmessungen) - diff_doc = docs['diff'] + diff_doc = docs["diff"] page_size = diff_doc.pagePointSize(page_num) - + # Hohe Auflösung für Vollbild (entspricht ca. Matrix(2.0, 2.0) in PyMuPDF) scale_factor = 2.0 - render_size = QSize(int(page_size.width() * scale_factor), - int(page_size.height() * scale_factor)) + render_size = QSize(int(page_size.width() * scale_factor), int(page_size.height() * scale_factor)) # Diff-Seite rendern (immer vorhanden) diff_image = diff_doc.render(page_num, render_size) diff_pixmap = QPixmap.fromImage(diff_image) - + # Ermittle die Abmessungen für weiße Seiten diff_width = diff_pixmap.width() diff_height = diff_pixmap.height() # Ref-Seite prüfen und rendern oder weiße Seite erstellen - ref_doc = docs['ref'] + ref_doc = docs["ref"] if page_num < ref_doc.pageCount(): ref_image = ref_doc.render(page_num, render_size) ref_pixmap = QPixmap.fromImage(ref_image) @@ -575,7 +590,7 @@ class MainWindow(QMainWindow): print(f"Weiße Ref-Seite {page_num + 1} erstellt") # New-Seite prüfen und rendern oder weiße Seite erstellen - new_doc = docs['new'] + new_doc = docs["new"] if page_num < new_doc.pageCount(): new_image = new_doc.render(page_num, render_size) new_pixmap = QPixmap.fromImage(new_image) @@ -587,11 +602,7 @@ class MainWindow(QMainWindow): print(f"Weiße New-Seite {page_num + 1} erstellt") # Cache die gerenderten Pixmaps für schnelle Alpha/Zoom-Operationen - self.current_rendered_pixmaps = { - 'ref': ref_pixmap, - 'diff': diff_pixmap, - 'new': new_pixmap - } + self.current_rendered_pixmaps = {"ref": ref_pixmap, "diff": diff_pixmap, "new": new_pixmap} # Aktualisiere aktuelle Seite self.current_page = page_num @@ -620,9 +631,9 @@ class MainWindow(QMainWindow): return # Hole die gecachten Pixmaps - ref_pixmap = self.current_rendered_pixmaps['ref'] - diff_pixmap = self.current_rendered_pixmaps['diff'] - new_pixmap = self.current_rendered_pixmaps['new'] + ref_pixmap = self.current_rendered_pixmaps["ref"] + diff_pixmap = self.current_rendered_pixmaps["diff"] + new_pixmap = self.current_rendered_pixmaps["new"] # Erstelle das überlagerte Bild mit aktuellem Alpha-Wert alpha_value = self.ui.alpha.value() @@ -664,13 +675,13 @@ class MainWindow(QMainWindow): if alpha_value <= 0: # Alpha von -100 bis 0: Übergang von ref zu diff - ref_opacity = 1.0 # - (alpha_value + 100) / 100.0 + ref_opacity = 1.0 # - (alpha_value + 100) / 100.0 diff_opacity = (alpha_value + 100) / 100.0 new_opacity = 0.0 else: # Alpha von 0 bis 100: Übergang von diff zu new ref_opacity = 0.0 - diff_opacity = 1.0 # - alpha_value / 100.0 + diff_opacity = 1.0 # - alpha_value / 100.0 new_opacity = alpha_value / 100.0 # Zeichne die Ebenen mit entsprechender Transparenz @@ -710,11 +721,11 @@ class MainWindow(QMainWindow): # Alpha-Slider verbinden self.ui.alpha.valueChanged.connect(self.on_alpha_changed) self.ui.alpha.mouseDoubleClickEvent = lambda event: self.ui.alpha.setValue(0) - + # Menü-Aktionen verbinden self.ui.actionNeu.triggered.connect(self.open_new_project_dialog) self.ui.actionEinstellungen.triggered.connect(self.open_settings_dialog) - + # Button "lade aus FN2" verbinden self.ui.pB_lade_aus_fn2.clicked.connect(self.on_load_from_fn2_clicked) @@ -747,24 +758,24 @@ class MainWindow(QMainWindow): /*margin: 2px 0px;*/ } """ - + # Wende das Stylesheet auf das TreeWidget an self.ui.treeWidget.setStyleSheet(tree_stylesheet) print("TreeWidget Styling für größeren vertikalen Abstand angewendet") - + except Exception as e: print(f"Fehler beim Anwenden des TreeWidget-Stylings: {e}") def _show_tree_context_menu(self, position): """ Zeigt das Kontextmenü für das TreeWidget an. - + Args: position: Position des Rechtsklicks """ # Hole das Item an der Position item = self.ui.treeWidget.itemAt(position) - + if not item: # Kein Item gefunden - zeige Kontextmenü für Root-Elemente node_type = "Unknown" @@ -774,7 +785,7 @@ class MainWindow(QMainWindow): node_type = self._get_node_type_from_item(item) # Erstelle das entsprechende Kontextmenü context_menu = self._create_context_menu_for_type(node_type, item) - + if context_menu: # Zeige das Kontextmenü an der globalen Position global_pos = self.ui.treeWidget.mapToGlobal(position) @@ -783,17 +794,17 @@ class MainWindow(QMainWindow): def _get_node_type_from_item(self, item): """ Bestimmt den Node-Typ basierend auf dem TreeWidgetItem. - + Args: item: Das TreeWidgetItem - + Returns: str: Der Node-Typ ('TreeNode', 'XslFile', 'XmlFile' oder 'Unknown') """ try: # Prüfe ob das Item ein Parent hat (dann ist es ein Child-Item) parent_item = item.parent() - + if parent_item: # Child-Item - prüfe ob es ein XML-File ist text = item.text(0) @@ -811,7 +822,7 @@ class MainWindow(QMainWindow): else: # Root-Item - bestimme Typ basierend auf gespeicherten Daten return self._determine_node_type_from_data(item) - + except Exception as e: print(f"Fehler beim Bestimmen des Node-Typs: {e}") return "Unknown" @@ -819,10 +830,10 @@ class MainWindow(QMainWindow): def _determine_node_type_from_data(self, item): """ Bestimmt den Node-Typ basierend auf den gespeicherten Daten im Item. - + Args: item: Das TreeWidgetItem - + Returns: str: Der Node-Typ ('TreeNode', 'XslFile' oder 'Unknown') """ @@ -831,7 +842,7 @@ class MainWindow(QMainWindow): node = item.data(0, Qt.ItemDataRole.UserRole) if not node: return "Unknown" - + # Bestimme den Typ direkt vom Node-Objekt if isinstance(node, TreeNode): return "TreeNode" @@ -839,9 +850,9 @@ class MainWindow(QMainWindow): return "XslFile" elif isinstance(node, XmlFile): return "XmlFile" - + return "Unknown" - + except Exception as e: print(f"Fehler beim Bestimmen des Node-Typs aus Daten: {e}") return "Unknown" @@ -849,64 +860,64 @@ class MainWindow(QMainWindow): def _find_node_by_id(self, nodes, target_id): """ Sucht rekursiv nach einem Node mit der angegebenen ID. - + Args: nodes: Liste der Nodes zum Durchsuchen target_id: Die zu suchende ID - + Returns: TreeNode|XslFile|None: Der gefundene Node oder None """ for node in nodes: if node.id == target_id: return node - + # Rekursiv in Knotenn suchen (nur bei TreeNode) if isinstance(node, TreeNode) and node.children: found = self._find_node_by_id(node.children, target_id) if found: return found - + return None def _create_context_menu_for_type(self, node_type, item): """ Erstellt das Kontextmenü für den angegebenen Node-Typ. - + Args: node_type: Der Typ des Nodes ('TreeNode', 'XslFile', 'XmlFile') item: Das TreeWidgetItem - + Returns: QMenu: Das erstellte Kontextmenü oder None """ try: menu = QMenu(self) - + if node_type == "TreeNode": # Kontextmenü für TreeNode action_add_child = QAction("Unterknoten hinzufügen", self) - action_add_child.setIcon(QIcon(QIcon.fromTheme(u"folder-new"))) + action_add_child.setIcon(QIcon(QIcon.fromTheme("folder-new"))) action_add_child.triggered.connect(lambda: self._add_tree_node_child(item)) menu.addAction(action_add_child) - + action_add_xsl = QAction("XSL-Datei hinzufügen", self) action_add_xsl.setIcon(QIcon(QIcon.fromTheme("document-new"))) action_add_xsl.triggered.connect(lambda: self._add_xsl_file_to_node(item)) menu.addAction(action_add_xsl) - + menu.addSeparator() - + action_edit = QAction("Bearbeiten", self) action_edit.setIcon(QIcon(QIcon.fromTheme(QIcon.ThemeIcon.DocumentProperties))) action_edit.triggered.connect(lambda: self._edit_tree_node(item)) menu.addAction(action_edit) - + action_delete = QAction("Löschen", self) action_delete.setIcon(QIcon(QIcon.fromTheme(QIcon.ThemeIcon.EditDelete))) action_delete.triggered.connect(lambda: self._delete_tree_node(item)) menu.addAction(action_delete) - + elif node_type == "XslFile": # Kontextmenü für XslFile action_add_xml = QAction("XML-Datei hinzufügen", self) @@ -938,7 +949,7 @@ class MainWindow(QMainWindow): action_delete.setIcon(QIcon(QIcon.fromTheme(QIcon.ThemeIcon.EditDelete))) action_delete.triggered.connect(lambda: self._delete_xsl_file(item)) menu.addAction(action_delete) - + elif node_type == "XmlFile": # Kontextmenü für XmlFile # Transformations-Aktionen @@ -963,16 +974,16 @@ class MainWindow(QMainWindow): action_delete.setIcon(QIcon(QIcon.fromTheme(QIcon.ThemeIcon.EditDelete))) action_delete.triggered.connect(lambda: self._delete_xml_file(item)) menu.addAction(action_delete) - + else: # Unbekannter Typ oder leerer Bereich - Menü für Root-Elemente action_add_tree_node = QAction("Unterknoten hinzufügen", self) - action_add_tree_node.setIcon(QIcon(QIcon.fromTheme(u"folder-new"))) + action_add_tree_node.setIcon(QIcon(QIcon.fromTheme("folder-new"))) action_add_tree_node.triggered.connect(lambda: self._add_root_tree_node()) menu.addAction(action_add_tree_node) - + return menu - + except Exception as e: print(f"Fehler beim Erstellen des Kontextmenüs: {e}") return None @@ -981,12 +992,12 @@ class MainWindow(QMainWindow): """ Wird ausgeführt, wenn der Alpha-Slider geändert wird. Optimiert: Verwendet gecachte Pixmaps statt erneutes PDF-Rendering. - + Args: alpha_value: Der neue Alpha-Wert (-100 bis 100) """ print(f"Alpha geändert auf {alpha_value}") - + start_time = time.time() # Verwende gecachte Pixmaps für schnelle Alpha-Änderungen self.update_current_display() @@ -1012,72 +1023,74 @@ class MainWindow(QMainWindow): if dialog.exec() == PdfProjectDlg.DialogCode.Accepted: # Hole die Projektdaten aus dem Dialog project_data = dialog.get_project_data() - + # Erstelle neue ID für das Projekt new_id = max([p.id for p in app_settings.pdf_projects], default=0) + 1 - + # Erstelle PdfProject-Objekt new_project = Project( id=new_id, - name=project_data['name'], - project_dir=Path(project_data['project_dir']), - java_vm_id=project_data['java_vm_id'] if project_data['java_vm_id'] != -1 else 1, - diff_pdf_id=project_data['diff_pdf_id'] if project_data['diff_pdf_id'] != -1 else 1, - saxon_jar_id=project_data['saxon_jar_id'] if project_data['saxon_jar_id'] != -1 else 1, - apache_fop_id=project_data['apache_fop_id'] if project_data['apache_fop_id'] != -1 else 1, - xsl_dir_id=project_data['xsl_dir_id'] if project_data['xsl_dir_id'] != -1 else 1, - postgre_sql_db_id=project_data['postgre_sql_db_id'] if project_data['postgre_sql_db_id'] != -1 else 1, + name=project_data["name"], + project_dir=Path(project_data["project_dir"]), + java_vm_id=project_data["java_vm_id"] if project_data["java_vm_id"] != -1 else 1, + diff_pdf_id=project_data["diff_pdf_id"] if project_data["diff_pdf_id"] != -1 else 1, + saxon_jar_id=project_data["saxon_jar_id"] if project_data["saxon_jar_id"] != -1 else 1, + apache_fop_id=project_data["apache_fop_id"] if project_data["apache_fop_id"] != -1 else 1, + xsl_dir_id=project_data["xsl_dir_id"] if project_data["xsl_dir_id"] != -1 else 1, + postgre_sql_db_id=project_data["postgre_sql_db_id"] + if project_data["postgre_sql_db_id"] != -1 + else 1, ) - + # Erstelle Projekt-Ordnerstruktur self._create_project_structure(new_project) - + # Füge das neue Projekt zu app_settings hinzu app_settings.pdf_projects.append(new_project) - + # Speichere app_settings app_settings.save() - + print(f"Neues PDF-Projekt '{project_data['name']}' wurde erstellt und gespeichert") print(f"Projekt-ID: {new_id}") print(f"Projekt-Ordner: {project_data['project_dir']}") - + # Aktualisiere das Projekte-Menü self._setup_projects_menu() - + except Exception as e: print(f"Fehler beim Erstellen des neuen Projekts: {e}") - + def _create_project_structure(self, project: Project): """ Erstellt die Ordnerstruktur und project.yaml-Datei für ein neues Projekt. - + Args: project: Das PdfProject-Objekt """ try: project_dir = Path(project.project_dir) - + # Erstelle Unterordner - subdirs = ['xml', 'new', 'diff', 'ref', 'tmp'] + subdirs = ["xml", "new", "diff", "ref", "tmp"] for subdir in subdirs: subdir_path = project_dir / subdir subdir_path.mkdir(parents=True, exist_ok=True) print(f"Ordner erstellt: {subdir_path}") - - project_yaml_path = project_dir / 'project.yaml' - + + project_yaml_path = project_dir / "project.yaml" + # Erstelle Standard-Projekt-Einstellungen und speichere sie if not project_yaml_path.exists(): # Erstelle Standard-PdfProjectSettings default_settings = ProjectData() - + # Speichere die Standard-Einstellungen in die project.yaml default_settings.writeSettings(project_dir=project_dir) print(f"project.yaml mit Standard-Einstellungen erstellt: {project_yaml_path}") else: print(f"project.yaml existiert bereits: {project_yaml_path}") - + except Exception as e: print(f"Fehler beim Erstellen der Projekt-Struktur: {e}") raise @@ -1096,11 +1109,11 @@ class MainWindow(QMainWindow): """ page_info = self.thumbnail_to_page.get(thumbnail) if page_info: - pdf_filename = page_info['pdf_filename'] - page_num = page_info['page_num'] - + pdf_filename = page_info["pdf_filename"] + page_num = page_info["page_num"] + print(f"Thumbnail für Seite {page_num + 1} von {pdf_filename} wurde angeklickt") - + # Rendere und zeige die gewählte Seite an self.render_and_display_page(pdf_filename, page_num) @@ -1159,94 +1172,105 @@ class MainWindow(QMainWindow): Sortiert die Items alphabetisch nach ihrer ID. """ print("Lade Nodes in TreeWidget...") - + try: # TreeWidget leeren self.ui.treeWidget.clear() - + + # Lösche XML-Item-Map + self.xml_item_map.clear() + # Prüfe ob pdf_project existiert und Nodes hat - if not hasattr(self, 'pdf_project') or not self.pdf_project: + if not hasattr(self, "pdf_project") or not self.pdf_project: print("Keine Projekt-Einstellungen verfügbar") return - + if not self.pdf_project.nodes: print("Keine Nodes in den Projekt-Einstellungen gefunden") return - + # Sortiere Root-Nodes alphabetisch nach ID sorted_nodes = sorted(self.pdf_project.nodes, key=lambda node: node.id) - + # Lade alle Root-Nodes (sortiert) for node in sorted_nodes: tree_item = self._create_tree_item_from_node(node) self.ui.treeWidget.addTopLevelItem(tree_item) - + print(f"{len(self.pdf_project.nodes)} Root-Nodes in TreeWidget geladen (alphabetisch sortiert)") - + except Exception as e: print(f"Fehler beim Laden der Nodes in TreeWidget: {e}") - + def _create_tree_item_from_node(self, node): """ Erstellt ein QTreeWidgetItem aus einem TreeNode oder XslFile. Speichert die vollständigen Node-Daten für spätere Verwendung. - + Args: node: TreeNode oder XslFile Objekt - + Returns: QTreeWidgetItem: Das erstellte Tree-Item mit vollständigen Node-Daten """ try: # Erstelle Tree-Item item = QTreeWidgetItem() - + # Setze die Bezeichnung in Spalte 0 bez_text = str(node.bez) if node.bez else "" item.setText(0, bez_text) - + # Speichere das komplette Node-Objekt als UserRole-Daten # Dies ermöglicht späteren Zugriff auf alle Node-Eigenschaften item.setData(0, Qt.ItemDataRole.UserRole, node) - + # Setze zusätzliche Informationen in Spalte 1 if isinstance(node, TreeNode): # TreeNode: Zeige Anzahl der Knoten child_count = len(node.children) if node.children else 0 item.setText(1, f"{child_count} Knoten") - + # Speichere zusätzlich die Node-ID in UserRole+1 für Kompatibilität item.setData(0, Qt.ItemDataRole.UserRole + 1, node.id) - + # Lade Knoten rekursiv (sortiert nach ID) if node.children: sorted_children = sorted(node.children, key=lambda child: child.id) for child in sorted_children: child_item = self._create_tree_item_from_node(child) item.addChild(child_item) - + elif isinstance(node, XslFile): # XslFile: Zeige XSL-Datei-Pfad item.setText(1, str(node.xsl_file)) - + # Speichere zusätzlich die Node-ID in UserRole+1 für Kompatibilität item.setData(0, Qt.ItemDataRole.UserRole + 1, node.id) - + # Lade XML-Dateien als Knoten if node.xmls: for xml in node.xmls: xml_item = QTreeWidgetItem() xml_item.setText(0, f"XML: {xml.xml.name}") xml_item.setText(1, str(xml.xml)) - + # Speichere auch das XmlFile-Objekt für XML-Items xml_item.setData(0, Qt.ItemDataRole.UserRole, xml) xml_item.setData(0, Qt.ItemDataRole.UserRole + 1, f"xml_{xml.xml.name}") - + item.addChild(xml_item) - + + # Speichere XML-Item für spätere Widget-Updates (Progress Bar, Icon) + # Key: "xml_path|xsl_id" um mehrfache Verwendung derselben XML zu unterstützen + xml_path_str = str(xml.xml) + xsl_id_str = "_".join(str(x) for x in node.id) + map_key = f"{xml_path_str}|{xsl_id_str}" + self.xml_item_map[map_key] = xml_item + logger.debug(f"XML-Item zur Map hinzugefügt: '{map_key}'") + return item - + except Exception as e: print(f"Fehler beim Erstellen des Tree-Items: {e}") # Fallback: Erstelle einfaches Item @@ -1255,6 +1279,144 @@ class MainWindow(QMainWindow): fallback_item.setText(1, str(e)) return fallback_item + def _create_centered_progress_bar(self) -> tuple[QWidget, QProgressBar]: + """ + Erstellt eine zentrierte Progress Bar in einem Container-Widget. + + Returns: + tuple: (container_widget, progress_bar) + """ + # Container-Widget erstellen + container = QWidget() + layout = QHBoxLayout(container) + layout.setContentsMargins(0, 0, 0, 0) + layout.setAlignment(Qt.AlignmentFlag.AlignCenter) + + # Progress Bar erstellen (indeterminate mode für pulsierenden Effekt) + progress_bar = QProgressBar() + progress_bar.setMinimum(0) + progress_bar.setMaximum(0) # Pulsierend + progress_bar.setMaximumWidth(80) # Kompakte Breite + progress_bar.setMaximumHeight(16) # Kompakte Höhe + progress_bar.setTextVisible(False) + + layout.addWidget(progress_bar) + + return container, progress_bar + + def _create_centered_diff_icon(self, xml_file_path: Path) -> QWidget: + """ + Erstellt ein zentriertes, klickbares Icon für Diff-PDF. + + Args: + xml_file_path: Pfad zur XML-Datei (für Event-Handler) + + Returns: + QWidget: Container mit klickbarem Icon + """ + # Container-Widget + container = QWidget() + layout = QHBoxLayout(container) + layout.setContentsMargins(0, 0, 0, 0) + layout.setAlignment(Qt.AlignmentFlag.AlignCenter) + + # Icon-Label + icon_label = QLabel() + icon_label.setPixmap(QIcon.fromTheme("document-preview").pixmap(16, 16)) + icon_label.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) + icon_label.setToolTip("Diff-PDF öffnen (Doppelklick)") + + # Klick-Event für Icon (Doppelklick öffnet PDF) + icon_label.mouseDoubleClickEvent = lambda event: self._open_diff_pdf(xml_file_path) + + layout.addWidget(icon_label) + + return container + + def _open_diff_pdf(self, xml_file_path: Path): + """ + Öffnet die Diff-PDF für eine XML-Datei mit dem Standard-PDF-Viewer. + + Args: + xml_file_path: Pfad zur XML-Datei (relativ) + """ + import subprocess + import sys + + try: + # Ermittle Diff-PDF-Pfad basierend auf XML-Datei + # WICHTIG: Berücksichtige XSL-ID im Dateinamen! + # Vereinfachung: Suche nach allen PDFs die mit xml_stem beginnen + xml_stem = xml_file_path.stem + diff_dir = self.project.project_dir / "diff" + + # Finde passende Diff-PDF (könnte mehrere geben bei verschiedenen XSL-IDs) + matching_pdfs = list(diff_dir.glob(f"{xml_stem}*.pdf")) + + if not matching_pdfs: + QMessageBox.information(self, "Keine Diff-PDF", f"Keine Diff-PDF für {xml_file_path.name} gefunden") + return + + # Bei mehreren: Nehme neueste + diff_pdf = max(matching_pdfs, key=lambda p: p.stat().st_mtime) + + logger.info(f"Öffne Diff-PDF: {diff_pdf}") + + # Öffne PDF mit Plattform-spezifischem Befehl + if sys.platform == "win32": + subprocess.Popen(["start", str(diff_pdf)], shell=True) + elif sys.platform == "darwin": + subprocess.Popen(["open", str(diff_pdf)]) + else: + subprocess.Popen(["xdg-open", str(diff_pdf)]) + + logger.info(f"Diff-PDF geöffnet: {diff_pdf}") + + except Exception as e: + logger.error(f"Fehler beim Öffnen der Diff-PDF: {e}") + QMessageBox.critical(self, "Fehler", f"Konnte Diff-PDF nicht öffnen: {str(e)}") + + def _update_diff_icons_for_existing_pdfs(self): + """ + Durchläuft alle XML-Items und setzt Icons für bereits existierende Diff-PDFs. + Wird nach dem Laden eines Projekts aufgerufen. + """ + if not hasattr(self, "project") or not self.project: + logger.debug("Kein Projekt geladen, überspringe Diff-Icon-Update") + return + + diff_dir = self.project.project_dir / "diff" + if not diff_dir.exists(): + logger.debug(f"Diff-Ordner existiert nicht: {diff_dir}") + return + + logger.info("Aktualisiere Diff-Icons für existierende PDFs...") + logger.info(f"XML-Item-Map hat {len(self.xml_item_map)} Einträge") + + # Durchlaufe alle XML-Items in der Map + icon_count = 0 + for map_key, tree_item in self.xml_item_map.items(): + # Map-Key hat Format "xml_path|xsl_id" + parts = map_key.split("|") + if len(parts) != 2: + continue + + xml_path_str, xsl_id_str = parts + xml_path = Path(xml_path_str) + xml_stem = xml_path.stem + + # Diff-PDF-Dateiname: "{xml_stem}_xsl_{xsl_id_str}.pdf" + expected_pdf = diff_dir / f"{xml_stem}_xsl_{xsl_id_str}.pdf" + + if expected_pdf.exists(): + # Icon setzen + icon_widget = self._create_centered_diff_icon(xml_path) + self.ui.treeWidget.setItemWidget(tree_item, 2, icon_widget) + icon_count += 1 + logger.debug(f"Diff-Icon für existierende PDF gesetzt: {map_key}") + + logger.info(f"{icon_count} Diff-Icons für existierende PDFs gesetzt") + # Kontextmenü-Aktionen für TreeNode def _add_tree_node_child(self, parent_item): """Fügt einen Unterknoten zu einem TreeNode hinzu.""" @@ -1269,27 +1431,27 @@ class MainWindow(QMainWindow): def _edit_tree_node(self, item): """ Bearbeitet einen TreeNode. - + Args: item: Das TreeWidgetItem des TreeNode """ print(f"TreeNode bearbeiten: {item.text(0)}") - + try: # Hole das Node-Objekt aus dem TreeWidgetItem node = item.data(0, Qt.ItemDataRole.UserRole) if not node or not isinstance(node, TreeNode): QMessageBox.warning(self, "Warnung", "Kein gültiger TreeNode gefunden.") return - + # Prüfe ob Projekt verfügbar ist if not self.pdf_project: QMessageBox.warning(self, "Warnung", "Keine Projekt-Einstellungen verfügbar.") return - + # Sammle Eltern-Parameter parent_params = self._collect_parent_params(item) - + # Erstelle und zeige den Dialog dialog = TreeNodeEditDialog(self, node, parent_params) if dialog.exec() == TreeNodeEditDialog.DialogCode.Accepted: @@ -1297,20 +1459,20 @@ class MainWindow(QMainWindow): data = dialog.get_data() if data: # Aktualisiere den Node - node.bez = data['bez'] - node.xslt_params = data['xslt_params'] - + node.bez = data["bez"] + node.xslt_params = data["xslt_params"] + print(f"TreeNode '{node.bez}' wurde aktualisiert") print(f"XSLT-Parameter: {node.xslt_params}") - + # Speichere die Änderungen self._save_project_settings() - + # Aktualisiere das TreeWidget self._load_nodes_to_tree() - + # QMessageBox.information(self, "Erfolg", "TreeNode wurde erfolgreich aktualisiert.") - + except Exception as e: error_msg = f"Fehler beim Bearbeiten des TreeNode: {str(e)}" print(error_msg) @@ -1325,54 +1487,51 @@ class MainWindow(QMainWindow): def _add_xml_file_to_xsl(self, parent_item): """ Fügt eine XML-Datei zu einer XSL-Datei hinzu. - + Args: parent_item: Das TreeWidgetItem des XslFile-Nodes """ print(f"XML-Datei zu XslFile hinzufügen: {parent_item.text(0)}") - + try: # Prüfe ob ein Projekt geladen ist - if not hasattr(self, 'project') or not self.project: + if not hasattr(self, "project") or not self.project: QMessageBox.warning(self, "Warnung", "Kein Projekt geladen. Bitte öffnen Sie zuerst ein Projekt.") return - - if not hasattr(self, 'pdf_project') or not self.pdf_project: + + if not hasattr(self, "pdf_project") or not self.pdf_project: QMessageBox.warning(self, "Warnung", "Keine Projekt-Einstellungen geladen.") return - + # Hole das XslFile-Node-Objekt direkt aus dem TreeWidgetItem xsl_node = parent_item.data(0, Qt.ItemDataRole.UserRole) if not xsl_node or not isinstance(xsl_node, XslFile): QMessageBox.warning(self, "Warnung", "Keine gültige XSL-Datei-Node gefunden.") return - + # Öffne Datei-Dialog zum Auswählen der XML-Datei xml_file_path, _ = QFileDialog.getOpenFileName( - self, - "XML-Datei auswählen", - "", - "XML-Dateien (*.xml);;Alle Dateien (*)" + self, "XML-Datei auswählen", "", "XML-Dateien (*.xml);;Alle Dateien (*)" ) - + if not xml_file_path: # Benutzer hat abgebrochen return - + xml_file_path = Path(xml_file_path) - + # Prüfe ob die Datei existiert if not xml_file_path.exists(): QMessageBox.critical(self, "Fehler", f"Die ausgewählte XML-Datei existiert nicht:\n{xml_file_path}") return - + # Erstelle xml-Ordner im Projekt-Verzeichnis falls er nicht existiert xml_dir = Path(self.project.project_dir) / "xml" xml_dir.mkdir(parents=True, exist_ok=True) - + # Bestimme den Ziel-Pfad in xml-Ordner target_xml_path = xml_dir / xml_file_path.name - + # Prüfe ob eine Datei mit gleichem Namen bereits existiert if target_xml_path.exists(): reply = QMessageBox.question( @@ -1381,55 +1540,55 @@ class MainWindow(QMainWindow): f"Eine XML-Datei mit dem Namen '{xml_file_path.name}' existiert bereits im xml-Ordner.\n\n" "Möchten Sie sie überschreiben?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, - QMessageBox.StandardButton.No + QMessageBox.StandardButton.No, ) - + if reply != QMessageBox.StandardButton.Yes: return - + # Kopiere die XML-Datei in den xml-Ordner shutil.copy2(xml_file_path, target_xml_path) print(f"XML-Datei kopiert: {xml_file_path} -> {target_xml_path}") - + # Erstelle relatives Path zur XML-Datei (relativ zum xml-Ordner) relative_xml_path = Path("xml") / xml_file_path.name - + # Prüfe ob diese XML-Datei bereits in der XslFile-Node vorhanden ist existing_xml = None for xml_file in xsl_node.xmls: if xml_file.xml == relative_xml_path: existing_xml = xml_file break - + if existing_xml: QMessageBox.information( self, "XML-Datei bereits vorhanden", - f"Die XML-Datei '{xml_file_path.name}' ist bereits in dieser XSL-Datei enthalten." + f"Die XML-Datei '{xml_file_path.name}' ist bereits in dieser XSL-Datei enthalten.", ) return - + # Erstelle neues XmlFile-Objekt und füge es zur XslFile-Node hinzu new_xml_file = XmlFile(xml=relative_xml_path) xsl_node.xmls.append(new_xml_file) - + print(f"XML-Datei '{xml_file_path.name}' zu XslFile-Node '{xsl_node.bez}' hinzugefügt") - + # Berechne Hash für die neue XML-Datei self._calculate_hash_for_xml_file(new_xml_file) - + # Speichere die aktualisierten Projekt-Einstellungen self._save_project_settings() - + # Aktualisiere das TreeWidget self._load_nodes_to_tree() - + # QMessageBox.information( # self, # "Erfolg", # f"XML-Datei '{xml_file_path.name}' wurde erfolgreich hinzugefügt und in den xml-Ordner kopiert." # ) - + except Exception as e: error_msg = f"Fehler beim Hinzufügen der XML-Datei: {str(e)}" print(error_msg) @@ -1438,22 +1597,22 @@ class MainWindow(QMainWindow): def _edit_xsl_file(self, item): """ Bearbeitet eine XSL-Datei. - + Args: item: Das TreeWidgetItem des XslFile """ print(f"XslFile bearbeiten: {item.text(0)}") - + try: # Hole das Node-Objekt aus dem TreeWidgetItem node = item.data(0, Qt.ItemDataRole.UserRole) if not node or not isinstance(node, XslFile): QMessageBox.warning(self, "Warnung", "Keine gültige XSL-Datei gefunden.") return - + # Sammle Eltern-Parameter parent_params = self._collect_parent_params(item) - + # Erstelle und zeige den Dialog dialog = XslFileEditDialog(self, node, parent_params) if dialog.exec() == XslFileEditDialog.DialogCode.Accepted: @@ -1461,20 +1620,20 @@ class MainWindow(QMainWindow): data = dialog.get_data() if data: # Aktualisiere den Node - node.bez = data['bez'] - node.xslt_params = data['xslt_params'] - + node.bez = data["bez"] + node.xslt_params = data["xslt_params"] + print(f"XslFile '{node.bez}' wurde aktualisiert") print(f"XSLT-Parameter: {node.xslt_params}") - + # Speichere die Änderungen self._save_project_settings() - + # Aktualisiere das TreeWidget self._load_nodes_to_tree() - + # QMessageBox.information(self, "Erfolg", "XSL-Datei wurde erfolgreich aktualisiert.") - + except Exception as e: error_msg = f"Fehler beim Bearbeiten der XSL-Datei: {str(e)}" print(error_msg) @@ -1494,40 +1653,40 @@ class MainWindow(QMainWindow): def _delete_xml_file(self, item): """ Löscht eine XML-Datei aus einem XSL-Knoten. - + Args: item: Das TreeWidgetItem der XML-Datei """ print(f"XmlFile löschen: {item.text(0)}") - + try: # Prüfe ob ein Projekt geladen ist - if not hasattr(self, 'project') or not self.project: + if not hasattr(self, "project") or not self.project: QMessageBox.warning(self, "Warnung", "Kein Projekt geladen. Bitte öffnen Sie zuerst ein Projekt.") return - - if not hasattr(self, 'pdf_project') or not self.pdf_project: + + if not hasattr(self, "pdf_project") or not self.pdf_project: QMessageBox.warning(self, "Warnung", "Keine Projekt-Einstellungen geladen.") return - + # Hole das XmlFile-Objekt aus dem TreeWidgetItem xml_file_obj = item.data(0, Qt.ItemDataRole.UserRole) if not xml_file_obj or not isinstance(xml_file_obj, XmlFile): QMessageBox.warning(self, "Warnung", "Keine gültige XML-Datei gefunden.") return - + # Hole das Eltern-Item (sollte ein XslFile sein) parent_item = item.parent() if not parent_item: QMessageBox.warning(self, "Warnung", "Eltern-XSL-Datei nicht gefunden.") return - + # Hole das XslFile-Objekt aus dem Eltern-Item xsl_file_obj = parent_item.data(0, Qt.ItemDataRole.UserRole) if not xsl_file_obj or not isinstance(xsl_file_obj, XslFile): QMessageBox.warning(self, "Warnung", "Keine gültige Eltern-XSL-Datei gefunden.") return - + # Bestätigungsdialog anzeigen xml_filename = xml_file_obj.xml.name reply = QMessageBox.question( @@ -1536,30 +1695,30 @@ class MainWindow(QMainWindow): f"Möchten Sie die XML-Datei '{xml_filename}' aus der XSL-Datei '{xsl_file_obj.bez}' entfernen?\n\n" "Die XML-Datei wird nur aus der Zuordnung entfernt, nicht physisch gelöscht.", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, - QMessageBox.StandardButton.No + QMessageBox.StandardButton.No, ) - + if reply != QMessageBox.StandardButton.Yes: print("Löschung abgebrochen") return - + # Entferne die XML-Datei aus der XslFile-Node xml_files_before = len(xsl_file_obj.xmls) xsl_file_obj.xmls = [xml for xml in xsl_file_obj.xmls if xml.xml != xml_file_obj.xml] xml_files_after = len(xsl_file_obj.xmls) - + if xml_files_before == xml_files_after: QMessageBox.warning(self, "Warnung", "XML-Datei konnte nicht aus der XSL-Datei entfernt werden.") return - + print(f"XML-Datei '{xml_filename}' aus XSL-Datei '{xsl_file_obj.bez}' entfernt") - + # Frage ob die physische Datei auch gelöscht werden soll xml_file_path = Path(self.project.project_dir) / xml_file_obj.xml if xml_file_path.exists(): # Prüfe ob die XML-Datei noch in anderen XSL-Dateien verwendet wird is_used_elsewhere = self._is_xml_file_used_elsewhere(xml_file_obj.xml, xsl_file_obj) - + if not is_used_elsewhere: delete_reply = QMessageBox.question( self, @@ -1567,43 +1726,41 @@ class MainWindow(QMainWindow): f"Die XML-Datei '{xml_filename}' wird in keiner anderen XSL-Datei verwendet.\n\n" "Möchten Sie auch die physische Datei aus dem xml-Ordner löschen?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, - QMessageBox.StandardButton.No + QMessageBox.StandardButton.No, ) - + if delete_reply == QMessageBox.StandardButton.Yes: try: xml_file_path.unlink() print(f"Physische XML-Datei gelöscht: {xml_file_path}") except Exception as e: - QMessageBox.warning( - self, - "Warnung", - f"Fehler beim Löschen der physischen Datei:\n{str(e)}" - ) + QMessageBox.warning(self, "Warnung", f"Fehler beim Löschen der physischen Datei:\n{str(e)}") else: - print(f"XML-Datei '{xml_filename}' wird noch in anderen XSL-Dateien verwendet - physische Datei nicht gelöscht") - + print( + f"XML-Datei '{xml_filename}' wird noch in anderen XSL-Dateien verwendet - physische Datei nicht gelöscht" + ) + # Speichere die aktualisierten Projekt-Einstellungen self._save_project_settings() - + # Aktualisiere das TreeWidget self._load_nodes_to_tree() - + print(f"XML-Datei '{xml_filename}' erfolgreich entfernt") - + except Exception as e: error_msg = f"Fehler beim Löschen der XML-Datei: {str(e)}" print(error_msg) QMessageBox.critical(self, "Fehler", error_msg) - + def _is_xml_file_used_elsewhere(self, xml_path, exclude_xsl_file): """ Prüft ob eine XML-Datei noch in anderen XSL-Dateien verwendet wird. - + Args: xml_path: Pfad zur XML-Datei (relativ) exclude_xsl_file: XSL-Datei die ausgeschlossen werden soll - + Returns: bool: True wenn die XML-Datei noch anderswo verwendet wird """ @@ -1611,21 +1768,21 @@ class MainWindow(QMainWindow): # Prüfe ob pdf_project und nodes existieren if not self.pdf_project or not self.pdf_project.nodes: return False # Keine Nodes vorhanden, also nicht verwendet - + return self._check_xml_usage_recursive(self.pdf_project.nodes, xml_path, exclude_xsl_file) except Exception as e: print(f"Fehler beim Prüfen der XML-Datei-Verwendung: {e}") return True # Im Zweifelsfall annehmen, dass sie verwendet wird - + def _check_xml_usage_recursive(self, nodes, xml_path, exclude_xsl_file): """ Prüft rekursiv ob eine XML-Datei in den Nodes verwendet wird. - + Args: nodes: Liste der zu prüfenden Nodes xml_path: Pfad zur XML-Datei (relativ) exclude_xsl_file: XSL-Datei die ausgeschlossen werden soll - + Returns: bool: True wenn die XML-Datei gefunden wird """ @@ -1639,7 +1796,7 @@ class MainWindow(QMainWindow): # Rekursiv in Knoten suchen if self._check_xml_usage_recursive(node.children, xml_path, exclude_xsl_file): return True - + return False # Kontextmenü-Aktionen für Root-Elemente (Unbekannter Typ) @@ -1654,54 +1811,54 @@ class MainWindow(QMainWindow): Führt SQL-Abfrage aus und aktualisiert die Projekt-Nodes. """ print("Button 'lade aus FN2' wurde geklickt!") - + try: # Prüfe ob ein Projekt geladen ist - if not hasattr(self, 'project') or not self.project: + if not hasattr(self, "project") or not self.project: QMessageBox.warning(self, "Warnung", "Kein Projekt geladen. Bitte öffnen Sie zuerst ein Projekt.") return - + # Hole das aktuelle Projekt aus app_settings if not self.project: QMessageBox.warning(self, "Warnung", "Aktuelles Projekt nicht in den Einstellungen gefunden.") return - + # Hole die PostgreSQL-Datenbank-Konfiguration db_config = self._get_database_config(self.project.postgre_sql_db_id) if not db_config: QMessageBox.warning(self, "Warnung", "PostgreSQL-Datenbank-Konfiguration nicht gefunden.") return - + # Führe SQL-Abfrage aus df = self._execute_sql_query(db_config) if df is None: return # Fehler bereits angezeigt - + # Verarbeite die Daten wie in readCsv.py new_nodes = self._process_sql_data(df) - + # Merge mit vorhandenen Nodes self._merge_nodes_with_existing(new_nodes) - + # Speichere die aktualisierten Projekt-Einstellungen self._save_project_settings() - + # Lade das Projekt neu self._load_nodes_to_tree() - + # QMessageBox.information(self, "Erfolg", "Daten erfolgreich aus FN2 geladen und Projekt aktualisiert!") - + except Exception as e: print(f"Fehler beim Laden aus FN2: {e}") QMessageBox.critical(self, "Fehler", f"Fehler beim Laden aus FN2:\n{str(e)}") - + def _get_database_config(self, db_id): """ Holt die Datenbank-Konfiguration anhand der ID. - + Args: db_id: ID der PostgreSQL-Datenbank - + Returns: PostgreSqlDb|None: Die Datenbank-Konfiguration oder None """ @@ -1709,14 +1866,14 @@ class MainWindow(QMainWindow): if db.id == db_id: return db return None - + def _execute_sql_query(self, db_config): """ Führt die SQL-Abfrage aus der data.sql Datei aus. - + Args: db_config: PostgreSQL-Datenbank-Konfiguration - + Returns: pl.DataFrame|None: Die Abfrageergebnisse oder None bei Fehler """ @@ -1726,14 +1883,15 @@ class MainWindow(QMainWindow): if not sql_file_path.exists(): QMessageBox.critical(self, "Fehler", f"SQL-Datei nicht gefunden: {sql_file_path}") return None - - with open(sql_file_path, 'r', encoding='utf-8') as f: + + with open(sql_file_path, "r", encoding="utf-8") as f: sql_query = f.read() - + print(f"SQL-Abfrage geladen: {len(sql_query)} Zeichen") - + # Verbindung zur PostgreSQL-Datenbank herstellen - connection_string = ("postgresql://" + connection_string = ( + "postgresql://" f"{db_config.username}:" f"{db_config.password}@" f"{db_config.host}:" @@ -1741,42 +1899,44 @@ class MainWindow(QMainWindow): f"{db_config.database}?" f"sslmode={db_config.ssl_mode.value}" ) - + print(f"Verbinde zu PostgreSQL: {db_config.host}:{db_config.port}/{db_config.database}") - - df = pl.read_database_uri(sql_query, connection_string, engine='connectorx').sort(["reporttyp_bez", "report_bez", "repfile_bez"]) + + df = pl.read_database_uri(sql_query, connection_string, engine="connectorx").sort( + ["reporttyp_bez", "report_bez", "repfile_bez"] + ) return df except Exception as e: error_msg = f"Fehler beim Ausführen der SQL-Abfrage: {str(e)}" print(error_msg) QMessageBox.critical(self, "Fehler", error_msg) return None - + def _process_sql_data(self, df): """ Verarbeitet die SQL-Daten wie in readCsv.py und erstellt Node-Struktur. - + Args: df: Polars DataFrame mit den SQL-Ergebnissen - + Returns: list[TreeNode]: Liste der erstellten Root-Nodes """ try: start_time = time.time() - + # Gruppiere die Daten wie in readCsv.py ebene_1 = df.group_by(["reporttyp", "reporttyp_bez"]).len() ebene_2 = df.group_by(["reporttyp", "report", "report_bez"]).len() ebene_3 = df.group_by(["reporttyp", "report", "repfile", "repfile_bez", "xsl_datei"]).len() - + group_time = time.time() - start_time print(f"Performance: Gruppierung in {group_time:.3f}s") - + new_nodes = [] - + start_time = time.time() - + # Erstelle Node-Struktur wie in readCsv.py for r1 in ebene_1.rows(named=True): tn_1 = TreeNode(id=(r1["reporttyp"],), bez=r1["reporttyp_bez"], children=[]) @@ -1784,51 +1944,53 @@ class MainWindow(QMainWindow): for r2 in r1_children.rows(named=True): tn_2 = TreeNode(id=(r2["reporttyp"], r2["report"]), bez=r2["report_bez"], children=[]) - r2_children = ebene_3.filter((pl.col("reporttyp") == r1["reporttyp"]) & (pl.col("report") == r2["report"])) + r2_children = ebene_3.filter( + (pl.col("reporttyp") == r1["reporttyp"]) & (pl.col("report") == r2["report"]) + ) for r3 in r2_children.rows(named=True): x = XslFile( id=(r3["reporttyp"], r3["report"], r3["repfile"]), bez=r3["repfile_bez"], xsl_file=Path(r3["xsl_datei"]), - xmls=[] + xmls=[], ) tn_2.children.append(x) tn_1.children.append(tn_2) new_nodes.append(tn_1) - + nodes_time = time.time() - start_time print(f"Performance: Node-Erstellung in {nodes_time:.3f}s") print(f"Erstellt: {len(new_nodes)} Root-Nodes") - + return new_nodes - + except Exception as e: print(f"Fehler beim Verarbeiten der SQL-Daten: {e}") raise - + def _merge_nodes_with_existing(self, new_nodes): """ Merged neue Nodes mit vorhandenen Nodes basierend auf IDs. Überschreibt nur einzelne Eigenschaften, nicht ganze Nodes. - + Args: new_nodes: Liste der neuen Nodes """ try: print("Merge neue Nodes mit vorhandenen...") - + # Erstelle ein Dictionary der neuen Nodes für schnellen Zugriff new_nodes_dict = {} self._build_nodes_dict(new_nodes, new_nodes_dict) - + print(f"Neue Nodes Dictionary erstellt: {len(new_nodes_dict)} Einträge") - + # Merge mit vorhandenen Nodes if self.pdf_project and self.pdf_project.nodes: self._merge_nodes_recursive(self.pdf_project.nodes, new_nodes_dict) - + # Füge komplett neue Root-Nodes hinzu if self.pdf_project and self.pdf_project.nodes: existing_root_ids = {node.id for node in self.pdf_project.nodes} @@ -1840,31 +2002,31 @@ class MainWindow(QMainWindow): # Wenn keine Nodes vorhanden sind, füge alle neuen Nodes hinzu self.pdf_project.nodes = new_nodes print(f"Alle {len(new_nodes)} Root-Nodes hinzugefügt (keine vorhandenen Nodes)") - + print("Merge abgeschlossen") - + except Exception as e: print(f"Fehler beim Mergen der Nodes: {e}") raise - + def _build_nodes_dict(self, nodes, nodes_dict): """ Erstellt rekursiv ein Dictionary aller Nodes für schnellen ID-basierten Zugriff. - + Args: nodes: Liste der Nodes nodes_dict: Dictionary zum Füllen """ for node in nodes: nodes_dict[node.id] = node - + if isinstance(node, TreeNode) and node.children: self._build_nodes_dict(node.children, nodes_dict) - + def _merge_nodes_recursive(self, existing_nodes, new_nodes_dict): """ Merged rekursiv vorhandene Nodes mit neuen Nodes. - + Args: existing_nodes: Liste der vorhandenen Nodes new_nodes_dict: Dictionary der neuen Nodes @@ -1872,22 +2034,26 @@ class MainWindow(QMainWindow): for existing_node in existing_nodes: if existing_node.id in new_nodes_dict: new_node = new_nodes_dict[existing_node.id] - + # Aktualisiere nur die Bezeichnung, falls sie sich geändert hat if existing_node.bez != new_node.bez: - print(f"Aktualisiere Bezeichnung für Node {existing_node.id}: '{existing_node.bez}' -> '{new_node.bez}'") + print( + f"Aktualisiere Bezeichnung für Node {existing_node.id}: '{existing_node.bez}' -> '{new_node.bez}'" + ) existing_node.bez = new_node.bez - + # Für XslFile: Aktualisiere xsl_file Pfad if isinstance(existing_node, XslFile) and isinstance(new_node, XslFile): if existing_node.xsl_file != new_node.xsl_file: - print(f"Aktualisiere XSL-Datei für Node {existing_node.id}: '{existing_node.xsl_file}' -> '{new_node.xsl_file}'") + print( + f"Aktualisiere XSL-Datei für Node {existing_node.id}: '{existing_node.xsl_file}' -> '{new_node.xsl_file}'" + ) existing_node.xsl_file = new_node.xsl_file - + # Rekursiv für Knoten (nur bei TreeNode) if isinstance(existing_node, TreeNode) and existing_node.children: self._merge_nodes_recursive(existing_node.children, new_nodes_dict) - + # Füge neue Knoten hinzu, die noch nicht existieren if existing_node.id in new_nodes_dict: new_node = new_nodes_dict[existing_node.id] @@ -1897,40 +2063,40 @@ class MainWindow(QMainWindow): if new_child.id not in existing_child_ids: existing_node.children.append(new_child) print(f"Neues Kind hinzugefügt zu Node {existing_node.id}: {new_child.bez}") - + def _collect_parent_params(self, item): """ Sammelt die XSLT-Parameter aller Eltern-Nodes. - + Args: item: Das TreeWidgetItem - + Returns: dict: Dictionary mit allen Eltern-Parametern """ parent_params = {} - + try: # Gehe die Hierarchie nach oben durch current_item = item.parent() - + while current_item: # Hole das Node-Objekt parent_node = current_item.data(0, Qt.ItemDataRole.UserRole) - - if parent_node and hasattr(parent_node, 'xslt_params') and parent_node.xslt_params: + + if parent_node and hasattr(parent_node, "xslt_params") and parent_node.xslt_params: # Füge die Parameter des Eltern-Nodes hinzu # Eltern-Parameter haben niedrigere Priorität (werden überschrieben) for key, value in parent_node.xslt_params.items(): if key not in parent_params: # Nur hinzufügen wenn noch nicht vorhanden parent_params[key] = value - + # Gehe zum nächsten Eltern-Element current_item = current_item.parent() - + print(f"Gesammelte Eltern-Parameter: {parent_params}") return parent_params - + except Exception as e: print(f"Fehler beim Sammeln der Eltern-Parameter: {e}") return {} @@ -1938,7 +2104,7 @@ class MainWindow(QMainWindow): def _save_project_settings(self): """ Speichert die aktualisierten Projekt-Einstellungen. - + Args: current_project: Das aktuelle Projekt """ @@ -1947,19 +2113,19 @@ class MainWindow(QMainWindow): if not self.pdf_project: print("Keine Projekt-Einstellungen zum Speichern verfügbar") return - + if not self.project or not self.project.project_dir: print("Kein Projekt-Verzeichnis zum Speichern verfügbar") return - + start_time = time.time() - + # Speichere in project.yaml im Projekt-Verzeichnis self.pdf_project.writeSettings(project_dir=self.project.project_dir) - + dump_time = time.time() - start_time print(f"Performance: Projekt-Einstellungen gespeichert in {dump_time:.3f}s") - + except Exception as e: print(f"Fehler beim Speichern der Projekt-Einstellungen: {e}") raise @@ -1970,21 +2136,21 @@ class MainWindow(QMainWindow): # 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 - + print("Drag&Drop für TreeWidget aktiviert") - + except Exception as e: print(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 """ @@ -1992,11 +2158,10 @@ class MainWindow(QMainWindow): # 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')] - + xml_files = [url.toLocalFile() for url in urls if url.toLocalFile().lower().endswith(".xml")] + if xml_files: event.acceptProposedAction() print(f"Drag-Enter akzeptiert: {len(xml_files)} XML-Dateien") @@ -2006,7 +2171,7 @@ class MainWindow(QMainWindow): else: event.ignore() print("Drag-Enter ignoriert: Keine URLs") - + except Exception as e: print(f"Fehler in tree_drag_enter_event: {e}") event.ignore() @@ -2014,7 +2179,7 @@ class MainWindow(QMainWindow): 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 """ @@ -2022,18 +2187,17 @@ class MainWindow(QMainWindow): # 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')] - + 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: print(f"Fehler in tree_drag_move_event: {e}") event.ignore() @@ -2041,49 +2205,49 @@ class MainWindow(QMainWindow): 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: + 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: + + 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'): + 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 - + print(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) - + event.acceptProposedAction() - + except Exception as e: error_msg = f"Fehler beim Verarbeiten des Drop-Events: {str(e)}" print(error_msg) @@ -2093,34 +2257,32 @@ class MainWindow(QMainWindow): def _handle_xml_file_drop(self, xml_file_path: Path): """ Verarbeitet eine einzelne XML-Datei, die per Drag&Drop hinzugefügt wurde. - + Args: xml_file_path: Pfad zur XML-Datei """ try: print(f"Verarbeite XML-Datei: {xml_file_path}") - + # Prüfe ob die Datei existiert if not xml_file_path.exists(): QMessageBox.critical(self, "Fehler", f"Die XML-Datei existiert nicht:\n{xml_file_path}") return - + # 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 - + # Öffne den Dialog zur Zuordnung zu XSL-Knoten dialog = XmlToXslAssignDialog( - parent=self, - xml_file_path=xml_file_path, - project_nodes=self.pdf_project.nodes + 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 self._assign_xml_to_xsl_nodes(xml_file_path, selected_xsl_nodes) @@ -2128,7 +2290,7 @@ class MainWindow(QMainWindow): print("Keine XSL-Knoten ausgewählt") else: print("Dialog abgebrochen") - + except Exception as e: error_msg = f"Fehler beim Verarbeiten der XML-Datei '{xml_file_path}': {str(e)}" print(error_msg) @@ -2138,22 +2300,22 @@ class MainWindow(QMainWindow): """ Ordnet eine XML-Datei den ausgewählten XSL-Knoten zu. Implementiert Hash-basierte Duplikatserkennung und intelligente Dateinamen-Verwaltung. - + Args: xml_file_path: Pfad zur XML-Datei selected_xsl_nodes: Liste der ausgewählten XSL-Knoten """ try: logger.info(f"Ordne XML-Datei '{xml_file_path.name}' zu {len(selected_xsl_nodes)} XSL-Knoten zu") - + # 1. Hash für die neue XML-Datei berechnen file_hash = self._calculate_hash_for_file(xml_file_path) if not file_hash: logger.warning(f"Hash-Berechnung für {xml_file_path} fehlgeschlagen") - + # 2. Prüfe ob eine XML-Datei mit gleichem Hash bereits im Projekt existiert existing_xml = self._find_xml_file_by_hash(file_hash) if file_hash else None - + 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}") @@ -2162,7 +2324,7 @@ class MainWindow(QMainWindow): # 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) - + except Exception as e: error_msg = f"Fehler beim Zuordnen der XML-Datei: {str(e)}" logger.error(error_msg) @@ -2173,70 +2335,69 @@ class MainWindow(QMainWindow): Startet die asynchrone Hash-Berechnung für alle XML-Dateien im Projekt. """ try: - if not hasattr(self, 'pdf_project') or not self.pdf_project: + if not hasattr(self, "pdf_project") or not self.pdf_project: logger.debug("Keine Projekt-Einstellungen verfügbar für Hash-Berechnung") return - + # Sammle alle XML-Dateien aus dem Projekt xml_files = self._collect_all_xml_files() - + if not xml_files: logger.debug("Keine XML-Dateien für Hash-Berechnung gefunden") return - + logger.info(f"Starte Hash-Berechnung für {len(xml_files)} XML-Dateien") - + # Prüfe ob Projekt verfügbar ist if not self.project or not self.project.project_dir: logger.warning("Kein Projekt-Verzeichnis für Hash-Berechnung verfügbar") return - + # Stoppe vorherigen Thread falls noch aktiv if self.hash_calculator_thread and self.hash_calculator_thread.isRunning(): self.hash_calculator_thread.quit() self.hash_calculator_thread.wait() - + # Erstelle und starte neuen Hash-Berechnungs-Thread self.hash_calculator_thread = XmlHashCalculatorThread( - project_dir=Path(self.project.project_dir), - xml_files=xml_files + project_dir=Path(self.project.project_dir), xml_files=xml_files ) - + # Verbinde Signale self.hash_calculator_thread.hash_calculated.connect(self._on_hash_calculated) self.hash_calculator_thread.calculation_finished.connect(self._on_hash_calculation_finished) self.hash_calculator_thread.error_occurred.connect(self._on_hash_calculation_error) - + # Starte Thread self.hash_calculator_thread.start() - + except Exception as e: logger.error(f"Fehler beim Starten der Hash-Berechnung: {e}") - + def _collect_all_xml_files(self) -> List[XmlFile]: """ Sammelt alle XmlFile-Objekte aus der Projektstruktur. - + Returns: List[XmlFile]: Liste aller gefundenen XML-Dateien """ xml_files = [] - + try: if self.pdf_project and self.pdf_project.nodes: self._collect_xml_files_recursive(self.pdf_project.nodes, xml_files) - + logger.debug(f"Gesammelt: {len(xml_files)} XML-Dateien") return xml_files - + except Exception as e: logger.error(f"Fehler beim Sammeln der XML-Dateien: {e}") return [] - + def _collect_xml_files_recursive(self, nodes, xml_files: List[XmlFile]): """ Sammelt rekursiv alle XML-Dateien aus den Nodes. - + Args: nodes: Liste der zu durchsuchenden Nodes xml_files: Liste zum Sammeln der XML-Dateien @@ -2250,11 +2411,11 @@ class MainWindow(QMainWindow): elif isinstance(node, TreeNode) and node.children: # Rekursiv in Kinder-Nodes suchen self._collect_xml_files_recursive(node.children, xml_files) - + def _on_hash_calculated(self, xml_file: XmlFile, hash_value: str): """ Wird aufgerufen, wenn ein Hash-Wert berechnet wurde. - + Args: xml_file: Das XmlFile-Objekt hash_value: Der berechnete Hash-Wert mit Präfix @@ -2263,44 +2424,44 @@ class MainWindow(QMainWindow): # Setze den Hash-Wert xml_file.hashsum = hash_value logger.debug(f"Hash gesetzt für {xml_file.xml}: {hash_value}") - + except Exception as e: logger.error(f"Fehler beim Setzen des Hash-Werts: {e}") - + def _on_hash_calculation_finished(self, processed_count: int, total_count: int): """ Wird aufgerufen, wenn die Hash-Berechnung abgeschlossen ist. - + Args: processed_count: Anzahl der verarbeiteten Dateien total_count: Gesamtanzahl der Dateien """ try: logger.info(f"Hash-Berechnung abgeschlossen: {processed_count}/{total_count} Dateien verarbeitet") - + # Speichere die aktualisierten Projekt-Einstellungen if processed_count > 0: self._save_project_settings() logger.info("Projekt-Einstellungen mit neuen Hash-Werten gespeichert") - + except Exception as e: logger.error(f"Fehler beim Abschließen der Hash-Berechnung: {e}") - + def _on_hash_calculation_error(self, xml_file_path: str, error_message: str): """ Wird aufgerufen, wenn ein Fehler bei der Hash-Berechnung auftritt. - + Args: xml_file_path: Pfad zur XML-Datei error_message: Fehlermeldung """ logger.warning(f"Hash-Berechnungsfehler für {xml_file_path}: {error_message}") - + def _calculate_hash_for_xml_file(self, xml_file: XmlFile): """ Berechnet synchron den Hash für eine einzelne XML-Datei. Wird verwendet beim Hinzufügen neuer XML-Dateien. - + Args: xml_file: Das XmlFile-Objekt """ @@ -2308,55 +2469,55 @@ class MainWindow(QMainWindow): if xml_file.hashsum: logger.debug(f"Hash bereits vorhanden für {xml_file.xml}: {xml_file.hashsum}") return - + # Prüfe ob Projekt verfügbar ist if not self.project or not self.project.project_dir: logger.warning("Kein Projekt-Verzeichnis für Hash-Berechnung verfügbar") return - + xml_file_path = Path(self.project.project_dir) / xml_file.xml - + if not xml_file_path.exists(): logger.warning(f"XML-Datei nicht gefunden: {xml_file_path}") return - + # Datei binär lesen und Hash berechnen - with open(xml_file_path, 'rb') as f: + with open(xml_file_path, "rb") as f: file_content = f.read() hash_obj = hashlib.blake2b(file_content) hash_hex = hash_obj.hexdigest() - + # Hash mit Präfix setzen xml_file.hashsum = f"blake2b:{hash_hex}" logger.debug(f"Hash berechnet für {xml_file.xml}: {xml_file.hashsum}") - + except Exception as e: logger.error(f"Fehler beim Berechnen des Hash für {xml_file.xml}: {e}") def _get_all_project_xml_files(self) -> List[XmlFile]: """ Sammelt alle XmlFile-Objekte aus dem gesamten Projekt für Hash-Vergleiche. - + Returns: List[XmlFile]: Liste aller XML-Dateien im Projekt """ xml_files = [] - + try: if self.pdf_project and self.pdf_project.nodes: self._collect_xml_files_for_hash_comparison(self.pdf_project.nodes, xml_files) - + logger.debug(f"Hash-Vergleich: {len(xml_files)} XML-Dateien im Projekt gefunden") return xml_files - + except Exception as e: logger.error(f"Fehler beim Sammeln der XML-Dateien für Hash-Vergleich: {e}") return [] - + def _collect_xml_files_for_hash_comparison(self, nodes, xml_files: List[XmlFile]): """ Sammelt rekursiv alle XML-Dateien aus den Nodes für Hash-Vergleiche. - + Args: nodes: Liste der zu durchsuchenden Nodes xml_files: Liste zum Sammeln der XML-Dateien @@ -2371,35 +2532,35 @@ class MainWindow(QMainWindow): elif isinstance(node, TreeNode) and node.children: # Rekursiv in Kinder-Nodes suchen self._collect_xml_files_for_hash_comparison(node.children, xml_files) - + def _find_xml_file_by_hash(self, target_hash: str) -> XmlFile | None: """ Sucht eine XML-Datei mit dem angegebenen Hash im gesamten Projekt. - + Args: target_hash: Der zu suchende Hash-Wert (mit blake2b: Präfix) - + Returns: XmlFile|None: Die gefundene XML-Datei oder None """ try: if not target_hash: return None - + all_xml_files = self._get_all_project_xml_files() - + for xml_file in all_xml_files: if xml_file.hashsum == target_hash: logger.debug(f"Hash-Match gefunden: {xml_file.xml} hat Hash {target_hash}") return xml_file - + logger.debug(f"Kein Hash-Match für {target_hash} gefunden") return None - + except Exception as e: logger.error(f"Fehler bei Hash-Suche für {target_hash}: {e}") return None - + def _generate_alternative_filename(self, original_path: Path, xml_dir: Path) -> Path: """ Generiert alternative Dateinamen im Format: datei_1.xml, datei_2.xml, ... @@ -2441,37 +2602,37 @@ class MainWindow(QMainWindow): timestamp = int(time.time()) fallback_name = f"{original_path.stem}_{timestamp}{original_path.suffix}" return xml_dir / fallback_name - + def _is_filename_used_in_project(self, relative_xml_path: Path) -> bool: """ Prüft ob ein relativer XML-Dateipfad bereits im Projekt verwendet wird. - + Args: relative_xml_path: Relativer Pfad zur XML-Datei (z.B. xml/datei_1.xml) - + Returns: bool: True wenn der Dateiname bereits verwendet wird """ try: all_xml_files = self._get_all_project_xml_files() - + for xml_file in all_xml_files: if xml_file.xml == relative_xml_path: return True - + return False - + except Exception as e: logger.error(f"Fehler beim Prüfen der Dateiname-Verwendung für {relative_xml_path}: {e}") return True # Im Zweifelsfall annehmen, dass der Name verwendet wird - + def _calculate_hash_for_file(self, file_path: Path) -> str | None: """ Berechnet synchron den blake2b-Hash für eine Datei. - + Args: file_path: Pfad zur Datei - + Returns: str|None: Hash-Wert mit blake2b: Präfix oder None bei Fehler """ @@ -2479,37 +2640,37 @@ class MainWindow(QMainWindow): if not file_path.exists(): logger.warning(f"Datei für Hash-Berechnung nicht gefunden: {file_path}") return None - + # Datei binär lesen und Hash berechnen - with open(file_path, 'rb') as f: + with open(file_path, "rb") as f: file_content = f.read() hash_obj = hashlib.blake2b(file_content) hash_hex = hash_obj.hexdigest() - + # Hash mit Präfix zurückgeben hash_value = f"blake2b:{hash_hex}" logger.debug(f"Hash berechnet für {file_path}: {hash_value}") return hash_value - + except Exception as e: logger.error(f"Fehler beim Berechnen des Hash für {file_path}: {e}") return None - + def _assign_existing_xml_to_nodes(self, existing_xml: XmlFile, selected_xsl_nodes: list): """ Ordnet eine bereits vorhandene XML-Datei (basierend auf Hash-Match) den XSL-Knoten zu. - + Args: existing_xml: Die bereits vorhandene XML-Datei selected_xsl_nodes: Liste der ausgewählten XSL-Knoten """ try: added_count = 0 - + for xsl_node in selected_xsl_nodes: # Prüfe ob diese XML-Datei bereits in der XslFile-Node vorhanden ist already_assigned = any(xml_file.xml == existing_xml.xml for xml_file in xsl_node.xmls) - + if not already_assigned: # Erstelle neue XmlFile-Referenz mit gleichem Pfad und Hash new_xml_ref = XmlFile(xml=existing_xml.xml, hashsum=existing_xml.hashsum) @@ -2518,37 +2679,37 @@ class MainWindow(QMainWindow): logger.info(f"Vorhandene XML-Datei '{existing_xml.xml}' zu XSL-Node '{xsl_node.bez}' zugeordnet") else: logger.debug(f"XML-Datei '{existing_xml.xml}' bereits in XSL-Node '{xsl_node.bez}' vorhanden") - + if added_count > 0: # Speichere die aktualisierten Projekt-Einstellungen self._save_project_settings() - + # 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." + f"Die vorhandene Datei '{existing_xml.xml.name}' wurde automatisch zu {added_count} XSL-Knoten zugeordnet.", ) else: QMessageBox.information( self, "Bereits zugeordnet", - "Die XML-Datei mit gleichem Inhalt ist bereits in allen ausgewählten XSL-Knoten vorhanden." + "Die XML-Datei mit gleichem Inhalt ist bereits in allen ausgewählten XSL-Knoten vorhanden.", ) - + 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) - + def _process_new_xml_file(self, xml_file_path: Path, selected_xsl_nodes: list, file_hash: str | None): """ Verarbeitet eine neue XML-Datei (kein Hash-Match gefunden). - + Args: xml_file_path: Pfad zur neuen XML-Datei selected_xsl_nodes: Liste der ausgewählten XSL-Knoten @@ -2560,14 +2721,14 @@ class MainWindow(QMainWindow): logger.error("Kein Projekt-Verzeichnis für neue XML-Datei verfügbar") QMessageBox.critical(self, "Fehler", "Kein Projekt-Verzeichnis verfügbar.") return - + # Erstelle xml-Ordner im Projekt-Verzeichnis falls er nicht existiert xml_dir = Path(self.project.project_dir) / "xml" xml_dir.mkdir(parents=True, exist_ok=True) - + # Bestimme den Ziel-Pfad in xml-Ordner target_xml_path = xml_dir / xml_file_path.name - + # Prüfe ob eine Datei mit gleichem Namen bereits existiert if target_xml_path.exists() or self._is_filename_used_in_project(Path("xml") / xml_file_path.name): # Generiere alternative Dateinamen @@ -2576,23 +2737,23 @@ class MainWindow(QMainWindow): alt_path = self._generate_alternative_filename(xml_file_path, xml_dir) if alt_path not in alternative_paths: alternative_paths.append(alt_path) - + # Zeige Dialog zur Auswahl des Dateinamens selected_path = self._show_filename_selection_dialog(xml_file_path.name, alternative_paths) - + if not selected_path: # Benutzer hat abgebrochen return - + target_xml_path = selected_path - + # Kopiere die XML-Datei in den xml-Ordner shutil.copy2(xml_file_path, target_xml_path) logger.info(f"XML-Datei kopiert: {xml_file_path} -> {target_xml_path}") - + # Erstelle relatives Path zur XML-Datei (relativ zum Projekt-Verzeichnis) relative_xml_path = Path("xml") / target_xml_path.name - + # Füge die XML-Datei zu allen ausgewählten XSL-Knoten hinzu added_count = 0 for xsl_node in selected_xsl_nodes: @@ -2602,7 +2763,7 @@ class MainWindow(QMainWindow): if xml_file.xml == relative_xml_path: existing_xml = xml_file break - + if not existing_xml: # Erstelle neues XmlFile-Objekt mit Hash new_xml_file = XmlFile(xml=relative_xml_path, hashsum=file_hash) @@ -2611,62 +2772,76 @@ class MainWindow(QMainWindow): logger.info(f"XML-Datei '{target_xml_path.name}' zu XSL-Node '{xsl_node.bez}' hinzugefügt") else: logger.debug(f"XML-Datei '{target_xml_path.name}' bereits in XSL-Node '{xsl_node.bez}' vorhanden") - + if added_count > 0: # Speichere die aktualisierten Projekt-Einstellungen self._save_project_settings() - + # 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." + 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}'." - + success_msg += ( + f"\n\nDie Datei wurde umbenannt von '{xml_file_path.name}' zu '{target_xml_path.name}'." + ) + QMessageBox.information(self, "Erfolg", success_msg) else: QMessageBox.information( self, "Information", - f"XML-Datei '{target_xml_path.name}' war bereits in allen ausgewählten XSL-Knoten vorhanden." + f"XML-Datei '{target_xml_path.name}' war bereits in allen ausgewählten XSL-Knoten vorhanden.", ) - + 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) - + def _show_filename_selection_dialog(self, original_name: str, alternative_paths: List[Path]) -> Path | None: """ Zeigt einen Dialog zur Auswahl eines alternativen Dateinamens. - + Args: original_name: Ursprünglicher Dateiname alternative_paths: Liste alternativer Pfade - + Returns: Path|None: Ausgewählter Pfad oder None bei Abbruch """ try: - from PySide6.QtWidgets import QDialog, QVBoxLayout, QLabel, QRadioButton, QButtonGroup, QPushButton, QHBoxLayout - + from PySide6.QtWidgets import ( + QDialog, + QVBoxLayout, + QLabel, + QRadioButton, + QButtonGroup, + QPushButton, + QHBoxLayout, + ) + dialog = QDialog(self) dialog.setWindowTitle("Dateiname auswählen") dialog.setModal(True) dialog.resize(400, 300) - + layout = QVBoxLayout(dialog) - + # Erklärungstext - info_label = QLabel(f"Eine Datei mit dem Namen '{original_name}' existiert bereits.\n\n" - "Bitte wählen Sie einen alternativen Dateinamen:") + info_label = QLabel( + f"Eine Datei mit dem Namen '{original_name}' existiert bereits.\n\n" + "Bitte wählen Sie einen alternativen Dateinamen:" + ) layout.addWidget(info_label) - + # Radio-Buttons für alternative Namen button_group = QButtonGroup(dialog) radio_buttons = [] - + for i, alt_path in enumerate(alternative_paths): radio_button = QRadioButton(alt_path.name) if i == 0: # Ersten als Standard auswählen @@ -2674,28 +2849,28 @@ class MainWindow(QMainWindow): button_group.addButton(radio_button, i) radio_buttons.append(radio_button) layout.addWidget(radio_button) - + # Buttons button_layout = QHBoxLayout() ok_button = QPushButton("OK") cancel_button = QPushButton("Abbrechen") - + button_layout.addWidget(ok_button) button_layout.addWidget(cancel_button) layout.addLayout(button_layout) - + # Event-Handler ok_button.clicked.connect(dialog.accept) cancel_button.clicked.connect(dialog.reject) - + # Dialog anzeigen if dialog.exec() == QDialog.DialogCode.Accepted: selected_id = button_group.checkedId() if 0 <= selected_id < len(alternative_paths): return alternative_paths[selected_id] - + return None - + except Exception as e: logger.error(f"Fehler beim Anzeigen des Dateiname-Auswahl-Dialogs: {e}") # Fallback: Ersten alternativen Namen verwenden @@ -2807,16 +2982,19 @@ class MainWindow(QMainWindow): # 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") + 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)}" + self, "Fehlende Konfiguration", f"Folgende Konfigurationen fehlen: {', '.join(missing)}" ) return None @@ -2839,7 +3017,7 @@ class MainWindow(QMainWindow): 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 + xsl_id=xsl_file_obj.id, ) return job @@ -2882,16 +3060,37 @@ class MainWindow(QMainWindow): logger.error(f"Fehler beim Starten der Transformation: {e}") QMessageBox.critical(self, "Fehler", f"Fehler beim Starten: {str(e)}") - def _on_transformation_job_started(self, xml_file_name: str): + 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}") + 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 + sample_keys = 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: + # 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") + 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. @@ -2921,22 +3120,49 @@ class MainWindow(QMainWindow): 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}" + self, "Transformation fehlgeschlagen", f"XML-Datei: {xml_file}\n\nFehler:\n{error_text}" ) - def _on_transformation_job_error(self, xml_file_name: str, error_message: str): + # 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) + 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 """ - logger.error(f"Transformation-Fehler bei {xml_file_name}: {error_message}") + 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): """ Signal-Handler: Alle Jobs wurden abgeschlossen. @@ -2950,32 +3176,36 @@ class MainWindow(QMainWindow): if successful_count == total_count: self.statusBar().showMessage(f"✓ Alle {total_count} Transformationen erfolgreich", 5000) QMessageBox.information( - self, - "Abgeschlossen", - f"Alle {total_count} Transformationen wurden erfolgreich abgeschlossen" + self, "Abgeschlossen", f"Alle {total_count} Transformationen wurden erfolgreich abgeschlossen" ) else: failed_count = total_count - successful_count self.statusBar().showMessage( - f"⚠ {successful_count}/{total_count} erfolgreich, {failed_count} fehlgeschlagen", - 5000 + f"⚠ {successful_count}/{total_count} erfolgreich, {failed_count} fehlgeschlagen", 5000 ) QMessageBox.warning( self, "Abgeschlossen mit Fehlern", - f"{successful_count} von {total_count} Transformationen erfolgreich\n" - f"{failed_count} fehlgeschlagen" + f"{successful_count} von {total_count} Transformationen erfolgreich\n{failed_count} fehlgeschlagen", ) def closeEvent(self, event): """Wird beim Schließen der Anwendung aufgerufen.""" # Stoppe Hash-Berechnungs-Thread falls noch aktiv - if hasattr(self, 'hash_calculator_thread') and self.hash_calculator_thread and self.hash_calculator_thread.isRunning(): + if ( + hasattr(self, "hash_calculator_thread") + and self.hash_calculator_thread + and self.hash_calculator_thread.isRunning() + ): self.hash_calculator_thread.quit() self.hash_calculator_thread.wait() # Stoppe Transformations-Thread falls noch aktiv - if hasattr(self, 'transformation_thread') and self.transformation_thread and self.transformation_thread.isRunning(): + if ( + hasattr(self, "transformation_thread") + and self.transformation_thread + and self.transformation_thread.isRunning() + ): self.transformation_thread.quit() self.transformation_thread.wait()