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,
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,
+11 -6
View File
@@ -64,7 +64,7 @@
</sizepolicy>
</property>
<property name="columnCount">
<number>2</number>
<number>3</number>
</property>
<attribute name="headerHighlightSections">
<bool>true</bool>
@@ -82,6 +82,11 @@
<string notr="true">2</string>
</property>
</column>
<column>
<property name="text">
<string notr="true">3</string>
</property>
</column>
</widget>
</item>
<item>
@@ -171,8 +176,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>54</width>
<height>718</height>
<width>68</width>
<height>728</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
@@ -349,8 +354,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>649</width>
<height>690</height>
<width>625</width>
<height>700</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
@@ -396,7 +401,7 @@
<x>0</x>
<y>0</y>
<width>1263</width>
<height>33</height>
<height>22</height>
</rect>
</property>
<widget class="QMenu" name="menuProjekt">
+6 -5
View File
@@ -3,7 +3,7 @@
################################################################################
## 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!
################################################################################
@@ -69,6 +69,7 @@ class Ui_MainWindow(object):
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)
@@ -78,7 +79,7 @@ class Ui_MainWindow(object):
sizePolicy1.setVerticalStretch(0)
sizePolicy1.setHeightForWidth(self.treeWidget.sizePolicy().hasHeightForWidth())
self.treeWidget.setSizePolicy(sizePolicy1)
self.treeWidget.setColumnCount(2)
self.treeWidget.setColumnCount(3)
self.treeWidget.header().setHighlightSections(True)
self.treeWidget.header().setStretchLastSection(True)
@@ -131,7 +132,7 @@ class Ui_MainWindow(object):
self.scrollArea.setWidgetResizable(True)
self.scrollAreaWidgetContents = QWidget()
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.setObjectName(u"verticalLayout_2")
self.label = QLabel(self.scrollAreaWidgetContents)
@@ -216,7 +217,7 @@ class Ui_MainWindow(object):
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.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)
@@ -241,7 +242,7 @@ class Ui_MainWindow(object):
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QMenuBar(MainWindow)
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.setObjectName(u"menuProjekt")
self.menuThema = QMenu(self.menubar)
+367 -137
View File
@@ -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
@@ -100,7 +112,7 @@ class XmlHashCalculatorThread(QThread):
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()
@@ -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))
@@ -219,6 +233,9 @@ class MainWindow(QMainWindow):
# 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()
@@ -226,10 +243,10 @@ class MainWindow(QMainWindow):
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()
@@ -292,9 +309,7 @@ class MainWindow(QMainWindow):
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)
@@ -317,7 +332,7 @@ class MainWindow(QMainWindow):
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
@@ -338,6 +353,9 @@ class MainWindow(QMainWindow):
# 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")
@@ -469,8 +485,9 @@ class MainWindow(QMainWindow):
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)
@@ -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:
@@ -546,13 +562,12 @@ class MainWindow(QMainWindow):
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)
@@ -563,7 +578,7 @@ class MainWindow(QMainWindow):
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
@@ -886,7 +897,7 @@ class MainWindow(QMainWindow):
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)
@@ -967,7 +978,7 @@ class MainWindow(QMainWindow):
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)
@@ -1019,14 +1030,16 @@ class MainWindow(QMainWindow):
# 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
@@ -1059,13 +1072,13 @@ class MainWindow(QMainWindow):
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():
@@ -1096,8 +1109,8 @@ 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")
@@ -1164,8 +1177,11 @@ class MainWindow(QMainWindow):
# 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
@@ -1245,6 +1261,14 @@ class MainWindow(QMainWindow):
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:
@@ -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."""
@@ -1297,8 +1459,8 @@ 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}")
@@ -1333,11 +1495,11 @@ class MainWindow(QMainWindow):
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
@@ -1349,10 +1511,7 @@ class MainWindow(QMainWindow):
# Ö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:
@@ -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"
"Möchten Sie sie überschreiben?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No
QMessageBox.StandardButton.No,
)
if reply != QMessageBox.StandardButton.Yes:
@@ -1405,7 +1564,7 @@ class MainWindow(QMainWindow):
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
@@ -1461,8 +1620,8 @@ 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}")
@@ -1502,11 +1661,11 @@ class MainWindow(QMainWindow):
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
@@ -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"
"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:
@@ -1567,7 +1726,7 @@ 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:
@@ -1575,13 +1734,11 @@ class MainWindow(QMainWindow):
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()
@@ -1657,7 +1814,7 @@ class MainWindow(QMainWindow):
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
@@ -1727,13 +1884,14 @@ class MainWindow(QMainWindow):
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}:"
@@ -1744,7 +1902,9 @@ class MainWindow(QMainWindow):
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)}"
@@ -1784,14 +1944,16 @@ 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)
@@ -1875,13 +2037,17 @@ class MainWindow(QMainWindow):
# 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)
@@ -1918,7 +2084,7 @@ class MainWindow(QMainWindow):
# 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():
@@ -1994,8 +2160,7 @@ class MainWindow(QMainWindow):
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()
@@ -2024,8 +2189,7 @@ class MainWindow(QMainWindow):
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()
@@ -2047,12 +2211,12 @@ class MainWindow(QMainWindow):
"""
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
@@ -2068,7 +2232,7 @@ class MainWindow(QMainWindow):
# 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:
@@ -2112,9 +2276,7 @@ class MainWindow(QMainWindow):
# Ö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:
@@ -2173,7 +2335,7 @@ 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
@@ -2198,8 +2360,7 @@ class MainWindow(QMainWindow):
# 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
@@ -2321,7 +2482,7 @@ class MainWindow(QMainWindow):
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()
@@ -2481,7 +2642,7 @@ class MainWindow(QMainWindow):
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()
@@ -2531,13 +2692,13 @@ class MainWindow(QMainWindow):
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:
@@ -2620,16 +2781,20 @@ class MainWindow(QMainWindow):
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:
@@ -2649,7 +2814,15 @@ class MainWindow(QMainWindow):
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")
@@ -2659,8 +2832,10 @@ class MainWindow(QMainWindow):
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
@@ -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()