Progress Bar und Diff-PDF-Icon im TreeWidget implementiert

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 <noreply@anthropic.com>
This commit is contained in:
2025-12-13 21:06:40 +01:00
parent b961fe1e1a
commit 629485f5e4
4 changed files with 1084 additions and 838 deletions
+19 -9
View File
@@ -34,7 +34,7 @@ class TransformationJob:
apache_fop_dir: Path, apache_fop_dir: Path,
diff_pdf_path: Path, diff_pdf_path: Path,
diff_pdf_params: list[str], diff_pdf_params: list[str],
xsl_id: tuple | None = None xsl_id: tuple | None = None,
): ):
""" """
Initialisiert einen Transformations-Job. Initialisiert einen Transformations-Job.
@@ -92,6 +92,7 @@ class TransformationJob:
# Apache FOP Binaries (plattformabhängig) # Apache FOP Binaries (plattformabhängig)
import sys import sys
if sys.platform == "win32": if sys.platform == "win32":
self.fop_cmd = self.apache_fop_dir / "fop.cmd" self.fop_cmd = self.apache_fop_dir / "fop.cmd"
else: else:
@@ -158,11 +159,13 @@ class TransformationJob:
# Sammle alle JAR-Dateien im Saxon-Verzeichnis für den Classpath # Sammle alle JAR-Dateien im Saxon-Verzeichnis für den Classpath
import glob import glob
saxon_dir = self.saxon_jar_path.parent saxon_dir = self.saxon_jar_path.parent
all_jars = glob.glob(str(saxon_dir / "*.jar")) all_jars = glob.glob(str(saxon_dir / "*.jar"))
# Verwende alle JARs im Classpath (getrennt durch : auf Linux/Mac, ; auf Windows) # Verwende alle JARs im Classpath (getrennt durch : auf Linux/Mac, ; auf Windows)
import sys import sys
classpath_separator = ";" if sys.platform == "win32" else ":" classpath_separator = ";" if sys.platform == "win32" else ":"
classpath = classpath_separator.join(all_jars) classpath = classpath_separator.join(all_jars)
@@ -187,14 +190,16 @@ class TransformationJob:
cmd_line, cmd_line,
capture_output=True, capture_output=True,
text=True, text=True,
timeout=120 # 2 Minuten Timeout timeout=120, # 2 Minuten Timeout
) )
if result.returncode == 0: if result.returncode == 0:
logger.info(f"Saxon-Transformation erfolgreich: {self.xml_file.name}") logger.info(f"Saxon-Transformation erfolgreich: {self.xml_file.name}")
return True, "Erfolgreich" return True, "Erfolgreich"
else: 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) logger.error(error_msg)
return False, error_msg return False, error_msg
@@ -230,10 +235,13 @@ class TransformationJob:
# Apache FOP Kommandozeile # Apache FOP Kommandozeile
cmd_line = [ cmd_line = [
str(self.fop_cmd), 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", "-r",
"-fo", str(self.temp_fo), "-fo",
"-pdf", str(self.new_pdf), str(self.temp_fo),
"-pdf",
str(self.new_pdf),
] ]
# Entferne leere Config-Parameter wenn fop.xconf nicht existiert # Entferne leere Config-Parameter wenn fop.xconf nicht existiert
@@ -248,7 +256,7 @@ class TransformationJob:
cmd_line, cmd_line,
capture_output=True, capture_output=True,
text=True, text=True,
timeout=180 # 3 Minuten Timeout timeout=180, # 3 Minuten Timeout
) )
# Temporäre FO-Datei löschen # Temporäre FO-Datei löschen
@@ -264,6 +272,7 @@ class TransformationJob:
if not self.ref_pdf.exists(): if not self.ref_pdf.exists():
try: try:
import shutil import shutil
shutil.copy2(self.new_pdf, self.ref_pdf) shutil.copy2(self.new_pdf, self.ref_pdf)
logger.info(f"Ref-PDF erstellt: {self.ref_pdf}") logger.info(f"Ref-PDF erstellt: {self.ref_pdf}")
except Exception as e: except Exception as e:
@@ -320,7 +329,7 @@ class TransformationJob:
cmd_compare, cmd_compare,
capture_output=True, capture_output=True,
text=True, text=True,
timeout=60 # 1 Minute Timeout timeout=60, # 1 Minute Timeout
) )
if result.returncode == 0: if result.returncode == 0:
@@ -355,7 +364,7 @@ class TransformationJob:
cmd_diff, cmd_diff,
capture_output=True, capture_output=True,
text=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(): if result_diff.returncode == 0 or self.diff_pdf.exists():
@@ -392,6 +401,7 @@ class TransformationJob:
result = { result = {
"success": False, "success": False,
"xml_file": str(self.xml_file), "xml_file": str(self.xml_file),
"xsl_id": self.xsl_id,
"steps": {}, "steps": {},
"duration": None, "duration": None,
"new_pdf": str(self.new_pdf) if self.new_pdf.exists() else None, "new_pdf": str(self.new_pdf) if self.new_pdf.exists() else None,
+11 -6
View File
@@ -64,7 +64,7 @@
</sizepolicy> </sizepolicy>
</property> </property>
<property name="columnCount"> <property name="columnCount">
<number>2</number> <number>3</number>
</property> </property>
<attribute name="headerHighlightSections"> <attribute name="headerHighlightSections">
<bool>true</bool> <bool>true</bool>
@@ -82,6 +82,11 @@
<string notr="true">2</string> <string notr="true">2</string>
</property> </property>
</column> </column>
<column>
<property name="text">
<string notr="true">3</string>
</property>
</column>
</widget> </widget>
</item> </item>
<item> <item>
@@ -171,8 +176,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>54</width> <width>68</width>
<height>718</height> <height>728</height>
</rect> </rect>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_2"> <layout class="QVBoxLayout" name="verticalLayout_2">
@@ -349,8 +354,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>649</width> <width>625</width>
<height>690</height> <height>700</height>
</rect> </rect>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_3"> <layout class="QVBoxLayout" name="verticalLayout_3">
@@ -396,7 +401,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>1263</width> <width>1263</width>
<height>33</height> <height>22</height>
</rect> </rect>
</property> </property>
<widget class="QMenu" name="menuProjekt"> <widget class="QMenu" name="menuProjekt">
+6 -5
View File
@@ -3,7 +3,7 @@
################################################################################ ################################################################################
## Form generated from reading UI file 'MainWinddow.ui' ## Form generated from reading UI file 'MainWinddow.ui'
## ##
## Created by: Qt User Interface Compiler version 6.9.1 ## Created by: Qt User Interface Compiler version 6.9.2
## ##
## WARNING! All changes made in this file will be lost when recompiling UI file! ## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################ ################################################################################
@@ -69,6 +69,7 @@ class Ui_MainWindow(object):
self.verticalLayout.setContentsMargins(-1, -1, -1, 0) self.verticalLayout.setContentsMargins(-1, -1, -1, 0)
self.treeWidget = QTreeWidget(self.frame) self.treeWidget = QTreeWidget(self.frame)
__qtreewidgetitem = QTreeWidgetItem() __qtreewidgetitem = QTreeWidgetItem()
__qtreewidgetitem.setText(2, u"3");
__qtreewidgetitem.setText(1, u"2"); __qtreewidgetitem.setText(1, u"2");
__qtreewidgetitem.setText(0, u"1"); __qtreewidgetitem.setText(0, u"1");
self.treeWidget.setHeaderItem(__qtreewidgetitem) self.treeWidget.setHeaderItem(__qtreewidgetitem)
@@ -78,7 +79,7 @@ class Ui_MainWindow(object):
sizePolicy1.setVerticalStretch(0) sizePolicy1.setVerticalStretch(0)
sizePolicy1.setHeightForWidth(self.treeWidget.sizePolicy().hasHeightForWidth()) sizePolicy1.setHeightForWidth(self.treeWidget.sizePolicy().hasHeightForWidth())
self.treeWidget.setSizePolicy(sizePolicy1) self.treeWidget.setSizePolicy(sizePolicy1)
self.treeWidget.setColumnCount(2) self.treeWidget.setColumnCount(3)
self.treeWidget.header().setHighlightSections(True) self.treeWidget.header().setHighlightSections(True)
self.treeWidget.header().setStretchLastSection(True) self.treeWidget.header().setStretchLastSection(True)
@@ -131,7 +132,7 @@ class Ui_MainWindow(object):
self.scrollArea.setWidgetResizable(True) self.scrollArea.setWidgetResizable(True)
self.scrollAreaWidgetContents = QWidget() self.scrollAreaWidgetContents = QWidget()
self.scrollAreaWidgetContents.setObjectName(u"scrollAreaWidgetContents") self.scrollAreaWidgetContents.setObjectName(u"scrollAreaWidgetContents")
self.scrollAreaWidgetContents.setGeometry(QRect(0, 0, 54, 718)) self.scrollAreaWidgetContents.setGeometry(QRect(0, 0, 68, 728))
self.verticalLayout_2 = QVBoxLayout(self.scrollAreaWidgetContents) self.verticalLayout_2 = QVBoxLayout(self.scrollAreaWidgetContents)
self.verticalLayout_2.setObjectName(u"verticalLayout_2") self.verticalLayout_2.setObjectName(u"verticalLayout_2")
self.label = QLabel(self.scrollAreaWidgetContents) self.label = QLabel(self.scrollAreaWidgetContents)
@@ -216,7 +217,7 @@ class Ui_MainWindow(object):
self.scrollArea_2.setWidgetResizable(True) self.scrollArea_2.setWidgetResizable(True)
self.scrollAreaWidgetContents_2 = QWidget() self.scrollAreaWidgetContents_2 = QWidget()
self.scrollAreaWidgetContents_2.setObjectName(u"scrollAreaWidgetContents_2") self.scrollAreaWidgetContents_2.setObjectName(u"scrollAreaWidgetContents_2")
self.scrollAreaWidgetContents_2.setGeometry(QRect(0, 0, 649, 690)) self.scrollAreaWidgetContents_2.setGeometry(QRect(0, 0, 625, 700))
self.verticalLayout_3 = QVBoxLayout(self.scrollAreaWidgetContents_2) self.verticalLayout_3 = QVBoxLayout(self.scrollAreaWidgetContents_2)
self.verticalLayout_3.setObjectName(u"verticalLayout_3") self.verticalLayout_3.setObjectName(u"verticalLayout_3")
self.verticalLayout_3.setContentsMargins(0, 0, 0, 0) self.verticalLayout_3.setContentsMargins(0, 0, 0, 0)
@@ -241,7 +242,7 @@ class Ui_MainWindow(object):
MainWindow.setCentralWidget(self.centralwidget) MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QMenuBar(MainWindow) self.menubar = QMenuBar(MainWindow)
self.menubar.setObjectName(u"menubar") self.menubar.setObjectName(u"menubar")
self.menubar.setGeometry(QRect(0, 0, 1263, 33)) self.menubar.setGeometry(QRect(0, 0, 1263, 22))
self.menuProjekt = QMenu(self.menubar) self.menuProjekt = QMenu(self.menubar)
self.menuProjekt.setObjectName(u"menuProjekt") self.menuProjekt.setObjectName(u"menuProjekt")
self.menuThema = QMenu(self.menubar) self.menuThema = QMenu(self.menubar)
+365 -135
View File
@@ -9,7 +9,19 @@ from typing import List
from PySide6.QtCore import Qt, QSize, QThread, Signal from PySide6.QtCore import Qt, QSize, QThread, Signal
from PySide6.QtGui import QCursor, QPixmap, QPainter, QAction, QIcon, QDragEnterEvent, QDropEvent 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 PySide6.QtPdf import QPdfDocument
from ui.MainWinddow_ui import Ui_MainWindow from ui.MainWinddow_ui import Ui_MainWindow
@@ -100,7 +112,7 @@ class XmlHashCalculatorThread(QThread):
return None return None
# Datei binär lesen und Hash berechnen # 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() file_content = f.read()
hash_obj = hashlib.blake2b(file_content) hash_obj = hashlib.blake2b(file_content)
hash_hex = hash_obj.hexdigest() hash_hex = hash_obj.hexdigest()
@@ -119,9 +131,9 @@ class TransformationThread(QThread):
""" """
# Signale für die Kommunikation mit dem Haupt-Thread # 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_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 all_jobs_finished = Signal(int, int) # successful_count, total_count
def __init__(self, jobs: list[TransformationJob], force: bool = False): def __init__(self, jobs: list[TransformationJob], force: bool = False):
@@ -145,8 +157,9 @@ class TransformationThread(QThread):
for job in self.jobs: for job in self.jobs:
try: try:
# Sende Start-Signal # Sende Start-Signal mit XSL-ID
self.job_started.emit(str(job.xml_file)) 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 # Führe Transformations-Pipeline aus
result = job.run_full_pipeline(force=self.force) result = job.run_full_pipeline(force=self.force)
@@ -160,7 +173,8 @@ class TransformationThread(QThread):
except Exception as e: except Exception as e:
error_msg = f"Unerwarteter Fehler bei Transformation: {str(e)}" error_msg = f"Unerwarteter Fehler bei Transformation: {str(e)}"
logger.error(error_msg) 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 # Sende Abschluss-Signal für alle Jobs
self.all_jobs_finished.emit(self.successful_count, len(self.jobs)) self.all_jobs_finished.emit(self.successful_count, len(self.jobs))
@@ -219,6 +233,9 @@ class MainWindow(QMainWindow):
# Transformations-Thread # Transformations-Thread
self.transformation_thread = None self.transformation_thread = None
# Mapping: xml_file_path_str → QTreeWidgetItem (für Progress Bar und Icon Updates)
self.xml_item_map = {}
# Theme-Menü initialisieren # Theme-Menü initialisieren
self._setup_theme_menu() self._setup_theme_menu()
@@ -226,10 +243,10 @@ class MainWindow(QMainWindow):
self._setup_projects_menu() self._setup_projects_menu()
# #
if (theme := app_settings.theme): if theme := app_settings.theme:
self.change_theme(theme) self.change_theme(theme)
else: else:
self.change_theme('Fusion') self.change_theme("Fusion")
# Bilder laden # Bilder laden
self._load_images() self._load_images()
@@ -292,9 +309,7 @@ class MainWindow(QMainWindow):
project_action.setToolTip(f"Projekt-Ordner: {project.project_dir}") project_action.setToolTip(f"Projekt-Ordner: {project.project_dir}")
# Verbinde die Aktion mit der Projekt-Öffnen-Funktion # Verbinde die Aktion mit der Projekt-Öffnen-Funktion
project_action.triggered.connect( project_action.triggered.connect(lambda checked, proj=project: self.open_existing_project(proj))
lambda checked, proj=project: self.open_existing_project(proj)
)
projects_menu.addAction(project_action) projects_menu.addAction(project_action)
@@ -317,7 +332,7 @@ class MainWindow(QMainWindow):
try: try:
# Prüfe ob project.yaml existiert und nicht leer ist # 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: if project_yaml_path.exists() and project_yaml_path.stat().st_size > 0:
# Versuche die Projekt-Einstellungen zu laden # Versuche die Projekt-Einstellungen zu laden
@@ -338,6 +353,9 @@ class MainWindow(QMainWindow):
# Starte Hash-Berechnung für alle XML-Dateien # Starte Hash-Berechnung für alle XML-Dateien
self._start_xml_hash_calculation() self._start_xml_hash_calculation()
# Setze Icons für bereits existierende Diff-PDFs
self._update_diff_icons_for_existing_pdfs()
except Exception as e: except Exception as e:
print(f"Fehler beim Laden des Projekts '{project.name}': {e}") print(f"Fehler beim Laden des Projekts '{project.name}': {e}")
# Fallback: Erstelle Standard-Einstellungen # Fallback: Erstelle Standard-Einstellungen
@@ -436,18 +454,16 @@ class MainWindow(QMainWindow):
new_doc.load(new_pdf_path) new_doc.load(new_pdf_path)
# Warten bis PDFs geladen sind # Warten bis PDFs geladen sind
if (diff_doc.status() != QPdfDocument.Status.Ready or if (
ref_doc.status() != QPdfDocument.Status.Ready or diff_doc.status() != QPdfDocument.Status.Ready
new_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}") print(f"Fehler beim Laden der PDFs für {pdf_filename}")
continue continue
# PDF-Dokumente für später speichern # PDF-Dokumente für später speichern
self.pdf_documents[pdf_filename] = { self.pdf_documents[pdf_filename] = {"diff": diff_doc, "ref": ref_doc, "new": new_doc}
'diff': diff_doc,
'ref': ref_doc,
'new': new_doc
}
print(f"PDFs geladen: {pdf_filename}") print(f"PDFs geladen: {pdf_filename}")
print(f" diff: {diff_doc.pageCount()} Seiten") print(f" diff: {diff_doc.pageCount()} Seiten")
@@ -469,8 +485,9 @@ class MainWindow(QMainWindow):
scale_factor = 200.0 / page_size.width() # 200 Pixel Breite für Thumbnail scale_factor = 200.0 / page_size.width() # 200 Pixel Breite für Thumbnail
# Seite rendern # Seite rendern
page_image = diff_doc.render(page_num, QSize(int(page_size.width() * scale_factor), page_image = diff_doc.render(
int(page_size.height() * scale_factor))) page_num, QSize(int(page_size.width() * scale_factor), int(page_size.height() * scale_factor))
)
diff_pixmap = QPixmap.fromImage(page_image) diff_pixmap = QPixmap.fromImage(page_image)
@@ -488,10 +505,7 @@ class MainWindow(QMainWindow):
self.ui.verticalLayout_2.addWidget(thumbnail_info) self.ui.verticalLayout_2.addWidget(thumbnail_info)
# Beziehung zwischen Thumbnail und Seitennummer speichern # Beziehung zwischen Thumbnail und Seitennummer speichern
self.thumbnail_to_page[thumbnail] = { self.thumbnail_to_page[thumbnail] = {"pdf_filename": pdf_filename, "page_num": page_num}
'pdf_filename': pdf_filename,
'page_num': page_num
}
# Click-Event für das Thumbnail einrichten # Click-Event für das Thumbnail einrichten
thumbnail.mousePressEvent = lambda event, t=thumbnail: self.on_thumbnail_clicked(event, t) 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 # 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.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.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 # Zeige die erste Seite initial an
if self.current_pdf: if self.current_pdf:
@@ -546,13 +562,12 @@ class MainWindow(QMainWindow):
docs = self.pdf_documents[pdf_filename] docs = self.pdf_documents[pdf_filename]
# Diff-Seite laden (bestimmt die Abmessungen) # Diff-Seite laden (bestimmt die Abmessungen)
diff_doc = docs['diff'] diff_doc = docs["diff"]
page_size = diff_doc.pagePointSize(page_num) page_size = diff_doc.pagePointSize(page_num)
# Hohe Auflösung für Vollbild (entspricht ca. Matrix(2.0, 2.0) in PyMuPDF) # Hohe Auflösung für Vollbild (entspricht ca. Matrix(2.0, 2.0) in PyMuPDF)
scale_factor = 2.0 scale_factor = 2.0
render_size = QSize(int(page_size.width() * scale_factor), render_size = QSize(int(page_size.width() * scale_factor), int(page_size.height() * scale_factor))
int(page_size.height() * scale_factor))
# Diff-Seite rendern (immer vorhanden) # Diff-Seite rendern (immer vorhanden)
diff_image = diff_doc.render(page_num, render_size) diff_image = diff_doc.render(page_num, render_size)
@@ -563,7 +578,7 @@ class MainWindow(QMainWindow):
diff_height = diff_pixmap.height() diff_height = diff_pixmap.height()
# Ref-Seite prüfen und rendern oder weiße Seite erstellen # 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(): if page_num < ref_doc.pageCount():
ref_image = ref_doc.render(page_num, render_size) ref_image = ref_doc.render(page_num, render_size)
ref_pixmap = QPixmap.fromImage(ref_image) ref_pixmap = QPixmap.fromImage(ref_image)
@@ -575,7 +590,7 @@ class MainWindow(QMainWindow):
print(f"Weiße Ref-Seite {page_num + 1} erstellt") print(f"Weiße Ref-Seite {page_num + 1} erstellt")
# New-Seite prüfen und rendern oder weiße Seite erstellen # 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(): if page_num < new_doc.pageCount():
new_image = new_doc.render(page_num, render_size) new_image = new_doc.render(page_num, render_size)
new_pixmap = QPixmap.fromImage(new_image) new_pixmap = QPixmap.fromImage(new_image)
@@ -587,11 +602,7 @@ class MainWindow(QMainWindow):
print(f"Weiße New-Seite {page_num + 1} erstellt") print(f"Weiße New-Seite {page_num + 1} erstellt")
# Cache die gerenderten Pixmaps für schnelle Alpha/Zoom-Operationen # Cache die gerenderten Pixmaps für schnelle Alpha/Zoom-Operationen
self.current_rendered_pixmaps = { self.current_rendered_pixmaps = {"ref": ref_pixmap, "diff": diff_pixmap, "new": new_pixmap}
'ref': ref_pixmap,
'diff': diff_pixmap,
'new': new_pixmap
}
# Aktualisiere aktuelle Seite # Aktualisiere aktuelle Seite
self.current_page = page_num self.current_page = page_num
@@ -620,9 +631,9 @@ class MainWindow(QMainWindow):
return return
# Hole die gecachten Pixmaps # Hole die gecachten Pixmaps
ref_pixmap = self.current_rendered_pixmaps['ref'] ref_pixmap = self.current_rendered_pixmaps["ref"]
diff_pixmap = self.current_rendered_pixmaps['diff'] diff_pixmap = self.current_rendered_pixmaps["diff"]
new_pixmap = self.current_rendered_pixmaps['new'] new_pixmap = self.current_rendered_pixmaps["new"]
# Erstelle das überlagerte Bild mit aktuellem Alpha-Wert # Erstelle das überlagerte Bild mit aktuellem Alpha-Wert
alpha_value = self.ui.alpha.value() alpha_value = self.ui.alpha.value()
@@ -886,7 +897,7 @@ class MainWindow(QMainWindow):
if node_type == "TreeNode": if node_type == "TreeNode":
# Kontextmenü für TreeNode # Kontextmenü für TreeNode
action_add_child = QAction("Unterknoten hinzufügen", self) 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)) action_add_child.triggered.connect(lambda: self._add_tree_node_child(item))
menu.addAction(action_add_child) menu.addAction(action_add_child)
@@ -967,7 +978,7 @@ class MainWindow(QMainWindow):
else: else:
# Unbekannter Typ oder leerer Bereich - Menü für Root-Elemente # Unbekannter Typ oder leerer Bereich - Menü für Root-Elemente
action_add_tree_node = QAction("Unterknoten hinzufügen", self) 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()) action_add_tree_node.triggered.connect(lambda: self._add_root_tree_node())
menu.addAction(action_add_tree_node) menu.addAction(action_add_tree_node)
@@ -1019,14 +1030,16 @@ class MainWindow(QMainWindow):
# Erstelle PdfProject-Objekt # Erstelle PdfProject-Objekt
new_project = Project( new_project = Project(
id=new_id, id=new_id,
name=project_data['name'], name=project_data["name"],
project_dir=Path(project_data['project_dir']), project_dir=Path(project_data["project_dir"]),
java_vm_id=project_data['java_vm_id'] if project_data['java_vm_id'] != -1 else 1, 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, 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, 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, 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, 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, postgre_sql_db_id=project_data["postgre_sql_db_id"]
if project_data["postgre_sql_db_id"] != -1
else 1,
) )
# Erstelle Projekt-Ordnerstruktur # Erstelle Projekt-Ordnerstruktur
@@ -1059,13 +1072,13 @@ class MainWindow(QMainWindow):
project_dir = Path(project.project_dir) project_dir = Path(project.project_dir)
# Erstelle Unterordner # Erstelle Unterordner
subdirs = ['xml', 'new', 'diff', 'ref', 'tmp'] subdirs = ["xml", "new", "diff", "ref", "tmp"]
for subdir in subdirs: for subdir in subdirs:
subdir_path = project_dir / subdir subdir_path = project_dir / subdir
subdir_path.mkdir(parents=True, exist_ok=True) subdir_path.mkdir(parents=True, exist_ok=True)
print(f"Ordner erstellt: {subdir_path}") 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 # Erstelle Standard-Projekt-Einstellungen und speichere sie
if not project_yaml_path.exists(): if not project_yaml_path.exists():
@@ -1096,8 +1109,8 @@ class MainWindow(QMainWindow):
""" """
page_info = self.thumbnail_to_page.get(thumbnail) page_info = self.thumbnail_to_page.get(thumbnail)
if page_info: if page_info:
pdf_filename = page_info['pdf_filename'] pdf_filename = page_info["pdf_filename"]
page_num = page_info['page_num'] page_num = page_info["page_num"]
print(f"Thumbnail für Seite {page_num + 1} von {pdf_filename} wurde angeklickt") print(f"Thumbnail für Seite {page_num + 1} von {pdf_filename} wurde angeklickt")
@@ -1164,8 +1177,11 @@ class MainWindow(QMainWindow):
# TreeWidget leeren # TreeWidget leeren
self.ui.treeWidget.clear() self.ui.treeWidget.clear()
# Lösche XML-Item-Map
self.xml_item_map.clear()
# Prüfe ob pdf_project existiert und Nodes hat # 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") print("Keine Projekt-Einstellungen verfügbar")
return return
@@ -1245,6 +1261,14 @@ class MainWindow(QMainWindow):
item.addChild(xml_item) 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 return item
except Exception as e: except Exception as e:
@@ -1255,6 +1279,144 @@ class MainWindow(QMainWindow):
fallback_item.setText(1, str(e)) fallback_item.setText(1, str(e))
return fallback_item 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 # Kontextmenü-Aktionen für TreeNode
def _add_tree_node_child(self, parent_item): def _add_tree_node_child(self, parent_item):
"""Fügt einen Unterknoten zu einem TreeNode hinzu.""" """Fügt einen Unterknoten zu einem TreeNode hinzu."""
@@ -1297,8 +1459,8 @@ class MainWindow(QMainWindow):
data = dialog.get_data() data = dialog.get_data()
if data: if data:
# Aktualisiere den Node # Aktualisiere den Node
node.bez = data['bez'] node.bez = data["bez"]
node.xslt_params = data['xslt_params'] node.xslt_params = data["xslt_params"]
print(f"TreeNode '{node.bez}' wurde aktualisiert") print(f"TreeNode '{node.bez}' wurde aktualisiert")
print(f"XSLT-Parameter: {node.xslt_params}") print(f"XSLT-Parameter: {node.xslt_params}")
@@ -1333,11 +1495,11 @@ class MainWindow(QMainWindow):
try: try:
# Prüfe ob ein Projekt geladen ist # 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.") QMessageBox.warning(self, "Warnung", "Kein Projekt geladen. Bitte öffnen Sie zuerst ein Projekt.")
return 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.") QMessageBox.warning(self, "Warnung", "Keine Projekt-Einstellungen geladen.")
return return
@@ -1349,10 +1511,7 @@ class MainWindow(QMainWindow):
# Öffne Datei-Dialog zum Auswählen der XML-Datei # Öffne Datei-Dialog zum Auswählen der XML-Datei
xml_file_path, _ = QFileDialog.getOpenFileName( xml_file_path, _ = QFileDialog.getOpenFileName(
self, self, "XML-Datei auswählen", "", "XML-Dateien (*.xml);;Alle Dateien (*)"
"XML-Datei auswählen",
"",
"XML-Dateien (*.xml);;Alle Dateien (*)"
) )
if not xml_file_path: if not xml_file_path:
@@ -1381,7 +1540,7 @@ class MainWindow(QMainWindow):
f"Eine XML-Datei mit dem Namen '{xml_file_path.name}' existiert bereits im xml-Ordner.\n\n" f"Eine XML-Datei mit dem Namen '{xml_file_path.name}' existiert bereits im xml-Ordner.\n\n"
"Möchten Sie sie überschreiben?", "Möchten Sie sie überschreiben?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No QMessageBox.StandardButton.No,
) )
if reply != QMessageBox.StandardButton.Yes: if reply != QMessageBox.StandardButton.Yes:
@@ -1405,7 +1564,7 @@ class MainWindow(QMainWindow):
QMessageBox.information( QMessageBox.information(
self, self,
"XML-Datei bereits vorhanden", "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 return
@@ -1461,8 +1620,8 @@ class MainWindow(QMainWindow):
data = dialog.get_data() data = dialog.get_data()
if data: if data:
# Aktualisiere den Node # Aktualisiere den Node
node.bez = data['bez'] node.bez = data["bez"]
node.xslt_params = data['xslt_params'] node.xslt_params = data["xslt_params"]
print(f"XslFile '{node.bez}' wurde aktualisiert") print(f"XslFile '{node.bez}' wurde aktualisiert")
print(f"XSLT-Parameter: {node.xslt_params}") print(f"XSLT-Parameter: {node.xslt_params}")
@@ -1502,11 +1661,11 @@ class MainWindow(QMainWindow):
try: try:
# Prüfe ob ein Projekt geladen ist # 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.") QMessageBox.warning(self, "Warnung", "Kein Projekt geladen. Bitte öffnen Sie zuerst ein Projekt.")
return 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.") QMessageBox.warning(self, "Warnung", "Keine Projekt-Einstellungen geladen.")
return return
@@ -1536,7 +1695,7 @@ class MainWindow(QMainWindow):
f"Möchten Sie die XML-Datei '{xml_filename}' aus der XSL-Datei '{xsl_file_obj.bez}' entfernen?\n\n" 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.", "Die XML-Datei wird nur aus der Zuordnung entfernt, nicht physisch gelöscht.",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No QMessageBox.StandardButton.No,
) )
if reply != QMessageBox.StandardButton.Yes: if reply != QMessageBox.StandardButton.Yes:
@@ -1567,7 +1726,7 @@ class MainWindow(QMainWindow):
f"Die XML-Datei '{xml_filename}' wird in keiner anderen XSL-Datei verwendet.\n\n" 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?", "Möchten Sie auch die physische Datei aus dem xml-Ordner löschen?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No QMessageBox.StandardButton.No,
) )
if delete_reply == QMessageBox.StandardButton.Yes: if delete_reply == QMessageBox.StandardButton.Yes:
@@ -1575,13 +1734,11 @@ class MainWindow(QMainWindow):
xml_file_path.unlink() xml_file_path.unlink()
print(f"Physische XML-Datei gelöscht: {xml_file_path}") print(f"Physische XML-Datei gelöscht: {xml_file_path}")
except Exception as e: except Exception as e:
QMessageBox.warning( QMessageBox.warning(self, "Warnung", f"Fehler beim Löschen der physischen Datei:\n{str(e)}")
self,
"Warnung",
f"Fehler beim Löschen der physischen Datei:\n{str(e)}"
)
else: 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 # Speichere die aktualisierten Projekt-Einstellungen
self._save_project_settings() self._save_project_settings()
@@ -1657,7 +1814,7 @@ class MainWindow(QMainWindow):
try: try:
# Prüfe ob ein Projekt geladen ist # 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.") QMessageBox.warning(self, "Warnung", "Kein Projekt geladen. Bitte öffnen Sie zuerst ein Projekt.")
return return
@@ -1727,13 +1884,14 @@ class MainWindow(QMainWindow):
QMessageBox.critical(self, "Fehler", f"SQL-Datei nicht gefunden: {sql_file_path}") QMessageBox.critical(self, "Fehler", f"SQL-Datei nicht gefunden: {sql_file_path}")
return None 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() sql_query = f.read()
print(f"SQL-Abfrage geladen: {len(sql_query)} Zeichen") print(f"SQL-Abfrage geladen: {len(sql_query)} Zeichen")
# Verbindung zur PostgreSQL-Datenbank herstellen # Verbindung zur PostgreSQL-Datenbank herstellen
connection_string = ("postgresql://" connection_string = (
"postgresql://"
f"{db_config.username}:" f"{db_config.username}:"
f"{db_config.password}@" f"{db_config.password}@"
f"{db_config.host}:" f"{db_config.host}:"
@@ -1744,7 +1902,9 @@ class MainWindow(QMainWindow):
print(f"Verbinde zu PostgreSQL: {db_config.host}:{db_config.port}/{db_config.database}") 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 return df
except Exception as e: except Exception as e:
error_msg = f"Fehler beim Ausführen der SQL-Abfrage: {str(e)}" error_msg = f"Fehler beim Ausführen der SQL-Abfrage: {str(e)}"
@@ -1784,14 +1944,16 @@ class MainWindow(QMainWindow):
for r2 in r1_children.rows(named=True): for r2 in r1_children.rows(named=True):
tn_2 = TreeNode(id=(r2["reporttyp"], r2["report"]), bez=r2["report_bez"], children=[]) 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): for r3 in r2_children.rows(named=True):
x = XslFile( x = XslFile(
id=(r3["reporttyp"], r3["report"], r3["repfile"]), id=(r3["reporttyp"], r3["report"], r3["repfile"]),
bez=r3["repfile_bez"], bez=r3["repfile_bez"],
xsl_file=Path(r3["xsl_datei"]), xsl_file=Path(r3["xsl_datei"]),
xmls=[] xmls=[],
) )
tn_2.children.append(x) tn_2.children.append(x)
@@ -1875,13 +2037,17 @@ class MainWindow(QMainWindow):
# Aktualisiere nur die Bezeichnung, falls sie sich geändert hat # Aktualisiere nur die Bezeichnung, falls sie sich geändert hat
if existing_node.bez != new_node.bez: 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 existing_node.bez = new_node.bez
# Für XslFile: Aktualisiere xsl_file Pfad # Für XslFile: Aktualisiere xsl_file Pfad
if isinstance(existing_node, XslFile) and isinstance(new_node, XslFile): if isinstance(existing_node, XslFile) and isinstance(new_node, XslFile):
if existing_node.xsl_file != new_node.xsl_file: 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 existing_node.xsl_file = new_node.xsl_file
# Rekursiv für Knoten (nur bei TreeNode) # Rekursiv für Knoten (nur bei TreeNode)
@@ -1918,7 +2084,7 @@ class MainWindow(QMainWindow):
# Hole das Node-Objekt # Hole das Node-Objekt
parent_node = current_item.data(0, Qt.ItemDataRole.UserRole) 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 # Füge die Parameter des Eltern-Nodes hinzu
# Eltern-Parameter haben niedrigere Priorität (werden überschrieben) # Eltern-Parameter haben niedrigere Priorität (werden überschrieben)
for key, value in parent_node.xslt_params.items(): for key, value in parent_node.xslt_params.items():
@@ -1994,8 +2160,7 @@ class MainWindow(QMainWindow):
urls = event.mimeData().urls() urls = event.mimeData().urls()
# Prüfe ob mindestens eine XML-Datei dabei ist # Prüfe ob mindestens eine XML-Datei dabei ist
xml_files = [url.toLocalFile() for url in urls xml_files = [url.toLocalFile() for url in urls if url.toLocalFile().lower().endswith(".xml")]
if url.toLocalFile().lower().endswith('.xml')]
if xml_files: if xml_files:
event.acceptProposedAction() event.acceptProposedAction()
@@ -2024,8 +2189,7 @@ class MainWindow(QMainWindow):
urls = event.mimeData().urls() urls = event.mimeData().urls()
# Prüfe ob mindestens eine XML-Datei dabei ist # Prüfe ob mindestens eine XML-Datei dabei ist
xml_files = [url.toLocalFile() for url in urls xml_files = [url.toLocalFile() for url in urls if url.toLocalFile().lower().endswith(".xml")]
if url.toLocalFile().lower().endswith('.xml')]
if xml_files: if xml_files:
event.acceptProposedAction() event.acceptProposedAction()
@@ -2047,12 +2211,12 @@ class MainWindow(QMainWindow):
""" """
try: try:
# Prüfe ob ein Projekt geladen ist # 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.") QMessageBox.warning(self, "Warnung", "Kein Projekt geladen. Bitte öffnen Sie zuerst ein Projekt.")
event.ignore() event.ignore()
return 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.") QMessageBox.warning(self, "Warnung", "Keine Projekt-Einstellungen geladen.")
event.ignore() event.ignore()
return return
@@ -2068,7 +2232,7 @@ class MainWindow(QMainWindow):
# Sammle alle XML-Dateien # Sammle alle XML-Dateien
for url in urls: for url in urls:
file_path = url.toLocalFile() file_path = url.toLocalFile()
if file_path.lower().endswith('.xml'): if file_path.lower().endswith(".xml"):
xml_files.append(Path(file_path)) xml_files.append(Path(file_path))
if not xml_files: if not xml_files:
@@ -2112,9 +2276,7 @@ class MainWindow(QMainWindow):
# Öffne den Dialog zur Zuordnung zu XSL-Knoten # Öffne den Dialog zur Zuordnung zu XSL-Knoten
dialog = XmlToXslAssignDialog( dialog = XmlToXslAssignDialog(
parent=self, parent=self, xml_file_path=xml_file_path, project_nodes=self.pdf_project.nodes
xml_file_path=xml_file_path,
project_nodes=self.pdf_project.nodes
) )
if dialog.exec() == XmlToXslAssignDialog.DialogCode.Accepted: if dialog.exec() == XmlToXslAssignDialog.DialogCode.Accepted:
@@ -2173,7 +2335,7 @@ class MainWindow(QMainWindow):
Startet die asynchrone Hash-Berechnung für alle XML-Dateien im Projekt. Startet die asynchrone Hash-Berechnung für alle XML-Dateien im Projekt.
""" """
try: 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") logger.debug("Keine Projekt-Einstellungen verfügbar für Hash-Berechnung")
return return
@@ -2198,8 +2360,7 @@ class MainWindow(QMainWindow):
# Erstelle und starte neuen Hash-Berechnungs-Thread # Erstelle und starte neuen Hash-Berechnungs-Thread
self.hash_calculator_thread = XmlHashCalculatorThread( self.hash_calculator_thread = XmlHashCalculatorThread(
project_dir=Path(self.project.project_dir), project_dir=Path(self.project.project_dir), xml_files=xml_files
xml_files=xml_files
) )
# Verbinde Signale # Verbinde Signale
@@ -2321,7 +2482,7 @@ class MainWindow(QMainWindow):
return return
# Datei binär lesen und Hash berechnen # 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() file_content = f.read()
hash_obj = hashlib.blake2b(file_content) hash_obj = hashlib.blake2b(file_content)
hash_hex = hash_obj.hexdigest() hash_hex = hash_obj.hexdigest()
@@ -2481,7 +2642,7 @@ class MainWindow(QMainWindow):
return None return None
# Datei binär lesen und Hash berechnen # 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() file_content = f.read()
hash_obj = hashlib.blake2b(file_content) hash_obj = hashlib.blake2b(file_content)
hash_hex = hash_obj.hexdigest() hash_hex = hash_obj.hexdigest()
@@ -2531,13 +2692,13 @@ class MainWindow(QMainWindow):
self, self,
"XML-Datei zugeordnet", "XML-Datei zugeordnet",
f"Eine XML-Datei mit gleichem Inhalt war bereits im Projekt vorhanden.\n\n" 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: else:
QMessageBox.information( QMessageBox.information(
self, self,
"Bereits zugeordnet", "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: except Exception as e:
@@ -2620,16 +2781,20 @@ class MainWindow(QMainWindow):
self._load_nodes_to_tree() self._load_nodes_to_tree()
# Zeige Erfolgsmeldung # 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: 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) QMessageBox.information(self, "Erfolg", success_msg)
else: else:
QMessageBox.information( QMessageBox.information(
self, self,
"Information", "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: except Exception as e:
@@ -2649,7 +2814,15 @@ class MainWindow(QMainWindow):
Path|None: Ausgewählter Pfad oder None bei Abbruch Path|None: Ausgewählter Pfad oder None bei Abbruch
""" """
try: 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 = QDialog(self)
dialog.setWindowTitle("Dateiname auswählen") dialog.setWindowTitle("Dateiname auswählen")
@@ -2659,8 +2832,10 @@ class MainWindow(QMainWindow):
layout = QVBoxLayout(dialog) layout = QVBoxLayout(dialog)
# Erklärungstext # Erklärungstext
info_label = QLabel(f"Eine Datei mit dem Namen '{original_name}' existiert bereits.\n\n" info_label = QLabel(
"Bitte wählen Sie einen alternativen Dateinamen:") f"Eine Datei mit dem Namen '{original_name}' existiert bereits.\n\n"
"Bitte wählen Sie einen alternativen Dateinamen:"
)
layout.addWidget(info_label) layout.addWidget(info_label)
# Radio-Buttons für alternative Namen # Radio-Buttons für alternative Namen
@@ -2807,16 +2982,19 @@ class MainWindow(QMainWindow):
# Prüfe ob alle Konfigurationen vorhanden sind # Prüfe ob alle Konfigurationen vorhanden sind
if not all([java_vm, saxon_jar, apache_fop, diff_pdf, xsl_dir]): if not all([java_vm, saxon_jar, apache_fop, diff_pdf, xsl_dir]):
missing = [] missing = []
if not java_vm: missing.append("Java VM") if not java_vm:
if not saxon_jar: missing.append("Saxon JAR") missing.append("Java VM")
if not apache_fop: missing.append("Apache FOP") if not saxon_jar:
if not diff_pdf: missing.append("diff-pdf") missing.append("Saxon JAR")
if not xsl_dir: missing.append("XSL-Verzeichnis") 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( QMessageBox.warning(
self, self, "Fehlende Konfiguration", f"Folgende Konfigurationen fehlen: {', '.join(missing)}"
"Fehlende Konfiguration",
f"Folgende Konfigurationen fehlen: {', '.join(missing)}"
) )
return None return None
@@ -2839,7 +3017,7 @@ class MainWindow(QMainWindow):
apache_fop_dir=apache_fop.path_to_dir, apache_fop_dir=apache_fop.path_to_dir,
diff_pdf_path=diff_pdf.path_to_binary_file, diff_pdf_path=diff_pdf.path_to_binary_file,
diff_pdf_params=diff_pdf.default_params, diff_pdf_params=diff_pdf.default_params,
xsl_id=xsl_file_obj.id xsl_id=xsl_file_obj.id,
) )
return job return job
@@ -2882,16 +3060,37 @@ class MainWindow(QMainWindow):
logger.error(f"Fehler beim Starten der Transformation: {e}") logger.error(f"Fehler beim Starten der Transformation: {e}")
QMessageBox.critical(self, "Fehler", f"Fehler beim Starten: {str(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. Signal-Handler: Ein Job wurde gestartet.
Args: Args:
xml_file_name: Name der XML-Datei 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}") 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): def _on_transformation_job_finished(self, result: dict):
""" """
Signal-Handler: Ein Job wurde abgeschlossen. 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" error_text = "\n".join(error_msgs) if error_msgs else "Unbekannter Fehler"
QMessageBox.critical( QMessageBox.critical(
self, self, "Transformation fehlgeschlagen", f"XML-Datei: {xml_file}\n\nFehler:\n{error_text}"
"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. Signal-Handler: Ein Job ist mit einem Fehler abgebrochen.
Args: Args:
xml_file_name: Name der XML-Datei xml_file_name: Name der XML-Datei
xsl_id_str: XSL-ID als String
error_message: Fehlermeldung 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}") 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): def _on_all_transformations_finished(self, successful_count: int, total_count: int):
""" """
Signal-Handler: Alle Jobs wurden abgeschlossen. Signal-Handler: Alle Jobs wurden abgeschlossen.
@@ -2950,32 +3176,36 @@ class MainWindow(QMainWindow):
if successful_count == total_count: if successful_count == total_count:
self.statusBar().showMessage(f"✓ Alle {total_count} Transformationen erfolgreich", 5000) self.statusBar().showMessage(f"✓ Alle {total_count} Transformationen erfolgreich", 5000)
QMessageBox.information( QMessageBox.information(
self, self, "Abgeschlossen", f"Alle {total_count} Transformationen wurden erfolgreich abgeschlossen"
"Abgeschlossen",
f"Alle {total_count} Transformationen wurden erfolgreich abgeschlossen"
) )
else: else:
failed_count = total_count - successful_count failed_count = total_count - successful_count
self.statusBar().showMessage( self.statusBar().showMessage(
f"{successful_count}/{total_count} erfolgreich, {failed_count} fehlgeschlagen", f"{successful_count}/{total_count} erfolgreich, {failed_count} fehlgeschlagen", 5000
5000
) )
QMessageBox.warning( QMessageBox.warning(
self, self,
"Abgeschlossen mit Fehlern", "Abgeschlossen mit Fehlern",
f"{successful_count} von {total_count} Transformationen erfolgreich\n" f"{successful_count} von {total_count} Transformationen erfolgreich\n{failed_count} fehlgeschlagen",
f"{failed_count} fehlgeschlagen"
) )
def closeEvent(self, event): def closeEvent(self, event):
"""Wird beim Schließen der Anwendung aufgerufen.""" """Wird beim Schließen der Anwendung aufgerufen."""
# Stoppe Hash-Berechnungs-Thread falls noch aktiv # 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.quit()
self.hash_calculator_thread.wait() self.hash_calculator_thread.wait()
# Stoppe Transformations-Thread falls noch aktiv # 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.quit()
self.transformation_thread.wait() self.transformation_thread.wait()