Files
xsl-validator/src/ui/MainWindow.py
T

2586 lines
106 KiB
Python
Raw Normal View History

import glob
2025-05-23 11:09:47 +02:00
import os
2025-05-31 21:27:58 +02:00
import time
2025-08-10 14:03:15 +02:00
import polars as pl
2025-08-10 17:32:22 +02:00
import shutil
import hashlib
import logging
from concurrent.futures import ThreadPoolExecutor
from typing import List
2025-05-23 11:09:47 +02:00
from PySide6.QtCore import Qt, QSize, QThread, Signal
from PySide6.QtGui import QCursor, QPixmap, QPainter, QAction, QIcon, QDragEnterEvent, QDropEvent
2025-08-10 17:32:22 +02:00
from PySide6.QtWidgets import QLabel, QMainWindow, QApplication, QStyleFactory, QMenu, QTreeWidgetItem, QMessageBox, QFileDialog
2025-05-31 21:27:58 +02:00
from PySide6.QtPdf import QPdfDocument
2025-05-23 11:09:47 +02:00
2025-05-21 20:26:03 +02:00
from ui.MainWinddow_ui import Ui_MainWindow
from ui.AppSettings import AppSettingsDlg
2025-06-16 20:30:56 +02:00
from ui.PdfProject import PdfProjectDlg
2025-08-12 20:56:26 +02:00
from ui.TreeNodeEditDialog import TreeNodeEditDialog
from ui.XslFileEditDialog import XslFileEditDialog
from ui.XmlToXslAssignDialog import XmlToXslAssignDialog
2025-08-10 17:32:22 +02:00
from conf import app_settings, Project, ProjectData, TreeNode, XslFile, XmlFile
2025-06-16 20:30:56 +02:00
from pathlib import Path
2025-05-20 11:24:07 +02:00
logger = logging.getLogger(__name__)
class XmlHashCalculatorThread(QThread):
"""
Thread für die asynchrone Berechnung von blake2b-Hash-Werten für XML-Dateien.
"""
# Signale für die Kommunikation mit dem Haupt-Thread
hash_calculated = Signal(object, str) # xml_file_object, hash_value
calculation_finished = Signal(int, int) # processed_count, total_count
error_occurred = Signal(str, str) # xml_file_path, error_message
def __init__(self, project_dir: Path, xml_files: List[XmlFile]):
"""
Initialisiert den Hash-Berechnungs-Thread.
Args:
project_dir: Pfad zum Projekt-Verzeichnis
xml_files: Liste der XmlFile-Objekte für die Hash-Berechnung
"""
super().__init__()
self.project_dir = project_dir
self.xml_files = xml_files
self.processed_count = 0
def run(self):
"""
Führt die Hash-Berechnung für alle XML-Dateien aus.
"""
logger.info(f"Starte Hash-Berechnung für {len(self.xml_files)} XML-Dateien")
for xml_file in self.xml_files:
try:
# Prüfe ob hashsum bereits vorhanden ist
if xml_file.hashsum:
logger.debug(f"Hash bereits vorhanden für {xml_file.xml}: {xml_file.hashsum}")
self.processed_count += 1
continue
# Berechne Hash für die XML-Datei
xml_file_path = self.project_dir / xml_file.xml
hash_value = self._calculate_blake2b_hash(xml_file_path)
if hash_value:
# Sende Signal mit berechnetem Hash
self.hash_calculated.emit(xml_file, hash_value)
logger.debug(f"Hash berechnet für {xml_file.xml}: {hash_value}")
self.processed_count += 1
except Exception as e:
error_msg = f"Fehler bei Hash-Berechnung für {xml_file.xml}: {str(e)}"
logger.error(error_msg)
self.error_occurred.emit(str(xml_file.xml), error_msg)
self.processed_count += 1
# Sende Abschluss-Signal
self.calculation_finished.emit(self.processed_count, len(self.xml_files))
logger.info(f"Hash-Berechnung abgeschlossen: {self.processed_count}/{len(self.xml_files)} verarbeitet")
def _calculate_blake2b_hash(self, file_path: Path) -> str | None:
"""
Berechnet den blake2b-Hash einer XML-Datei.
Args:
file_path: Pfad zur XML-Datei
Returns:
str: Hash-Wert mit "blake2b:" Präfix oder None bei Fehler
"""
try:
if not file_path.exists():
logger.warning(f"XML-Datei nicht gefunden: {file_path}")
return None
# Datei binär lesen und Hash berechnen
with open(file_path, 'rb') as f:
file_content = f.read()
hash_obj = hashlib.blake2b(file_content)
hash_hex = hash_obj.hexdigest()
# Präfix hinzufügen
return f"blake2b:{hash_hex}"
except Exception as e:
logger.error(f"Fehler beim Berechnen des Hash für {file_path}: {e}")
return None
2025-05-20 11:24:07 +02:00
class MainWindow(QMainWindow):
def __init__(self, parent=None):
"""
Konstruktor für die MainWindow-Klasse.
2025-05-31 21:27:58 +02:00
Verwendet PySide6.QtPdf für optimale Performance.
2025-05-20 11:24:07 +02:00
Args:
parent: Übergeordnetes Widget, falls vorhanden
"""
super().__init__(parent)
2025-05-23 11:09:47 +02:00
2025-05-20 11:24:07 +02:00
# UI einrichten
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
2025-05-23 11:09:47 +02:00
2025-05-29 16:30:01 +02:00
# Dict zum Speichern der Beziehung zwischen Thumbnails und Seitennummern
self.thumbnail_to_page = {}
2025-05-23 11:09:47 +02:00
2025-05-29 16:30:01 +02:00
# PDF-Dokumente für späteres On-Demand-Rendering speichern
2025-05-31 21:27:58 +02:00
self.pdf_documents = {} # {pdf_filename: {'diff': QPdfDocument, 'ref': QPdfDocument, 'new': QPdfDocument}}
2025-05-29 16:30:01 +02:00
2025-05-22 21:05:22 +02:00
# Aktueller Zoom-Faktor
self.current_zoom = 100 # 100%
2025-05-23 11:09:47 +02:00
2025-05-29 16:30:01 +02:00
# Aktuell angezeigte Seite
self.current_page = 0
self.current_pdf = None
2025-05-29 19:03:19 +02:00
# Cache für die aktuell gerenderten Pixmaps (Performance-Optimierung)
self.current_rendered_pixmaps = None
2025-05-29 16:30:01 +02:00
# Label für die Vollansicht (nur ein einziges Label)
self.fullsize_label = None
2025-05-23 21:26:50 +02:00
# Variablen für Drag-to-Scroll (Anti-Jitter für 4K/DPI-Skalierung)
self.is_dragging = False
self.last_drag_position = None
self.drag_threshold = 3 # Mindestbewegung in Pixeln vor dem Scrollen
self.scroll_sensitivity = 0.7 # Reduzierte Empfindlichkeit für sanfteres Scrollen
2025-08-10 17:32:22 +02:00
# Das aktuelle Projekt (Project) aus app_settings
self.project = None
# Das aktuelle ProjectData
self.pdf_project = None
# Hash-Berechnungs-Thread
self.hash_calculator_thread = None
2025-05-27 20:48:21 +02:00
# Theme-Menü initialisieren
self._setup_theme_menu()
2025-06-22 11:58:57 +02:00
# Vorhandene Projekte-Menü initialisieren
self._setup_projects_menu()
#
if (theme := app_settings.theme):
self.change_theme(theme)
else:
self.change_theme('Fusion')
2025-05-27 20:48:21 +02:00
# Bilder laden
2025-05-21 20:26:03 +02:00
self._load_images()
2025-05-23 11:09:47 +02:00
2025-05-20 11:24:07 +02:00
# Signale und Slots verbinden
self._connect_signals()
2025-08-03 16:31:38 +02:00
# Kontextmenü für TreeWidget einrichten
self._setup_tree_context_menu()
2025-08-14 20:32:29 +02:00
# TreeWidget Styling für größeren vertikalen Abstand
self._setup_tree_widget_styling()
# Drag&Drop für TreeWidget aktivieren
self._setup_drag_drop()
2025-05-23 11:09:47 +02:00
2025-05-27 20:48:21 +02:00
def _setup_theme_menu(self):
"""Initialisiert das Theme-Menü mit verfügbaren Themes."""
# Hole alle verfügbaren Themes
available_themes = QStyleFactory.keys()
current_theme = QApplication.style().objectName()
2025-05-28 19:30:37 +02:00
2025-05-27 20:48:21 +02:00
print(f"Verfügbare Themes: {available_themes}")
print(f"Aktuelles Theme: {current_theme}")
2025-05-28 19:30:37 +02:00
2025-05-27 20:48:21 +02:00
# Füge Theme-Aktionen zum Menü hinzu
for theme_name in available_themes:
action = QAction(theme_name, self)
action.setCheckable(True)
2025-05-28 19:30:37 +02:00
2025-05-27 20:48:21 +02:00
# Markiere das aktuelle Theme
if theme_name.lower() == current_theme.lower():
action.setChecked(True)
2025-05-28 19:30:37 +02:00
2025-05-27 20:48:21 +02:00
# Verbinde die Aktion mit der Theme-Wechsel-Funktion
action.triggered.connect(lambda checked, theme=theme_name: self.change_theme(theme))
2025-05-28 19:30:37 +02:00
2025-05-27 20:48:21 +02:00
# Füge die Aktion zum Theme-Menü hinzu
self.ui.menuThema.addAction(action)
2025-06-22 11:58:57 +02:00
def _setup_projects_menu(self):
"""Initialisiert das Vorhandene Projekte-Menü mit gespeicherten Projekten."""
# Prüfe ob Projekte vorhanden sind
if not app_settings.pdf_projects:
# Keine Projekte vorhanden - Menü deaktiviert lassen
self.ui.actionVorhandene_Projekte.setEnabled(False)
self.ui.actionVorhandene_Projekte.setText("Vorhandene Projekte (keine vorhanden)")
return
# Projekte vorhanden - Menü aktivieren und Untermenü erstellen
self.ui.actionVorhandene_Projekte.setEnabled(True)
self.ui.actionVorhandene_Projekte.setText("Vorhandene Projekte")
# Erstelle ein Untermenü für die Projekte
projects_menu = QMenu(self)
# Füge jedes Projekt als Menü-Eintrag hinzu
for project in app_settings.pdf_projects:
project_action = QAction(project.name, self)
project_action.setToolTip(f"Projekt-Ordner: {project.project_dir}")
# Verbinde die Aktion mit der Projekt-Öffnen-Funktion
project_action.triggered.connect(
lambda checked, proj=project: self.open_existing_project(proj)
)
projects_menu.addAction(project_action)
# Setze das Untermenü für die Aktion
self.ui.actionVorhandene_Projekte.setMenu(projects_menu)
print(f"Projekte-Menü initialisiert mit {len(app_settings.pdf_projects)} Projekten")
2025-08-10 17:32:22 +02:00
def open_existing_project(self, project: Project):
2025-06-22 11:58:57 +02:00
"""
Öffnet ein vorhandenes Projekt.
Args:
project: Das zu öffnende PdfProject-Objekt
"""
print(f"Öffne Projekt: {project.name}")
print(f"Projekt-Ordner: {project.project_dir}")
2025-08-10 14:03:15 +02:00
self.project = project
try:
# Prüfe ob project.yaml existiert und nicht leer ist
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
2025-08-10 17:32:22 +02:00
self.pdf_project = ProjectData.readSettings(project_dir=project.project_dir)
print(f"Projekt-Einstellungen aus {project_yaml_path} geladen!")
else:
# Erstelle Standard-Projekt-Einstellungen wenn Datei leer oder nicht vorhanden
print("project.yaml ist leer oder nicht vorhanden, erstelle Standard-Einstellungen")
2025-08-10 17:32:22 +02:00
self.pdf_project = ProjectData()
# Speichere die Standard-Einstellungen in die project.yaml
self.pdf_project.writeSettings(project_dir=project.project_dir)
print(f"Standard-Projekt-Einstellungen in {project_yaml_path} gespeichert")
# Lade die Nodes in das TreeWidget
self._load_nodes_to_tree()
# Starte Hash-Berechnung für alle XML-Dateien
self._start_xml_hash_calculation()
except Exception as e:
print(f"Fehler beim Laden des Projekts '{project.name}': {e}")
# Fallback: Erstelle Standard-Einstellungen
try:
2025-08-10 17:32:22 +02:00
self.pdf_project = ProjectData()
print("Fallback: Standard-Projekt-Einstellungen erstellt")
# Auch bei Fallback die Nodes laden
self._load_nodes_to_tree()
except Exception as fallback_error:
print(f"Fehler beim Erstellen der Fallback-Einstellungen: {fallback_error}")
2025-06-22 11:58:57 +02:00
2025-05-27 20:48:21 +02:00
def change_theme(self, theme_name):
"""
Wechselt das Theme der Anwendung.
2025-05-28 19:30:37 +02:00
2025-05-27 20:48:21 +02:00
Args:
theme_name: Name des zu verwendenden Themes
"""
print(f"Wechsle zu Theme: {theme_name}")
2025-05-28 19:30:37 +02:00
2025-05-27 20:48:21 +02:00
try:
# Erstelle den neuen Style
style = QStyleFactory.create(theme_name)
if style:
# Wende den neuen Style auf die Anwendung an
QApplication.setStyle(style)
2025-05-28 19:30:37 +02:00
2025-05-27 20:48:21 +02:00
# Aktualisiere die Checkmarks im Menü
for action in self.ui.menuThema.actions():
action.setChecked(action.text() == theme_name)
2025-05-28 19:30:37 +02:00
2025-05-27 20:48:21 +02:00
print(f"Theme erfolgreich gewechselt zu: {theme_name}")
app_settings.theme = theme_name
app_settings.save()
2025-05-27 20:48:21 +02:00
else:
print(f"Fehler: Theme '{theme_name}' konnte nicht erstellt werden")
2025-05-28 19:30:37 +02:00
2025-05-27 20:48:21 +02:00
except Exception as e:
print(f"Fehler beim Wechseln des Themes: {e}")
2025-05-21 20:26:03 +02:00
def _load_images(self):
2025-05-29 16:30:01 +02:00
"""Lädt PDF-Thumbnails und bereitet On-Demand-Rendering vor."""
# Entferne bestehende Widgets aus den Layouts
self._clear_layout(self.ui.verticalLayout_2)
self._clear_layout(self.ui.verticalLayout_3)
2025-05-23 11:09:47 +02:00
2025-05-22 21:05:22 +02:00
# Dicts zurücksetzen
2025-05-29 16:30:01 +02:00
self.thumbnail_to_page = {}
self.pdf_documents = {}
2025-05-29 19:03:19 +02:00
self.current_rendered_pixmaps = None
2025-05-23 11:09:47 +02:00
2025-05-27 18:31:45 +02:00
# Basis-Pfad zu den PDF-Ordnern
2025-05-21 20:26:03 +02:00
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
2025-06-09 17:17:53 +02:00
pdf_base_dir = os.path.join(base_dir, "ui", "res", "pdf")
2025-05-28 19:30:37 +02:00
2025-05-27 18:31:45 +02:00
diff_dir = os.path.join(pdf_base_dir, "diff")
ref_dir = os.path.join(pdf_base_dir, "ref")
new_dir = os.path.join(pdf_base_dir, "new")
# Prüfe ob diff-Ordner existiert und PDF-Dateien enthält
if not os.path.exists(diff_dir):
print(f"Diff-Ordner nicht gefunden: {diff_dir}")
return
2025-05-23 20:38:19 +02:00
2025-05-27 18:31:45 +02:00
# Finde alle PDF-Dateien im diff-Ordner
diff_pdfs = glob.glob(os.path.join(diff_dir, "*.pdf"))
if not diff_pdfs:
print("Keine PDF-Dateien im diff-Ordner gefunden")
2025-05-23 20:38:19 +02:00
return
2025-05-27 18:31:45 +02:00
print(f"Gefundene PDF-Dateien im diff-Ordner: {diff_pdfs}")
# Für jede PDF-Datei im diff-Ordner
for diff_pdf_path in diff_pdfs:
pdf_filename = os.path.basename(diff_pdf_path)
ref_pdf_path = os.path.join(ref_dir, pdf_filename)
new_pdf_path = os.path.join(new_dir, pdf_filename)
# Prüfe ob gleichnamige PDFs in ref und new existieren
if not os.path.exists(ref_pdf_path):
print(f"Referenz-PDF nicht gefunden: {ref_pdf_path}")
continue
if not os.path.exists(new_pdf_path):
print(f"New-PDF nicht gefunden: {new_pdf_path}")
continue
try:
2025-05-31 21:27:58 +02:00
# Alle drei PDF-Dateien öffnen mit QtPdf
diff_doc = QPdfDocument()
ref_doc = QPdfDocument()
new_doc = QPdfDocument()
# PDF-Dateien laden
diff_doc.load(diff_pdf_path)
ref_doc.load(ref_pdf_path)
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):
print(f"Fehler beim Laden der PDFs für {pdf_filename}")
continue
2025-05-27 18:31:45 +02:00
2025-05-29 16:30:01 +02:00
# PDF-Dokumente für später speichern
self.pdf_documents[pdf_filename] = {
'diff': diff_doc,
'ref': ref_doc,
'new': new_doc
}
2025-05-27 18:31:45 +02:00
print(f"PDFs geladen: {pdf_filename}")
2025-05-31 21:27:58 +02:00
print(f" diff: {diff_doc.pageCount()} Seiten")
print(f" ref: {ref_doc.pageCount()} Seiten")
print(f" new: {new_doc.pageCount()} Seiten")
# Nehme die Seitenzahl der diff-PDF als Basis
max_pages = diff_doc.pageCount()
2025-05-27 18:31:45 +02:00
2025-05-31 21:27:58 +02:00
# Performance-Test: Messe Thumbnail-Erstellung
start_time = time.time()
2025-05-27 18:31:45 +02:00
2025-05-29 16:30:01 +02:00
# Erstelle nur Thumbnails (keine Vollbilder)
2025-05-27 18:31:45 +02:00
for page_num in range(max_pages):
2025-05-29 16:30:01 +02:00
# Nur diff-Seite für Thumbnail rendern
2025-05-31 21:27:58 +02:00
page_size = diff_doc.pagePointSize(page_num)
# Skalierung für Thumbnail (entspricht ca. Matrix(1.0, 1.0) in PyMuPDF)
scale_factor = 200.0 / page_size.width() # 200 Pixel Breite für Thumbnail
# Seite rendern
page_image = diff_doc.render(page_num, QSize(int(page_size.width() * scale_factor),
int(page_size.height() * scale_factor)))
diff_pixmap = QPixmap.fromImage(page_image)
2025-05-27 18:31:45 +02:00
2025-05-29 16:30:01 +02:00
# Thumbnail erstellen und zur linken Spalte hinzufügen
2025-05-27 18:31:45 +02:00
thumbnail = QLabel()
thumbnail.setObjectName(f"thumbnail_{pdf_filename}_page_{page_num + 1}")
thumbnail.setPixmap(diff_pixmap.scaledToWidth(200, Qt.TransformationMode.SmoothTransformation))
thumbnail.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
thumbnail.setMouseTracking(True)
self.ui.verticalLayout_2.addWidget(thumbnail)
2025-05-28 19:30:37 +02:00
2025-05-29 16:30:01 +02:00
# Seitennummer für Thumbnail anzeigen
thumbnail_info = QLabel(f"Seite {page_num + 1}")
thumbnail_info.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.ui.verticalLayout_2.addWidget(thumbnail_info)
2025-05-27 18:31:45 +02:00
2025-05-29 16:30:01 +02:00
# Beziehung zwischen Thumbnail und Seitennummer speichern
self.thumbnail_to_page[thumbnail] = {
'pdf_filename': pdf_filename,
'page_num': page_num
}
2025-05-27 18:31:45 +02:00
# Click-Event für das Thumbnail einrichten
thumbnail.mousePressEvent = lambda event, t=thumbnail: self.on_thumbnail_clicked(event, t)
2025-05-28 19:30:37 +02:00
2025-05-29 16:30:01 +02:00
print(f"Thumbnail für Seite {page_num + 1} erstellt")
2025-05-28 19:30:37 +02:00
2025-05-31 21:27:58 +02:00
thumbnail_time = time.time() - start_time
print(f"Performance: {max_pages} Thumbnails in {thumbnail_time:.3f}s")
2025-05-29 16:30:01 +02:00
# Setze die erste PDF als aktuelle PDF
if self.current_pdf is None:
self.current_pdf = pdf_filename
2025-05-27 18:31:45 +02:00
except Exception as e:
print(f"Fehler beim Laden der PDFs: {e}")
2025-06-16 20:30:56 +02:00
# Erstelle das eine Vollbild-Label für die rechte Spalte (immer erstellen)
if self.fullsize_label is None:
self.fullsize_label = QLabel()
self.fullsize_label.setObjectName("fullsize_current_page")
self.fullsize_label.setAlignment(Qt.AlignmentFlag.AlignHCenter)
self.fullsize_label.setCursor(QCursor(Qt.CursorShape.OpenHandCursor))
self.ui.verticalLayout_3.addWidget(self.fullsize_label)
2025-05-29 16:30:01 +02:00
2025-06-16 20:30:56 +02:00
# 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)
2025-05-29 16:30:01 +02:00
# Zeige die erste Seite initial an
if self.current_pdf:
self.render_and_display_page(self.current_pdf, 0)
def render_and_display_page(self, pdf_filename, page_num):
"""
Rendert und zeigt eine spezifische Seite in der Vollansicht an.
2025-05-29 19:03:19 +02:00
Cached die gerenderten Pixmaps für bessere Performance.
2025-05-29 16:30:01 +02:00
Args:
pdf_filename: Name der PDF-Datei
page_num: Seitennummer (0-basiert)
"""
print(f"Rendere Seite {page_num + 1} von {pdf_filename}")
if pdf_filename not in self.pdf_documents:
print(f"PDF-Dokument {pdf_filename} nicht gefunden")
return
2025-05-31 21:27:58 +02:00
start_time = time.time()
2025-05-29 16:30:01 +02:00
try:
docs = self.pdf_documents[pdf_filename]
2025-05-29 21:21:18 +02:00
# Diff-Seite laden (bestimmt die Abmessungen)
2025-05-31 21:27:58 +02:00
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))
2025-05-29 16:30:01 +02:00
2025-05-29 21:21:18 +02:00
# Diff-Seite rendern (immer vorhanden)
2025-05-31 21:27:58 +02:00
diff_image = diff_doc.render(page_num, render_size)
diff_pixmap = QPixmap.fromImage(diff_image)
2025-05-29 21:21:18 +02:00
2025-05-31 21:27:58 +02:00
# Ermittle die Abmessungen für weiße Seiten
2025-05-29 21:21:18 +02:00
diff_width = diff_pixmap.width()
diff_height = diff_pixmap.height()
# Ref-Seite prüfen und rendern oder weiße Seite erstellen
2025-05-31 21:27:58 +02:00
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)
2025-05-29 21:21:18 +02:00
print(f"Ref-Seite {page_num + 1} gerendert")
else:
# Erstelle weiße Seite mit gleichen Abmessungen wie Diff-Seite
ref_pixmap = QPixmap(diff_width, diff_height)
ref_pixmap.fill(Qt.GlobalColor.white)
2025-05-31 21:27:58 +02:00
print(f"Weiße Ref-Seite {page_num + 1} erstellt")
2025-05-29 21:21:18 +02:00
# New-Seite prüfen und rendern oder weiße Seite erstellen
2025-05-31 21:27:58 +02:00
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)
2025-05-29 21:21:18 +02:00
print(f"New-Seite {page_num + 1} gerendert")
else:
# Erstelle weiße Seite mit gleichen Abmessungen wie Diff-Seite
new_pixmap = QPixmap(diff_width, diff_height)
new_pixmap.fill(Qt.GlobalColor.white)
2025-05-31 21:27:58 +02:00
print(f"Weiße New-Seite {page_num + 1} erstellt")
2025-05-29 16:30:01 +02:00
2025-05-29 19:03:19 +02:00
# Cache die gerenderten Pixmaps für schnelle Alpha/Zoom-Operationen
self.current_rendered_pixmaps = {
'ref': ref_pixmap,
'diff': diff_pixmap,
'new': new_pixmap
}
2025-05-29 16:30:01 +02:00
# Aktualisiere aktuelle Seite
self.current_page = page_num
self.current_pdf = pdf_filename
2025-05-29 19:03:19 +02:00
# Zeige das Bild mit aktuellem Alpha- und Zoom-Wert an
self.update_current_display()
2025-05-31 21:27:58 +02:00
render_time = time.time() - start_time
print(f"Performance: Seite {page_num + 1} gerendert in {render_time:.3f}s")
2025-05-29 16:30:01 +02:00
except Exception as e:
print(f"Fehler beim Rendern der Seite {page_num + 1}: {e}")
2025-05-23 11:09:47 +02:00
2025-05-29 19:03:19 +02:00
def update_current_display(self):
"""
Aktualisiert die Anzeige der aktuellen Seite basierend auf gecachten Pixmaps.
Verwendet für Alpha- und Zoom-Änderungen ohne erneutes PDF-Rendering.
"""
if not self.current_rendered_pixmaps:
print("Keine gerenderten Pixmaps verfügbar")
return
2025-06-16 20:30:56 +02:00
if self.fullsize_label is None:
print("Fullsize-Label ist nicht verfügbar")
return
2025-05-29 19:03:19 +02:00
# 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']
# Erstelle das überlagerte Bild mit aktuellem Alpha-Wert
alpha_value = self.ui.alpha.value()
layered_pixmap = self.create_layered_pixmap(ref_pixmap, diff_pixmap, new_pixmap, alpha_value)
# Wende aktuellen Zoom an
zoom_factor = self.current_zoom / 100.0
if zoom_factor != 1.0:
new_width = int(layered_pixmap.width() * zoom_factor)
layered_pixmap = layered_pixmap.scaledToWidth(new_width, Qt.TransformationMode.SmoothTransformation)
# Setze das überlagerte Bild
self.fullsize_label.setPixmap(layered_pixmap)
self.fullsize_label.setAlignment(Qt.AlignmentFlag.AlignHCenter)
2025-05-27 18:31:45 +02:00
def create_layered_pixmap(self, ref_pixmap, diff_pixmap, new_pixmap, alpha_value):
"""
Erstellt ein übergelagertes Pixmap basierend auf dem Alpha-Wert.
2025-05-28 19:30:37 +02:00
2025-05-27 18:31:45 +02:00
Args:
ref_pixmap: Unterste Ebene (ref)
diff_pixmap: Mittlere Ebene (diff)
new_pixmap: Oberste Ebene (new)
alpha_value: Alpha-Wert (-100 bis 100)
2025-05-28 19:30:37 +02:00
2025-05-27 18:31:45 +02:00
Returns:
QPixmap: Das überlagerte Bild
"""
# Verwende die Größe des größten Bildes
max_width = max(ref_pixmap.width(), diff_pixmap.width(), new_pixmap.width())
max_height = max(ref_pixmap.height(), diff_pixmap.height(), new_pixmap.height())
2025-05-28 19:30:37 +02:00
2025-05-27 18:31:45 +02:00
# Erstelle ein leeres Pixmap für das Ergebnis
result = QPixmap(max_width, max_height)
result.fill(Qt.GlobalColor.white)
2025-05-28 19:30:37 +02:00
2025-05-27 18:31:45 +02:00
painter = QPainter(result)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
2025-05-28 19:30:37 +02:00
2025-05-27 18:31:45 +02:00
if alpha_value <= 0:
# Alpha von -100 bis 0: Übergang von ref zu diff
2025-06-15 19:52:24 +02:00
ref_opacity = 1.0 # - (alpha_value + 100) / 100.0
2025-05-27 18:31:45 +02:00
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
2025-06-15 19:52:24 +02:00
diff_opacity = 1.0 # - alpha_value / 100.0
2025-05-27 18:31:45 +02:00
new_opacity = alpha_value / 100.0
2025-05-28 19:30:37 +02:00
2025-05-27 18:31:45 +02:00
# Zeichne die Ebenen mit entsprechender Transparenz
if ref_opacity > 0:
painter.setOpacity(ref_opacity)
painter.drawPixmap(0, 0, ref_pixmap)
2025-05-28 19:30:37 +02:00
2025-05-27 18:31:45 +02:00
if diff_opacity > 0:
painter.setOpacity(diff_opacity)
painter.drawPixmap(0, 0, diff_pixmap)
2025-05-28 19:30:37 +02:00
2025-05-27 18:31:45 +02:00
if new_opacity > 0:
painter.setOpacity(new_opacity)
painter.drawPixmap(0, 0, new_pixmap)
2025-05-28 19:30:37 +02:00
2025-05-27 18:31:45 +02:00
painter.end()
return result
2025-05-23 11:09:47 +02:00
2025-05-31 21:27:58 +02:00
def _clear_layout(self, layout):
"""Entfernt alle Widgets aus einem Layout."""
if layout is not None:
while layout.count():
item = layout.takeAt(0)
widget = item.widget()
if widget is not None:
widget.deleteLater()
def _connect_signals(self):
"""Verbindet Signale mit den entsprechenden Slots."""
# Button-Klicks verbinden
self.ui.pushButton.clicked.connect(self.on_button_clicked)
# Zoom-Slider verbinden
self.ui.zoom.valueChanged.connect(self.apply_zoom)
self.ui.zoom.mouseDoubleClickEvent = lambda event: self.ui.zoom.setValue(100)
2025-05-31 21:27:58 +02:00
# Alpha-Slider verbinden
self.ui.alpha.valueChanged.connect(self.on_alpha_changed)
self.ui.alpha.mouseDoubleClickEvent = lambda event: self.ui.alpha.setValue(0)
# Menü-Aktionen verbinden
2025-06-16 20:30:56 +02:00
self.ui.actionNeu.triggered.connect(self.open_new_project_dialog)
self.ui.actionEinstellungen.triggered.connect(self.open_settings_dialog)
2025-08-10 14:03:15 +02:00
# Button "lade aus FN2" verbinden
self.ui.pB_lade_aus_fn2.clicked.connect(self.on_load_from_fn2_clicked)
2025-05-31 21:27:58 +02:00
2025-08-03 16:31:38 +02:00
def _setup_tree_context_menu(self):
"""Richtet das Kontextmenü für das TreeWidget ein."""
# Aktiviere Kontextmenü für das TreeWidget
self.ui.treeWidget.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.ui.treeWidget.customContextMenuRequested.connect(self._show_tree_context_menu)
print("Kontextmenü für TreeWidget eingerichtet")
2025-08-14 20:32:29 +02:00
def _setup_tree_widget_styling(self):
"""Richtet das Styling für das TreeWidget ein, um den vertikalen Abstand zu vergrößern."""
try:
# Stylesheet für größeren vertikalen Abstand zwischen Items
tree_stylesheet = """
QTreeWidget::item {
padding: 4px 4px;
}
QTreeWidget::item:selected {
background-color: palette(highlight);
color: palette(highlighted-text);
}
QTreeWidget::item:hover {
background-color: palette(alternate-base);
}
QTreeWidget::branch {
/*margin: 2px 0px;*/
}
"""
# Wende das Stylesheet auf das TreeWidget an
self.ui.treeWidget.setStyleSheet(tree_stylesheet)
print("TreeWidget Styling für größeren vertikalen Abstand angewendet")
except Exception as e:
print(f"Fehler beim Anwenden des TreeWidget-Stylings: {e}")
2025-08-03 16:31:38 +02:00
def _show_tree_context_menu(self, position):
"""
Zeigt das Kontextmenü für das TreeWidget an.
Args:
position: Position des Rechtsklicks
"""
# Hole das Item an der Position
item = self.ui.treeWidget.itemAt(position)
if not item:
# Kein Item gefunden - zeige Kontextmenü für Root-Elemente
node_type = "Unknown"
context_menu = self._create_context_menu_for_type(node_type, None)
else:
# Bestimme den Node-Typ basierend auf dem Item
node_type = self._get_node_type_from_item(item)
# Erstelle das entsprechende Kontextmenü
context_menu = self._create_context_menu_for_type(node_type, item)
2025-08-03 16:31:38 +02:00
if context_menu:
# Zeige das Kontextmenü an der globalen Position
global_pos = self.ui.treeWidget.mapToGlobal(position)
context_menu.exec(global_pos)
def _get_node_type_from_item(self, item):
"""
Bestimmt den Node-Typ basierend auf dem TreeWidgetItem.
Args:
item: Das TreeWidgetItem
Returns:
str: Der Node-Typ ('TreeNode', 'XslFile', 'XmlFile' oder 'Unknown')
"""
try:
# Prüfe ob das Item ein Parent hat (dann ist es ein Child-Item)
parent_item = item.parent()
if parent_item:
# Child-Item - prüfe ob es ein XML-File ist
text = item.text(0)
if text.startswith("XML:"):
return "XmlFile"
else:
# Könnte ein TreeNode-Child oder XslFile-Child sein
# Prüfe den Parent-Typ
parent_type = self._get_node_type_from_item(parent_item)
if parent_type == "XslFile":
return "XmlFile"
else:
# Rekursiv bestimmen basierend auf gespeicherten Daten
return self._determine_node_type_from_data(item)
else:
# Root-Item - bestimme Typ basierend auf gespeicherten Daten
return self._determine_node_type_from_data(item)
except Exception as e:
print(f"Fehler beim Bestimmen des Node-Typs: {e}")
return "Unknown"
def _determine_node_type_from_data(self, item):
"""
Bestimmt den Node-Typ basierend auf den gespeicherten Daten im Item.
Args:
item: Das TreeWidgetItem
Returns:
str: Der Node-Typ ('TreeNode', 'XslFile' oder 'Unknown')
"""
try:
2025-08-12 21:13:00 +02:00
# Hole das gespeicherte Node-Objekt direkt
node = item.data(0, Qt.ItemDataRole.UserRole)
if not node:
2025-08-03 16:31:38 +02:00
return "Unknown"
2025-08-12 21:13:00 +02:00
# Bestimme den Typ direkt vom Node-Objekt
if isinstance(node, TreeNode):
return "TreeNode"
elif isinstance(node, XslFile):
return "XslFile"
elif isinstance(node, XmlFile):
return "XmlFile"
2025-08-03 16:31:38 +02:00
return "Unknown"
except Exception as e:
print(f"Fehler beim Bestimmen des Node-Typs aus Daten: {e}")
return "Unknown"
def _find_node_by_id(self, nodes, target_id):
"""
Sucht rekursiv nach einem Node mit der angegebenen ID.
Args:
nodes: Liste der Nodes zum Durchsuchen
target_id: Die zu suchende ID
Returns:
TreeNode|XslFile|None: Der gefundene Node oder None
"""
for node in nodes:
if node.id == target_id:
return node
2025-08-14 20:37:03 +02:00
# Rekursiv in Knotenn suchen (nur bei TreeNode)
2025-08-03 16:31:38 +02:00
if isinstance(node, TreeNode) and node.children:
found = self._find_node_by_id(node.children, target_id)
if found:
return found
return None
def _create_context_menu_for_type(self, node_type, item):
"""
Erstellt das Kontextmenü für den angegebenen Node-Typ.
Args:
node_type: Der Typ des Nodes ('TreeNode', 'XslFile', 'XmlFile')
item: Das TreeWidgetItem
Returns:
QMenu: Das erstellte Kontextmenü oder None
"""
try:
menu = QMenu(self)
if node_type == "TreeNode":
# Kontextmenü für TreeNode
action_add_child = QAction("Unterknoten hinzufügen", self)
2025-08-03 20:31:32 +02:00
action_add_child.setIcon(QIcon(QIcon.fromTheme(u"folder-new")))
2025-08-03 16:31:38 +02:00
action_add_child.triggered.connect(lambda: self._add_tree_node_child(item))
menu.addAction(action_add_child)
action_add_xsl = QAction("XSL-Datei hinzufügen", self)
2025-08-03 20:31:32 +02:00
action_add_xsl.setIcon(QIcon(QIcon.fromTheme("document-new")))
2025-08-03 16:31:38 +02:00
action_add_xsl.triggered.connect(lambda: self._add_xsl_file_to_node(item))
menu.addAction(action_add_xsl)
menu.addSeparator()
action_edit = QAction("Bearbeiten", self)
2025-08-03 20:31:32 +02:00
action_edit.setIcon(QIcon(QIcon.fromTheme(QIcon.ThemeIcon.DocumentProperties)))
2025-08-03 16:31:38 +02:00
action_edit.triggered.connect(lambda: self._edit_tree_node(item))
menu.addAction(action_edit)
action_delete = QAction("Löschen", self)
2025-08-03 20:31:32 +02:00
action_delete.setIcon(QIcon(QIcon.fromTheme(QIcon.ThemeIcon.EditDelete)))
2025-08-03 16:31:38 +02:00
action_delete.triggered.connect(lambda: self._delete_tree_node(item))
menu.addAction(action_delete)
elif node_type == "XslFile":
# Kontextmenü für XslFile
action_add_xml = QAction("XML-Datei hinzufügen", self)
2025-08-03 20:31:32 +02:00
action_add_xml.setIcon(QIcon(QIcon.fromTheme("document-new")))
2025-08-03 16:31:38 +02:00
action_add_xml.triggered.connect(lambda: self._add_xml_file_to_xsl(item))
menu.addAction(action_add_xml)
menu.addSeparator()
action_edit = QAction("Bearbeiten", self)
2025-08-03 20:31:32 +02:00
action_edit.setIcon(QIcon(QIcon.fromTheme(QIcon.ThemeIcon.DocumentProperties)))
2025-08-03 16:31:38 +02:00
action_edit.triggered.connect(lambda: self._edit_xsl_file(item))
menu.addAction(action_edit)
action_delete = QAction("Löschen", self)
2025-08-03 20:31:32 +02:00
action_delete.setIcon(QIcon(QIcon.fromTheme(QIcon.ThemeIcon.EditDelete)))
2025-08-03 16:31:38 +02:00
action_delete.triggered.connect(lambda: self._delete_xsl_file(item))
menu.addAction(action_delete)
elif node_type == "XmlFile":
# Kontextmenü für XmlFile
action_edit = QAction("Bearbeiten", self)
2025-08-03 20:31:32 +02:00
action_edit.setIcon(QIcon(QIcon.fromTheme(QIcon.ThemeIcon.DocumentProperties)))
2025-08-03 16:31:38 +02:00
action_edit.triggered.connect(lambda: self._edit_xml_file(item))
menu.addAction(action_edit)
action_delete = QAction("Löschen", self)
2025-08-03 20:31:32 +02:00
action_delete.setIcon(QIcon(QIcon.fromTheme(QIcon.ThemeIcon.EditDelete)))
2025-08-03 16:31:38 +02:00
action_delete.triggered.connect(lambda: self._delete_xml_file(item))
menu.addAction(action_delete)
else:
# Unbekannter Typ oder leerer Bereich - Menü für Root-Elemente
action_add_tree_node = QAction("Unterknoten hinzufügen", self)
2025-08-03 20:31:32 +02:00
action_add_tree_node.setIcon(QIcon(QIcon.fromTheme(u"folder-new")))
action_add_tree_node.triggered.connect(lambda: self._add_root_tree_node())
menu.addAction(action_add_tree_node)
2025-08-03 16:31:38 +02:00
return menu
except Exception as e:
print(f"Fehler beim Erstellen des Kontextmenüs: {e}")
return None
2025-05-31 21:27:58 +02:00
def on_alpha_changed(self, alpha_value):
"""
Wird ausgeführt, wenn der Alpha-Slider geändert wird.
Optimiert: Verwendet gecachte Pixmaps statt erneutes PDF-Rendering.
Args:
alpha_value: Der neue Alpha-Wert (-100 bis 100)
"""
print(f"Alpha geändert auf {alpha_value}")
start_time = time.time()
# Verwende gecachte Pixmaps für schnelle Alpha-Änderungen
self.update_current_display()
alpha_time = time.time() - start_time
print(f"Alpha-Update in {alpha_time:.6f}s")
def open_settings_dialog(self):
"""Öffnet den Einstellungen-Dialog."""
try:
# Erstelle und zeige den Dialog
dialog = AppSettingsDlg(self, app_settings)
if dialog.exec() == AppSettingsDlg.DialogCode.Accepted:
# Einstellungen wurden gespeichert, hier könnten weitere Aktionen folgen
print("Einstellungen wurden gespeichert")
except Exception as e:
print(f"Fehler beim Öffnen des Einstellungen-Dialogs: {e}")
2025-06-16 20:30:56 +02:00
def open_new_project_dialog(self):
"""Öffnet Pdf-Projekt-Dialog."""
try:
# Erstelle und zeige den PdfProject-Dialog
dialog = PdfProjectDlg(self)
if dialog.exec() == PdfProjectDlg.DialogCode.Accepted:
# Hole die Projektdaten aus dem Dialog
project_data = dialog.get_project_data()
# Erstelle neue ID für das Projekt
new_id = max([p.id for p in app_settings.pdf_projects], default=0) + 1
# Erstelle PdfProject-Objekt
2025-08-10 17:32:22 +02:00
new_project = Project(
2025-06-16 20:30:56 +02:00
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,
2025-07-14 21:00:06 +02:00
postgre_sql_db_id=project_data['postgre_sql_db_id'] if project_data['postgre_sql_db_id'] != -1 else 1,
2025-06-16 20:30:56 +02:00
)
# Erstelle Projekt-Ordnerstruktur
self._create_project_structure(new_project)
# Füge das neue Projekt zu app_settings hinzu
app_settings.pdf_projects.append(new_project)
# Speichere app_settings
app_settings.save()
print(f"Neues PDF-Projekt '{project_data['name']}' wurde erstellt und gespeichert")
print(f"Projekt-ID: {new_id}")
print(f"Projekt-Ordner: {project_data['project_dir']}")
2025-06-22 11:58:57 +02:00
# Aktualisiere das Projekte-Menü
self._setup_projects_menu()
2025-06-16 20:30:56 +02:00
except Exception as e:
print(f"Fehler beim Erstellen des neuen Projekts: {e}")
2025-08-10 17:32:22 +02:00
def _create_project_structure(self, project: Project):
2025-06-16 20:30:56 +02:00
"""
Erstellt die Ordnerstruktur und project.yaml-Datei für ein neues Projekt.
Args:
project: Das PdfProject-Objekt
"""
try:
project_dir = Path(project.project_dir)
# Erstelle Unterordner
subdirs = ['xml', 'new', 'diff', 'ref', 'tmp']
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'
# Erstelle Standard-Projekt-Einstellungen und speichere sie
2025-06-16 20:30:56 +02:00
if not project_yaml_path.exists():
# Erstelle Standard-PdfProjectSettings
2025-08-10 17:32:22 +02:00
default_settings = ProjectData()
# Speichere die Standard-Einstellungen in die project.yaml
default_settings.writeSettings(project_dir=project_dir)
print(f"project.yaml mit Standard-Einstellungen erstellt: {project_yaml_path}")
else:
print(f"project.yaml existiert bereits: {project_yaml_path}")
2025-06-16 20:30:56 +02:00
except Exception as e:
print(f"Fehler beim Erstellen der Projekt-Struktur: {e}")
raise
def on_button_clicked(self):
"""Wird ausgeführt, wenn der Button geklickt wird."""
print("Button wurde geklickt!")
2025-05-23 11:09:47 +02:00
def on_thumbnail_clicked(self, event, thumbnail):
"""
Wird ausgeführt, wenn ein Thumbnail angeklickt wird.
2025-05-23 11:09:47 +02:00
Args:
event: Das Maus-Event
thumbnail: Das geklickte Thumbnail-Label
"""
2025-05-29 16:30:01 +02:00
page_info = self.thumbnail_to_page.get(thumbnail)
if page_info:
pdf_filename = page_info['pdf_filename']
page_num = page_info['page_num']
print(f"Thumbnail für Seite {page_num + 1} von {pdf_filename} wurde angeklickt")
# Rendere und zeige die gewählte Seite an
self.render_and_display_page(pdf_filename, page_num)
2025-05-23 11:09:47 +02:00
2025-05-22 21:05:22 +02:00
def apply_zoom(self, zoom_value):
"""
2025-05-29 16:30:01 +02:00
Wendet den Zoom-Faktor auf das aktuelle Bild an.
2025-05-29 19:03:19 +02:00
Optimiert: Verwendet gecachte Pixmaps statt erneutes PDF-Rendering.
2025-05-23 11:09:47 +02:00
2025-05-22 21:05:22 +02:00
Args:
zoom_value: Der neue Zoom-Wert (in Prozent)
"""
self.current_zoom = zoom_value
print(f"Zoom geändert auf {zoom_value}%")
2025-05-23 11:09:47 +02:00
2025-05-29 19:03:19 +02:00
# Verwende gecachte Pixmaps für schnelle Zoom-Änderungen
self.update_current_display()
2025-05-23 21:26:50 +02:00
def on_fullsize_mouse_press(self, event, fullsize_label):
2025-05-31 21:27:58 +02:00
"""Wird ausgeführt, wenn die Maustaste auf einem großen Bild gedrückt wird."""
2025-05-23 21:26:50 +02:00
if event.button() == Qt.MouseButton.LeftButton:
self.is_dragging = True
self.last_drag_position = event.globalPosition().toPoint()
fullsize_label.setCursor(QCursor(Qt.CursorShape.ClosedHandCursor))
def on_fullsize_mouse_move(self, event, fullsize_label):
2025-05-31 21:27:58 +02:00
"""Wird ausgeführt, wenn die Maus über einem großen Bild bewegt wird."""
2025-05-23 21:26:50 +02:00
if self.is_dragging and self.last_drag_position is not None:
current_pos = event.globalPosition().toPoint()
delta = current_pos - self.last_drag_position
2025-05-28 19:30:37 +02:00
2025-05-23 21:26:50 +02:00
if abs(delta.x()) >= self.drag_threshold or abs(delta.y()) >= self.drag_threshold:
v_scrollbar = self.ui.scrollArea_2.verticalScrollBar()
h_scrollbar = self.ui.scrollArea_2.horizontalScrollBar()
2025-05-28 19:30:37 +02:00
2025-05-23 21:26:50 +02:00
scroll_delta_y = int(-delta.y() * self.scroll_sensitivity)
scroll_delta_x = int(-delta.x() * self.scroll_sensitivity)
2025-05-28 19:30:37 +02:00
2025-05-23 21:26:50 +02:00
new_v_value = v_scrollbar.value() + scroll_delta_y
new_h_value = h_scrollbar.value() + scroll_delta_x
2025-05-28 19:30:37 +02:00
2025-05-23 21:26:50 +02:00
v_scrollbar.setValue(new_v_value)
h_scrollbar.setValue(new_h_value)
2025-05-28 19:30:37 +02:00
2025-05-23 21:26:50 +02:00
self.last_drag_position = current_pos
def on_fullsize_mouse_release(self, event, fullsize_label):
2025-05-31 21:27:58 +02:00
"""Wird ausgeführt, wenn die Maustaste auf einem großen Bild losgelassen wird."""
2025-05-23 21:26:50 +02:00
if event.button() == Qt.MouseButton.LeftButton:
self.is_dragging = False
self.last_drag_position = None
fullsize_label.setCursor(QCursor(Qt.CursorShape.OpenHandCursor))
2025-05-29 16:30:01 +02:00
def _load_nodes_to_tree(self):
"""
Lädt die Nodes aus den Projekt-Einstellungen in das TreeWidget.
2025-09-19 20:29:56 +02:00
Sortiert die Items alphabetisch nach ihrer ID.
"""
print("Lade Nodes in TreeWidget...")
try:
# TreeWidget leeren
self.ui.treeWidget.clear()
# Prüfe ob pdf_project existiert und Nodes hat
if not hasattr(self, 'pdf_project') or not self.pdf_project:
print("Keine Projekt-Einstellungen verfügbar")
return
if not self.pdf_project.nodes:
print("Keine Nodes in den Projekt-Einstellungen gefunden")
return
2025-09-19 20:29:56 +02:00
# Sortiere Root-Nodes alphabetisch nach ID
sorted_nodes = sorted(self.pdf_project.nodes, key=lambda node: node.id)
# Lade alle Root-Nodes (sortiert)
for node in sorted_nodes:
tree_item = self._create_tree_item_from_node(node)
self.ui.treeWidget.addTopLevelItem(tree_item)
2025-09-19 20:29:56 +02:00
print(f"{len(self.pdf_project.nodes)} Root-Nodes in TreeWidget geladen (alphabetisch sortiert)")
except Exception as e:
print(f"Fehler beim Laden der Nodes in TreeWidget: {e}")
def _create_tree_item_from_node(self, node):
"""
Erstellt ein QTreeWidgetItem aus einem TreeNode oder XslFile.
Speichert die vollständigen Node-Daten für spätere Verwendung.
Args:
node: TreeNode oder XslFile Objekt
Returns:
QTreeWidgetItem: Das erstellte Tree-Item mit vollständigen Node-Daten
"""
try:
# Erstelle Tree-Item
item = QTreeWidgetItem()
2025-08-03 16:31:38 +02:00
# Setze die Bezeichnung in Spalte 0
bez_text = str(node.bez) if node.bez else ""
item.setText(0, bez_text)
# Speichere das komplette Node-Objekt als UserRole-Daten
# Dies ermöglicht späteren Zugriff auf alle Node-Eigenschaften
item.setData(0, Qt.ItemDataRole.UserRole, node)
# Setze zusätzliche Informationen in Spalte 1
if isinstance(node, TreeNode):
2025-08-14 20:37:03 +02:00
# TreeNode: Zeige Anzahl der Knoten
child_count = len(node.children) if node.children else 0
2025-08-14 20:37:03 +02:00
item.setText(1, f"{child_count} Knoten")
# Speichere zusätzlich die Node-ID in UserRole+1 für Kompatibilität
item.setData(0, Qt.ItemDataRole.UserRole + 1, node.id)
2025-09-19 20:29:56 +02:00
# Lade Knoten rekursiv (sortiert nach ID)
if node.children:
2025-09-19 20:29:56 +02:00
sorted_children = sorted(node.children, key=lambda child: child.id)
for child in sorted_children:
child_item = self._create_tree_item_from_node(child)
item.addChild(child_item)
elif isinstance(node, XslFile):
# XslFile: Zeige XSL-Datei-Pfad
item.setText(1, str(node.xsl_file))
# Speichere zusätzlich die Node-ID in UserRole+1 für Kompatibilität
item.setData(0, Qt.ItemDataRole.UserRole + 1, node.id)
2025-08-14 20:37:03 +02:00
# Lade XML-Dateien als Knoten
if node.xmls:
for xml in node.xmls:
xml_item = QTreeWidgetItem()
xml_item.setText(0, f"XML: {xml.xml.name}")
xml_item.setText(1, str(xml.xml))
# Speichere auch das XmlFile-Objekt für XML-Items
xml_item.setData(0, Qt.ItemDataRole.UserRole, xml)
xml_item.setData(0, Qt.ItemDataRole.UserRole + 1, f"xml_{xml.xml.name}")
item.addChild(xml_item)
return item
except Exception as e:
print(f"Fehler beim Erstellen des Tree-Items: {e}")
# Fallback: Erstelle einfaches Item
fallback_item = QTreeWidgetItem()
fallback_item.setText(0, "Fehler beim Laden")
fallback_item.setText(1, str(e))
return fallback_item
2025-08-03 16:31:38 +02:00
# Kontextmenü-Aktionen für TreeNode
def _add_tree_node_child(self, parent_item):
"""Fügt einen Unterknoten zu einem TreeNode hinzu."""
print(f"Unterknoten zu TreeNode hinzufügen: {parent_item.text(0)}")
# TODO: Dialog zum Eingeben der Node-Daten öffnen
def _add_xsl_file_to_node(self, parent_item):
"""Fügt eine XSL-Datei zu einem TreeNode hinzu."""
print(f"XSL-Datei zu TreeNode hinzufügen: {parent_item.text(0)}")
# TODO: Dialog zum Auswählen der XSL-Datei öffnen
def _edit_tree_node(self, item):
2025-08-12 20:56:26 +02:00
"""
Bearbeitet einen TreeNode.
Args:
item: Das TreeWidgetItem des TreeNode
"""
2025-08-03 16:31:38 +02:00
print(f"TreeNode bearbeiten: {item.text(0)}")
2025-08-12 20:56:26 +02:00
try:
# Hole das Node-Objekt aus dem TreeWidgetItem
node = item.data(0, Qt.ItemDataRole.UserRole)
if not node or not isinstance(node, TreeNode):
QMessageBox.warning(self, "Warnung", "Kein gültiger TreeNode gefunden.")
return
# Sammle Eltern-Parameter
parent_params = self._collect_parent_params(item)
# Erstelle und zeige den Dialog
dialog = TreeNodeEditDialog(self, node, parent_params)
if dialog.exec() == TreeNodeEditDialog.DialogCode.Accepted:
# Hole die bearbeiteten Daten
data = dialog.get_data()
if data:
# Aktualisiere den Node
node.bez = data['bez']
node.xslt_params = data['xslt_params']
print(f"TreeNode '{node.bez}' wurde aktualisiert")
print(f"XSLT-Parameter: {node.xslt_params}")
# Speichere die Änderungen
self._save_project_settings()
# Aktualisiere das TreeWidget
self._load_nodes_to_tree()
# QMessageBox.information(self, "Erfolg", "TreeNode wurde erfolgreich aktualisiert.")
2025-08-12 20:56:26 +02:00
except Exception as e:
error_msg = f"Fehler beim Bearbeiten des TreeNode: {str(e)}"
print(error_msg)
QMessageBox.critical(self, "Fehler", error_msg)
2025-08-03 16:31:38 +02:00
def _delete_tree_node(self, item):
"""Löscht einen TreeNode."""
print(f"TreeNode löschen: {item.text(0)}")
# TODO: Bestätigungsdialog und Löschung implementieren
# Kontextmenü-Aktionen für XslFile
def _add_xml_file_to_xsl(self, parent_item):
2025-08-10 17:32:22 +02:00
"""
Fügt eine XML-Datei zu einer XSL-Datei hinzu.
Args:
parent_item: Das TreeWidgetItem des XslFile-Nodes
"""
2025-08-03 16:31:38 +02:00
print(f"XML-Datei zu XslFile hinzufügen: {parent_item.text(0)}")
2025-08-10 17:32:22 +02:00
try:
# Prüfe ob ein Projekt geladen ist
if not hasattr(self, 'project') or not self.project:
QMessageBox.warning(self, "Warnung", "Kein Projekt geladen. Bitte öffnen Sie zuerst ein Projekt.")
return
if not hasattr(self, 'pdf_project') or not self.pdf_project:
QMessageBox.warning(self, "Warnung", "Keine Projekt-Einstellungen geladen.")
return
2025-08-12 21:13:00 +02:00
# Hole das XslFile-Node-Objekt direkt aus dem TreeWidgetItem
xsl_node = parent_item.data(0, Qt.ItemDataRole.UserRole)
2025-08-10 17:32:22 +02:00
if not xsl_node or not isinstance(xsl_node, XslFile):
2025-08-12 21:13:00 +02:00
QMessageBox.warning(self, "Warnung", "Keine gültige XSL-Datei-Node gefunden.")
2025-08-10 17:32:22 +02:00
return
# Öffne Datei-Dialog zum Auswählen der XML-Datei
xml_file_path, _ = QFileDialog.getOpenFileName(
self,
"XML-Datei auswählen",
"",
"XML-Dateien (*.xml);;Alle Dateien (*)"
)
if not xml_file_path:
# Benutzer hat abgebrochen
return
xml_file_path = Path(xml_file_path)
# Prüfe ob die Datei existiert
if not xml_file_path.exists():
QMessageBox.critical(self, "Fehler", f"Die ausgewählte XML-Datei existiert nicht:\n{xml_file_path}")
return
# Erstelle xml-Ordner im Projekt-Verzeichnis falls er nicht existiert
xml_dir = Path(self.project.project_dir) / "xml"
xml_dir.mkdir(parents=True, exist_ok=True)
# Bestimme den Ziel-Pfad in xml-Ordner
target_xml_path = xml_dir / xml_file_path.name
# Prüfe ob eine Datei mit gleichem Namen bereits existiert
if target_xml_path.exists():
reply = QMessageBox.question(
self,
"Datei existiert bereits",
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
)
if reply != QMessageBox.StandardButton.Yes:
return
# Kopiere die XML-Datei in den xml-Ordner
shutil.copy2(xml_file_path, target_xml_path)
print(f"XML-Datei kopiert: {xml_file_path} -> {target_xml_path}")
# Erstelle relatives Path zur XML-Datei (relativ zum xml-Ordner)
relative_xml_path = Path("xml") / xml_file_path.name
# Prüfe ob diese XML-Datei bereits in der XslFile-Node vorhanden ist
existing_xml = None
for xml_file in xsl_node.xmls:
if xml_file.xml == relative_xml_path:
existing_xml = xml_file
break
if existing_xml:
QMessageBox.information(
self,
"XML-Datei bereits vorhanden",
f"Die XML-Datei '{xml_file_path.name}' ist bereits in dieser XSL-Datei enthalten."
)
return
# Erstelle neues XmlFile-Objekt und füge es zur XslFile-Node hinzu
new_xml_file = XmlFile(xml=relative_xml_path)
xsl_node.xmls.append(new_xml_file)
print(f"XML-Datei '{xml_file_path.name}' zu XslFile-Node '{xsl_node.bez}' hinzugefügt")
# Berechne Hash für die neue XML-Datei
self._calculate_hash_for_xml_file(new_xml_file)
2025-08-10 17:32:22 +02:00
# Speichere die aktualisierten Projekt-Einstellungen
self._save_project_settings()
# Aktualisiere das TreeWidget
self._load_nodes_to_tree()
# QMessageBox.information(
# self,
# "Erfolg",
# f"XML-Datei '{xml_file_path.name}' wurde erfolgreich hinzugefügt und in den xml-Ordner kopiert."
# )
2025-08-10 17:32:22 +02:00
except Exception as e:
error_msg = f"Fehler beim Hinzufügen der XML-Datei: {str(e)}"
print(error_msg)
QMessageBox.critical(self, "Fehler", error_msg)
2025-08-03 16:31:38 +02:00
def _edit_xsl_file(self, item):
2025-08-12 20:56:26 +02:00
"""
Bearbeitet eine XSL-Datei.
Args:
item: Das TreeWidgetItem des XslFile
"""
2025-08-03 16:31:38 +02:00
print(f"XslFile bearbeiten: {item.text(0)}")
2025-08-12 20:56:26 +02:00
try:
# Hole das Node-Objekt aus dem TreeWidgetItem
node = item.data(0, Qt.ItemDataRole.UserRole)
if not node or not isinstance(node, XslFile):
QMessageBox.warning(self, "Warnung", "Keine gültige XSL-Datei gefunden.")
return
# Sammle Eltern-Parameter
parent_params = self._collect_parent_params(item)
# Erstelle und zeige den Dialog
dialog = XslFileEditDialog(self, node, parent_params)
if dialog.exec() == XslFileEditDialog.DialogCode.Accepted:
# Hole die bearbeiteten Daten
data = dialog.get_data()
if data:
# Aktualisiere den Node
node.bez = data['bez']
node.xslt_params = data['xslt_params']
print(f"XslFile '{node.bez}' wurde aktualisiert")
print(f"XSLT-Parameter: {node.xslt_params}")
# Speichere die Änderungen
self._save_project_settings()
# Aktualisiere das TreeWidget
self._load_nodes_to_tree()
# QMessageBox.information(self, "Erfolg", "XSL-Datei wurde erfolgreich aktualisiert.")
2025-08-12 20:56:26 +02:00
except Exception as e:
error_msg = f"Fehler beim Bearbeiten der XSL-Datei: {str(e)}"
print(error_msg)
QMessageBox.critical(self, "Fehler", error_msg)
2025-08-03 16:31:38 +02:00
def _delete_xsl_file(self, item):
"""Löscht eine XSL-Datei."""
print(f"XslFile löschen: {item.text(0)}")
# TODO: Bestätigungsdialog und Löschung implementieren
# Kontextmenü-Aktionen für XmlFile
def _edit_xml_file(self, item):
"""Bearbeitet eine XML-Datei."""
print(f"XmlFile bearbeiten: {item.text(0)}")
# TODO: Dialog zum Bearbeiten der XML-Datei öffnen
def _delete_xml_file(self, item):
"""
Löscht eine XML-Datei aus einem XSL-Knoten.
Args:
item: Das TreeWidgetItem der XML-Datei
"""
2025-08-03 16:31:38 +02:00
print(f"XmlFile löschen: {item.text(0)}")
try:
# Prüfe ob ein Projekt geladen ist
if not hasattr(self, 'project') or not self.project:
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:
QMessageBox.warning(self, "Warnung", "Keine Projekt-Einstellungen geladen.")
return
# Hole das XmlFile-Objekt aus dem TreeWidgetItem
xml_file_obj = item.data(0, Qt.ItemDataRole.UserRole)
if not xml_file_obj or not isinstance(xml_file_obj, XmlFile):
QMessageBox.warning(self, "Warnung", "Keine gültige XML-Datei gefunden.")
return
# Hole das Eltern-Item (sollte ein XslFile sein)
parent_item = item.parent()
if not parent_item:
QMessageBox.warning(self, "Warnung", "Eltern-XSL-Datei nicht gefunden.")
return
# Hole das XslFile-Objekt aus dem Eltern-Item
xsl_file_obj = parent_item.data(0, Qt.ItemDataRole.UserRole)
if not xsl_file_obj or not isinstance(xsl_file_obj, XslFile):
QMessageBox.warning(self, "Warnung", "Keine gültige Eltern-XSL-Datei gefunden.")
return
# Bestätigungsdialog anzeigen
xml_filename = xml_file_obj.xml.name
reply = QMessageBox.question(
self,
"XML-Datei löschen",
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
)
if reply != QMessageBox.StandardButton.Yes:
print("Löschung abgebrochen")
return
# Entferne die XML-Datei aus der XslFile-Node
xml_files_before = len(xsl_file_obj.xmls)
xsl_file_obj.xmls = [xml for xml in xsl_file_obj.xmls if xml.xml != xml_file_obj.xml]
xml_files_after = len(xsl_file_obj.xmls)
if xml_files_before == xml_files_after:
QMessageBox.warning(self, "Warnung", "XML-Datei konnte nicht aus der XSL-Datei entfernt werden.")
return
print(f"XML-Datei '{xml_filename}' aus XSL-Datei '{xsl_file_obj.bez}' entfernt")
# Frage ob die physische Datei auch gelöscht werden soll
xml_file_path = Path(self.project.project_dir) / xml_file_obj.xml
if xml_file_path.exists():
# Prüfe ob die XML-Datei noch in anderen XSL-Dateien verwendet wird
is_used_elsewhere = self._is_xml_file_used_elsewhere(xml_file_obj.xml, xsl_file_obj)
if not is_used_elsewhere:
delete_reply = QMessageBox.question(
self,
"Physische Datei löschen",
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
)
if delete_reply == QMessageBox.StandardButton.Yes:
try:
xml_file_path.unlink()
print(f"Physische XML-Datei gelöscht: {xml_file_path}")
except Exception as e:
QMessageBox.warning(
self,
"Warnung",
f"Fehler beim Löschen der physischen Datei:\n{str(e)}"
)
else:
print(f"XML-Datei '{xml_filename}' wird noch in anderen XSL-Dateien verwendet - physische Datei nicht gelöscht")
# Speichere die aktualisierten Projekt-Einstellungen
self._save_project_settings()
# Aktualisiere das TreeWidget
self._load_nodes_to_tree()
print(f"XML-Datei '{xml_filename}' erfolgreich entfernt")
except Exception as e:
error_msg = f"Fehler beim Löschen der XML-Datei: {str(e)}"
print(error_msg)
QMessageBox.critical(self, "Fehler", error_msg)
def _is_xml_file_used_elsewhere(self, xml_path, exclude_xsl_file):
"""
Prüft ob eine XML-Datei noch in anderen XSL-Dateien verwendet wird.
Args:
xml_path: Pfad zur XML-Datei (relativ)
exclude_xsl_file: XSL-Datei die ausgeschlossen werden soll
Returns:
bool: True wenn die XML-Datei noch anderswo verwendet wird
"""
try:
return self._check_xml_usage_recursive(self.pdf_project.nodes, xml_path, exclude_xsl_file)
except Exception as e:
print(f"Fehler beim Prüfen der XML-Datei-Verwendung: {e}")
return True # Im Zweifelsfall annehmen, dass sie verwendet wird
def _check_xml_usage_recursive(self, nodes, xml_path, exclude_xsl_file):
"""
Prüft rekursiv ob eine XML-Datei in den Nodes verwendet wird.
Args:
nodes: Liste der zu prüfenden Nodes
xml_path: Pfad zur XML-Datei (relativ)
exclude_xsl_file: XSL-Datei die ausgeschlossen werden soll
Returns:
bool: True wenn die XML-Datei gefunden wird
"""
for node in nodes:
if isinstance(node, XslFile) and node != exclude_xsl_file:
# Prüfe ob diese XSL-Datei die XML-Datei verwendet
for xml_file in node.xmls:
if xml_file.xml == xml_path:
return True
elif isinstance(node, TreeNode) and node.children:
# Rekursiv in Knoten suchen
if self._check_xml_usage_recursive(node.children, xml_path, exclude_xsl_file):
return True
return False
2025-08-03 16:31:38 +02:00
# Kontextmenü-Aktionen für Root-Elemente (Unbekannter Typ)
def _add_root_tree_node(self):
"""Fügt einen neuen TreeNode als Root-Element hinzu."""
print("Neuen TreeNode als Root-Element hinzufügen")
# TODO: Dialog zum Eingeben der TreeNode-Daten öffnen
2025-08-10 14:03:15 +02:00
def on_load_from_fn2_clicked(self):
"""
Wird ausgeführt, wenn der Button "lade aus FN2" geklickt wird.
Führt SQL-Abfrage aus und aktualisiert die Projekt-Nodes.
"""
print("Button 'lade aus FN2' wurde geklickt!")
try:
# Prüfe ob ein Projekt geladen ist
if not hasattr(self, 'project') or not self.project:
QMessageBox.warning(self, "Warnung", "Kein Projekt geladen. Bitte öffnen Sie zuerst ein Projekt.")
return
# Hole das aktuelle Projekt aus app_settings
if not self.project:
QMessageBox.warning(self, "Warnung", "Aktuelles Projekt nicht in den Einstellungen gefunden.")
return
# Hole die PostgreSQL-Datenbank-Konfiguration
db_config = self._get_database_config(self.project.postgre_sql_db_id)
if not db_config:
QMessageBox.warning(self, "Warnung", "PostgreSQL-Datenbank-Konfiguration nicht gefunden.")
return
# Führe SQL-Abfrage aus
df = self._execute_sql_query(db_config)
if df is None:
return # Fehler bereits angezeigt
# Verarbeite die Daten wie in readCsv.py
new_nodes = self._process_sql_data(df)
# Merge mit vorhandenen Nodes
self._merge_nodes_with_existing(new_nodes)
# Speichere die aktualisierten Projekt-Einstellungen
self._save_project_settings()
# Lade das Projekt neu
self._load_nodes_to_tree()
# QMessageBox.information(self, "Erfolg", "Daten erfolgreich aus FN2 geladen und Projekt aktualisiert!")
2025-08-10 14:03:15 +02:00
except Exception as e:
print(f"Fehler beim Laden aus FN2: {e}")
QMessageBox.critical(self, "Fehler", f"Fehler beim Laden aus FN2:\n{str(e)}")
def _get_database_config(self, db_id):
"""
Holt die Datenbank-Konfiguration anhand der ID.
Args:
db_id: ID der PostgreSQL-Datenbank
Returns:
PostgreSqlDb|None: Die Datenbank-Konfiguration oder None
"""
for db in app_settings.postgresql_dbs:
if db.id == db_id:
return db
return None
def _execute_sql_query(self, db_config):
"""
Führt die SQL-Abfrage aus der data.sql Datei aus.
Args:
db_config: PostgreSQL-Datenbank-Konfiguration
Returns:
pl.DataFrame|None: Die Abfrageergebnisse oder None bei Fehler
"""
try:
# Lade SQL-Abfrage aus Datei
sql_file_path = Path("src/res/data.sql")
if not sql_file_path.exists():
QMessageBox.critical(self, "Fehler", f"SQL-Datei nicht gefunden: {sql_file_path}")
return None
with open(sql_file_path, 'r', encoding='utf-8') as f:
sql_query = f.read()
print(f"SQL-Abfrage geladen: {len(sql_query)} Zeichen")
# Verbindung zur PostgreSQL-Datenbank herstellen
connection_string = ("postgresql://"
f"{db_config.username}:"
f"{db_config.password}@"
f"{db_config.host}:"
f"{db_config.port}/"
f"{db_config.database}?"
f"sslmode={db_config.ssl_mode.value}"
)
print(f"Verbinde zu PostgreSQL: {db_config.host}:{db_config.port}/{db_config.database}")
df = pl.read_database_uri(sql_query, connection_string, engine='connectorx').sort(["reporttyp_bez", "report_bez", "repfile_bez"])
return df
except Exception as e:
error_msg = f"Fehler beim Ausführen der SQL-Abfrage: {str(e)}"
print(error_msg)
QMessageBox.critical(self, "Fehler", error_msg)
return None
def _process_sql_data(self, df):
"""
Verarbeitet die SQL-Daten wie in readCsv.py und erstellt Node-Struktur.
Args:
df: Polars DataFrame mit den SQL-Ergebnissen
Returns:
list[TreeNode]: Liste der erstellten Root-Nodes
"""
try:
start_time = time.time()
# Gruppiere die Daten wie in readCsv.py
ebene_1 = df.group_by(["reporttyp", "reporttyp_bez"]).len()
ebene_2 = df.group_by(["reporttyp", "report", "report_bez"]).len()
ebene_3 = df.group_by(["reporttyp", "report", "repfile", "repfile_bez", "xsl_datei"]).len()
group_time = time.time() - start_time
print(f"Performance: Gruppierung in {group_time:.3f}s")
new_nodes = []
start_time = time.time()
# Erstelle Node-Struktur wie in readCsv.py
for r1 in ebene_1.rows(named=True):
tn_1 = TreeNode(id=(r1["reporttyp"],), bez=r1["reporttyp_bez"], children=[])
r1_children = ebene_2.filter(pl.col("reporttyp") == r1["reporttyp"])
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"]))
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=[]
)
tn_2.children.append(x)
tn_1.children.append(tn_2)
new_nodes.append(tn_1)
nodes_time = time.time() - start_time
print(f"Performance: Node-Erstellung in {nodes_time:.3f}s")
print(f"Erstellt: {len(new_nodes)} Root-Nodes")
return new_nodes
except Exception as e:
print(f"Fehler beim Verarbeiten der SQL-Daten: {e}")
raise
def _merge_nodes_with_existing(self, new_nodes):
"""
Merged neue Nodes mit vorhandenen Nodes basierend auf IDs.
Überschreibt nur einzelne Eigenschaften, nicht ganze Nodes.
Args:
new_nodes: Liste der neuen Nodes
"""
try:
print("Merge neue Nodes mit vorhandenen...")
# Erstelle ein Dictionary der neuen Nodes für schnellen Zugriff
new_nodes_dict = {}
self._build_nodes_dict(new_nodes, new_nodes_dict)
print(f"Neue Nodes Dictionary erstellt: {len(new_nodes_dict)} Einträge")
# Merge mit vorhandenen Nodes
if self.pdf_project.nodes:
self._merge_nodes_recursive(self.pdf_project.nodes, new_nodes_dict)
# Füge komplett neue Root-Nodes hinzu
existing_root_ids = {node.id for node in self.pdf_project.nodes}
for new_node in new_nodes:
if new_node.id not in existing_root_ids:
self.pdf_project.nodes.append(new_node)
print(f"Neue Root-Node hinzugefügt: {new_node.bez}")
print("Merge abgeschlossen")
except Exception as e:
print(f"Fehler beim Mergen der Nodes: {e}")
raise
def _build_nodes_dict(self, nodes, nodes_dict):
"""
Erstellt rekursiv ein Dictionary aller Nodes für schnellen ID-basierten Zugriff.
Args:
nodes: Liste der Nodes
nodes_dict: Dictionary zum Füllen
"""
for node in nodes:
nodes_dict[node.id] = node
if isinstance(node, TreeNode) and node.children:
self._build_nodes_dict(node.children, nodes_dict)
def _merge_nodes_recursive(self, existing_nodes, new_nodes_dict):
"""
Merged rekursiv vorhandene Nodes mit neuen Nodes.
Args:
existing_nodes: Liste der vorhandenen Nodes
new_nodes_dict: Dictionary der neuen Nodes
"""
for existing_node in existing_nodes:
if existing_node.id in new_nodes_dict:
new_node = new_nodes_dict[existing_node.id]
# Aktualisiere nur die Bezeichnung, falls sie sich geändert hat
if existing_node.bez != new_node.bez:
print(f"Aktualisiere Bezeichnung für Node {existing_node.id}: '{existing_node.bez}' -> '{new_node.bez}'")
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}'")
existing_node.xsl_file = new_node.xsl_file
2025-08-14 20:37:03 +02:00
# Rekursiv für Knoten (nur bei TreeNode)
2025-08-10 14:03:15 +02:00
if isinstance(existing_node, TreeNode) and existing_node.children:
self._merge_nodes_recursive(existing_node.children, new_nodes_dict)
2025-08-14 20:37:03 +02:00
# Füge neue Knoten hinzu, die noch nicht existieren
2025-08-10 14:03:15 +02:00
if existing_node.id in new_nodes_dict:
new_node = new_nodes_dict[existing_node.id]
if isinstance(new_node, TreeNode) and new_node.children:
existing_child_ids = {child.id for child in existing_node.children}
for new_child in new_node.children:
if new_child.id not in existing_child_ids:
existing_node.children.append(new_child)
print(f"Neues Kind hinzugefügt zu Node {existing_node.id}: {new_child.bez}")
2025-08-12 20:56:26 +02:00
def _collect_parent_params(self, item):
"""
Sammelt die XSLT-Parameter aller Eltern-Nodes.
Args:
item: Das TreeWidgetItem
Returns:
dict: Dictionary mit allen Eltern-Parametern
"""
parent_params = {}
try:
# Gehe die Hierarchie nach oben durch
current_item = item.parent()
while current_item:
# Hole das Node-Objekt
parent_node = current_item.data(0, Qt.ItemDataRole.UserRole)
if parent_node and hasattr(parent_node, 'xslt_params') and parent_node.xslt_params:
# Füge die Parameter des Eltern-Nodes hinzu
# Eltern-Parameter haben niedrigere Priorität (werden überschrieben)
for key, value in parent_node.xslt_params.items():
if key not in parent_params: # Nur hinzufügen wenn noch nicht vorhanden
parent_params[key] = value
# Gehe zum nächsten Eltern-Element
current_item = current_item.parent()
print(f"Gesammelte Eltern-Parameter: {parent_params}")
return parent_params
except Exception as e:
print(f"Fehler beim Sammeln der Eltern-Parameter: {e}")
return {}
2025-08-10 14:03:15 +02:00
def _save_project_settings(self):
"""
Speichert die aktualisierten Projekt-Einstellungen.
Args:
current_project: Das aktuelle Projekt
"""
try:
start_time = time.time()
# Speichere in project.yaml im Projekt-Verzeichnis
self.pdf_project.writeSettings(project_dir=self.project.project_dir)
dump_time = time.time() - start_time
print(f"Performance: Projekt-Einstellungen gespeichert in {dump_time:.3f}s")
except Exception as e:
print(f"Fehler beim Speichern der Projekt-Einstellungen: {e}")
raise
def _setup_drag_drop(self):
"""Aktiviert Drag&Drop für das TreeWidget."""
try:
# Aktiviere Drag&Drop für das TreeWidget
self.ui.treeWidget.setAcceptDrops(True)
self.ui.treeWidget.setDragDropMode(self.ui.treeWidget.DragDropMode.DropOnly)
# Überschreibe die Drag&Drop-Events
self.ui.treeWidget.dragEnterEvent = self.tree_drag_enter_event
self.ui.treeWidget.dragMoveEvent = self.tree_drag_move_event
self.ui.treeWidget.dropEvent = self.tree_drop_event
print("Drag&Drop für TreeWidget aktiviert")
except Exception as e:
print(f"Fehler beim Aktivieren von Drag&Drop: {e}")
def tree_drag_enter_event(self, event: QDragEnterEvent):
"""
Wird ausgeführt, wenn ein Drag-Vorgang über das TreeWidget beginnt.
Args:
event: Das Drag-Enter-Event
"""
try:
# Prüfe ob URLs (Dateien) gedraggt werden
if event.mimeData().hasUrls():
urls = event.mimeData().urls()
# Prüfe ob mindestens eine XML-Datei dabei ist
xml_files = [url.toLocalFile() for url in urls
if url.toLocalFile().lower().endswith('.xml')]
if xml_files:
event.acceptProposedAction()
print(f"Drag-Enter akzeptiert: {len(xml_files)} XML-Dateien")
else:
event.ignore()
print("Drag-Enter ignoriert: Keine XML-Dateien")
else:
event.ignore()
print("Drag-Enter ignoriert: Keine URLs")
except Exception as e:
print(f"Fehler in tree_drag_enter_event: {e}")
event.ignore()
def tree_drag_move_event(self, event):
"""
Wird ausgeführt, wenn ein Drag-Vorgang über das TreeWidget bewegt wird.
Args:
event: Das Drag-Move-Event
"""
try:
# Prüfe ob URLs (Dateien) gedraggt werden
if event.mimeData().hasUrls():
urls = event.mimeData().urls()
# Prüfe ob mindestens eine XML-Datei dabei ist
xml_files = [url.toLocalFile() for url in urls
if url.toLocalFile().lower().endswith('.xml')]
if xml_files:
event.acceptProposedAction()
else:
event.ignore()
else:
event.ignore()
except Exception as e:
print(f"Fehler in tree_drag_move_event: {e}")
event.ignore()
def tree_drop_event(self, event: QDropEvent):
"""
Wird ausgeführt, wenn Dateien auf das TreeWidget gedroppt werden.
Args:
event: Das Drop-Event
"""
try:
# Prüfe ob ein Projekt geladen ist
if not hasattr(self, 'project') or not self.project:
QMessageBox.warning(self, "Warnung", "Kein Projekt geladen. Bitte öffnen Sie zuerst ein Projekt.")
event.ignore()
return
if not hasattr(self, 'pdf_project') or not self.pdf_project:
QMessageBox.warning(self, "Warnung", "Keine Projekt-Einstellungen geladen.")
event.ignore()
return
# Hole die URLs aus dem Drop-Event
if not event.mimeData().hasUrls():
event.ignore()
return
urls = event.mimeData().urls()
xml_files = []
# Sammle alle XML-Dateien
for url in urls:
file_path = url.toLocalFile()
if file_path.lower().endswith('.xml'):
xml_files.append(Path(file_path))
if not xml_files:
QMessageBox.information(self, "Information", "Keine XML-Dateien zum Hinzufügen gefunden.")
event.ignore()
return
print(f"Drop-Event: {len(xml_files)} XML-Dateien erkannt")
# Verarbeite jede XML-Datei einzeln
for xml_file_path in xml_files:
self._handle_xml_file_drop(xml_file_path)
event.acceptProposedAction()
except Exception as e:
error_msg = f"Fehler beim Verarbeiten des Drop-Events: {str(e)}"
print(error_msg)
QMessageBox.critical(self, "Fehler", error_msg)
event.ignore()
def _handle_xml_file_drop(self, xml_file_path: Path):
"""
Verarbeitet eine einzelne XML-Datei, die per Drag&Drop hinzugefügt wurde.
Args:
xml_file_path: Pfad zur XML-Datei
"""
try:
print(f"Verarbeite XML-Datei: {xml_file_path}")
# Prüfe ob die Datei existiert
if not xml_file_path.exists():
QMessageBox.critical(self, "Fehler", f"Die XML-Datei existiert nicht:\n{xml_file_path}")
return
# Öffne den Dialog zur Zuordnung zu XSL-Knoten
dialog = XmlToXslAssignDialog(
parent=self,
xml_file_path=xml_file_path,
project_nodes=self.pdf_project.nodes
)
if dialog.exec() == XmlToXslAssignDialog.DialogCode.Accepted:
# Hole die ausgewählten XSL-Knoten
selected_xsl_nodes = dialog.get_selected_xsl_nodes()
if selected_xsl_nodes:
# Verarbeite die Zuordnung
self._assign_xml_to_xsl_nodes(xml_file_path, selected_xsl_nodes)
else:
print("Keine XSL-Knoten ausgewählt")
else:
print("Dialog abgebrochen")
except Exception as e:
error_msg = f"Fehler beim Verarbeiten der XML-Datei '{xml_file_path}': {str(e)}"
print(error_msg)
QMessageBox.critical(self, "Fehler", error_msg)
def _assign_xml_to_xsl_nodes(self, xml_file_path: Path, selected_xsl_nodes: list):
"""
Ordnet eine XML-Datei den ausgewählten XSL-Knoten zu.
Implementiert Hash-basierte Duplikatserkennung und intelligente Dateinamen-Verwaltung.
Args:
xml_file_path: Pfad zur XML-Datei
selected_xsl_nodes: Liste der ausgewählten XSL-Knoten
"""
try:
logger.info(f"Ordne XML-Datei '{xml_file_path.name}' zu {len(selected_xsl_nodes)} XSL-Knoten zu")
# 1. Hash für die neue XML-Datei berechnen
file_hash = self._calculate_hash_for_file(xml_file_path)
if not file_hash:
logger.warning(f"Hash-Berechnung für {xml_file_path} fehlgeschlagen")
# 2. Prüfe ob eine XML-Datei mit gleichem Hash bereits im Projekt existiert
existing_xml = self._find_xml_file_by_hash(file_hash) if file_hash else None
if existing_xml:
# 3. Hash-Match gefunden: Ordne vorhandene XML-Datei zu
logger.info(f"Hash-Duplikat gefunden: {existing_xml.xml} hat gleichen Hash wie {xml_file_path.name}")
self._assign_existing_xml_to_nodes(existing_xml, selected_xsl_nodes)
else:
# 4. Kein Hash-Match: Verarbeite als neue XML-Datei
logger.info(f"Keine Hash-Duplikate gefunden für {xml_file_path.name}, verarbeite als neue Datei")
self._process_new_xml_file(xml_file_path, selected_xsl_nodes, file_hash)
except Exception as e:
error_msg = f"Fehler beim Zuordnen der XML-Datei: {str(e)}"
logger.error(error_msg)
QMessageBox.critical(self, "Fehler", error_msg)
def _start_xml_hash_calculation(self):
"""
Startet die asynchrone Hash-Berechnung für alle XML-Dateien im Projekt.
"""
try:
if not hasattr(self, 'pdf_project') or not self.pdf_project:
logger.debug("Keine Projekt-Einstellungen verfügbar für Hash-Berechnung")
return
# Sammle alle XML-Dateien aus dem Projekt
xml_files = self._collect_all_xml_files()
if not xml_files:
logger.debug("Keine XML-Dateien für Hash-Berechnung gefunden")
return
logger.info(f"Starte Hash-Berechnung für {len(xml_files)} XML-Dateien")
# Stoppe vorherigen Thread falls noch aktiv
if self.hash_calculator_thread and self.hash_calculator_thread.isRunning():
self.hash_calculator_thread.quit()
self.hash_calculator_thread.wait()
# Erstelle und starte neuen Hash-Berechnungs-Thread
self.hash_calculator_thread = XmlHashCalculatorThread(
project_dir=Path(self.project.project_dir),
xml_files=xml_files
)
# Verbinde Signale
self.hash_calculator_thread.hash_calculated.connect(self._on_hash_calculated)
self.hash_calculator_thread.calculation_finished.connect(self._on_hash_calculation_finished)
self.hash_calculator_thread.error_occurred.connect(self._on_hash_calculation_error)
# Starte Thread
self.hash_calculator_thread.start()
except Exception as e:
logger.error(f"Fehler beim Starten der Hash-Berechnung: {e}")
def _collect_all_xml_files(self) -> List[XmlFile]:
"""
Sammelt alle XmlFile-Objekte aus der Projektstruktur.
Returns:
List[XmlFile]: Liste aller gefundenen XML-Dateien
"""
xml_files = []
try:
if self.pdf_project and self.pdf_project.nodes:
self._collect_xml_files_recursive(self.pdf_project.nodes, xml_files)
logger.debug(f"Gesammelt: {len(xml_files)} XML-Dateien")
return xml_files
except Exception as e:
logger.error(f"Fehler beim Sammeln der XML-Dateien: {e}")
return []
def _collect_xml_files_recursive(self, nodes, xml_files: List[XmlFile]):
"""
Sammelt rekursiv alle XML-Dateien aus den Nodes.
Args:
nodes: Liste der zu durchsuchenden Nodes
xml_files: Liste zum Sammeln der XML-Dateien
"""
for node in nodes:
if isinstance(node, XslFile) and node.xmls:
# Füge alle XML-Dateien dieser XSL-Datei hinzu
for xml_file in node.xmls:
if xml_file not in xml_files: # Vermeide Duplikate
xml_files.append(xml_file)
elif isinstance(node, TreeNode) and node.children:
# Rekursiv in Kinder-Nodes suchen
self._collect_xml_files_recursive(node.children, xml_files)
def _on_hash_calculated(self, xml_file: XmlFile, hash_value: str):
"""
Wird aufgerufen, wenn ein Hash-Wert berechnet wurde.
Args:
xml_file: Das XmlFile-Objekt
hash_value: Der berechnete Hash-Wert mit Präfix
"""
try:
# Setze den Hash-Wert
xml_file.hashsum = hash_value
logger.debug(f"Hash gesetzt für {xml_file.xml}: {hash_value}")
except Exception as e:
logger.error(f"Fehler beim Setzen des Hash-Werts: {e}")
def _on_hash_calculation_finished(self, processed_count: int, total_count: int):
"""
Wird aufgerufen, wenn die Hash-Berechnung abgeschlossen ist.
Args:
processed_count: Anzahl der verarbeiteten Dateien
total_count: Gesamtanzahl der Dateien
"""
try:
logger.info(f"Hash-Berechnung abgeschlossen: {processed_count}/{total_count} Dateien verarbeitet")
# Speichere die aktualisierten Projekt-Einstellungen
if processed_count > 0:
self._save_project_settings()
logger.info("Projekt-Einstellungen mit neuen Hash-Werten gespeichert")
except Exception as e:
logger.error(f"Fehler beim Abschließen der Hash-Berechnung: {e}")
def _on_hash_calculation_error(self, xml_file_path: str, error_message: str):
"""
Wird aufgerufen, wenn ein Fehler bei der Hash-Berechnung auftritt.
Args:
xml_file_path: Pfad zur XML-Datei
error_message: Fehlermeldung
"""
logger.warning(f"Hash-Berechnungsfehler für {xml_file_path}: {error_message}")
def _calculate_hash_for_xml_file(self, xml_file: XmlFile):
"""
Berechnet synchron den Hash für eine einzelne XML-Datei.
Wird verwendet beim Hinzufügen neuer XML-Dateien.
Args:
xml_file: Das XmlFile-Objekt
"""
try:
if xml_file.hashsum:
logger.debug(f"Hash bereits vorhanden für {xml_file.xml}: {xml_file.hashsum}")
return
xml_file_path = Path(self.project.project_dir) / xml_file.xml
if not xml_file_path.exists():
logger.warning(f"XML-Datei nicht gefunden: {xml_file_path}")
return
# Datei binär lesen und Hash berechnen
with open(xml_file_path, 'rb') as f:
file_content = f.read()
hash_obj = hashlib.blake2b(file_content)
hash_hex = hash_obj.hexdigest()
# Hash mit Präfix setzen
xml_file.hashsum = f"blake2b:{hash_hex}"
logger.debug(f"Hash berechnet für {xml_file.xml}: {xml_file.hashsum}")
except Exception as e:
logger.error(f"Fehler beim Berechnen des Hash für {xml_file.xml}: {e}")
def _get_all_project_xml_files(self) -> List[XmlFile]:
"""
Sammelt alle XmlFile-Objekte aus dem gesamten Projekt für Hash-Vergleiche.
Returns:
List[XmlFile]: Liste aller XML-Dateien im Projekt
"""
xml_files = []
try:
if self.pdf_project and self.pdf_project.nodes:
self._collect_xml_files_for_hash_comparison(self.pdf_project.nodes, xml_files)
logger.debug(f"Hash-Vergleich: {len(xml_files)} XML-Dateien im Projekt gefunden")
return xml_files
except Exception as e:
logger.error(f"Fehler beim Sammeln der XML-Dateien für Hash-Vergleich: {e}")
return []
def _collect_xml_files_for_hash_comparison(self, nodes, xml_files: List[XmlFile]):
"""
Sammelt rekursiv alle XML-Dateien aus den Nodes für Hash-Vergleiche.
Args:
nodes: Liste der zu durchsuchenden Nodes
xml_files: Liste zum Sammeln der XML-Dateien
"""
for node in nodes:
if isinstance(node, XslFile) and node.xmls:
# Füge alle XML-Dateien dieser XSL-Datei hinzu
for xml_file in node.xmls:
# Vermeide Duplikate basierend auf Pfad
if not any(existing.xml == xml_file.xml for existing in xml_files):
xml_files.append(xml_file)
elif isinstance(node, TreeNode) and node.children:
# Rekursiv in Kinder-Nodes suchen
self._collect_xml_files_for_hash_comparison(node.children, xml_files)
def _find_xml_file_by_hash(self, target_hash: str) -> XmlFile | None:
"""
Sucht eine XML-Datei mit dem angegebenen Hash im gesamten Projekt.
Args:
target_hash: Der zu suchende Hash-Wert (mit blake2b: Präfix)
Returns:
XmlFile|None: Die gefundene XML-Datei oder None
"""
try:
if not target_hash:
return None
all_xml_files = self._get_all_project_xml_files()
for xml_file in all_xml_files:
if xml_file.hashsum == target_hash:
logger.debug(f"Hash-Match gefunden: {xml_file.xml} hat Hash {target_hash}")
return xml_file
logger.debug(f"Kein Hash-Match für {target_hash} gefunden")
return None
except Exception as e:
logger.error(f"Fehler bei Hash-Suche für {target_hash}: {e}")
return None
def _generate_alternative_filename(self, original_path: Path, xml_dir: Path) -> Path:
"""
Generiert alternative Dateinamen im Format: datei_1.xml, datei_2.xml, ...
Args:
original_path: Ursprünglicher Dateipfad
xml_dir: Ziel-XML-Verzeichnis
Returns:
Path: Pfad mit alternativem Dateinamen
"""
try:
base_name = original_path.stem # "datei"
extension = original_path.suffix # ".xml"
# Sammle einmalig alle verwendeten Dateinamen (Performance-Optimierung)
all_xml_files = self._get_all_project_xml_files()
used_names = {xml_file.xml.name for xml_file in all_xml_files}
counter = 1
while True:
new_name = f"{base_name}_{counter}{extension}"
new_path = xml_dir / new_name
# Prüfe sowohl physische Existenz als auch Verwendung im Projekt (optimierter Set-Lookup)
if not new_path.exists() and new_name not in used_names:
logger.debug(f"Alternativer Dateiname generiert: {new_name}")
return new_path
counter += 1
# Sicherheitsgrenze um Endlosschleifen zu vermeiden
if counter > 1000:
raise Exception("Zu viele alternative Dateinamen generiert")
except Exception as e:
logger.error(f"Fehler beim Generieren alternativer Dateinamen für {original_path}: {e}")
# Fallback: Zeitstempel verwenden
timestamp = int(time.time())
fallback_name = f"{original_path.stem}_{timestamp}{original_path.suffix}"
return xml_dir / fallback_name
def _is_filename_used_in_project(self, relative_xml_path: Path) -> bool:
"""
Prüft ob ein relativer XML-Dateipfad bereits im Projekt verwendet wird.
Args:
relative_xml_path: Relativer Pfad zur XML-Datei (z.B. xml/datei_1.xml)
Returns:
bool: True wenn der Dateiname bereits verwendet wird
"""
try:
all_xml_files = self._get_all_project_xml_files()
for xml_file in all_xml_files:
if xml_file.xml == relative_xml_path:
return True
return False
except Exception as e:
logger.error(f"Fehler beim Prüfen der Dateiname-Verwendung für {relative_xml_path}: {e}")
return True # Im Zweifelsfall annehmen, dass der Name verwendet wird
def _calculate_hash_for_file(self, file_path: Path) -> str | None:
"""
Berechnet synchron den blake2b-Hash für eine Datei.
Args:
file_path: Pfad zur Datei
Returns:
str|None: Hash-Wert mit blake2b: Präfix oder None bei Fehler
"""
try:
if not file_path.exists():
logger.warning(f"Datei für Hash-Berechnung nicht gefunden: {file_path}")
return None
# Datei binär lesen und Hash berechnen
with open(file_path, 'rb') as f:
file_content = f.read()
hash_obj = hashlib.blake2b(file_content)
hash_hex = hash_obj.hexdigest()
# Hash mit Präfix zurückgeben
hash_value = f"blake2b:{hash_hex}"
logger.debug(f"Hash berechnet für {file_path}: {hash_value}")
return hash_value
except Exception as e:
logger.error(f"Fehler beim Berechnen des Hash für {file_path}: {e}")
return None
def _assign_existing_xml_to_nodes(self, existing_xml: XmlFile, selected_xsl_nodes: list):
"""
Ordnet eine bereits vorhandene XML-Datei (basierend auf Hash-Match) den XSL-Knoten zu.
Args:
existing_xml: Die bereits vorhandene XML-Datei
selected_xsl_nodes: Liste der ausgewählten XSL-Knoten
"""
try:
added_count = 0
for xsl_node in selected_xsl_nodes:
# Prüfe ob diese XML-Datei bereits in der XslFile-Node vorhanden ist
already_assigned = any(xml_file.xml == existing_xml.xml for xml_file in xsl_node.xmls)
if not already_assigned:
# Erstelle neue XmlFile-Referenz mit gleichem Pfad und Hash
new_xml_ref = XmlFile(xml=existing_xml.xml, hashsum=existing_xml.hashsum)
xsl_node.xmls.append(new_xml_ref)
added_count += 1
logger.info(f"Vorhandene XML-Datei '{existing_xml.xml}' zu XSL-Node '{xsl_node.bez}' zugeordnet")
else:
logger.debug(f"XML-Datei '{existing_xml.xml}' bereits in XSL-Node '{xsl_node.bez}' vorhanden")
if added_count > 0:
# Speichere die aktualisierten Projekt-Einstellungen
self._save_project_settings()
# Aktualisiere das TreeWidget
self._load_nodes_to_tree()
# Zeige Erfolgsmeldung
QMessageBox.information(
self,
"XML-Datei zugeordnet",
f"Eine XML-Datei mit gleichem Inhalt war bereits im Projekt vorhanden.\n\n"
f"Die vorhandene Datei '{existing_xml.xml.name}' wurde automatisch zu {added_count} XSL-Knoten zugeordnet."
)
else:
QMessageBox.information(
self,
"Bereits zugeordnet",
f"Die XML-Datei mit gleichem Inhalt ist bereits in allen ausgewählten XSL-Knoten vorhanden."
)
except Exception as e:
error_msg = f"Fehler beim Zuordnen der vorhandenen XML-Datei: {str(e)}"
logger.error(error_msg)
QMessageBox.critical(self, "Fehler", error_msg)
def _process_new_xml_file(self, xml_file_path: Path, selected_xsl_nodes: list, file_hash: str | None):
"""
Verarbeitet eine neue XML-Datei (kein Hash-Match gefunden).
Args:
xml_file_path: Pfad zur neuen XML-Datei
selected_xsl_nodes: Liste der ausgewählten XSL-Knoten
file_hash: Berechneter Hash der Datei
"""
try:
# Erstelle xml-Ordner im Projekt-Verzeichnis falls er nicht existiert
xml_dir = Path(self.project.project_dir) / "xml"
xml_dir.mkdir(parents=True, exist_ok=True)
# Bestimme den Ziel-Pfad in xml-Ordner
target_xml_path = xml_dir / xml_file_path.name
# Prüfe ob eine Datei mit gleichem Namen bereits existiert
if target_xml_path.exists() or self._is_filename_used_in_project(Path("xml") / xml_file_path.name):
# Generiere alternative Dateinamen
alternative_paths = []
for i in range(1, 6): # Generiere bis zu 5 Alternativen
alt_path = self._generate_alternative_filename(xml_file_path, xml_dir)
if alt_path not in alternative_paths:
alternative_paths.append(alt_path)
# Zeige Dialog zur Auswahl des Dateinamens
selected_path = self._show_filename_selection_dialog(xml_file_path.name, alternative_paths)
if not selected_path:
# Benutzer hat abgebrochen
return
target_xml_path = selected_path
# Kopiere die XML-Datei in den xml-Ordner
shutil.copy2(xml_file_path, target_xml_path)
logger.info(f"XML-Datei kopiert: {xml_file_path} -> {target_xml_path}")
# Erstelle relatives Path zur XML-Datei (relativ zum Projekt-Verzeichnis)
relative_xml_path = Path("xml") / target_xml_path.name
# Füge die XML-Datei zu allen ausgewählten XSL-Knoten hinzu
added_count = 0
for xsl_node in selected_xsl_nodes:
# Prüfe ob diese XML-Datei bereits in der XslFile-Node vorhanden ist
existing_xml = None
for xml_file in xsl_node.xmls:
if xml_file.xml == relative_xml_path:
existing_xml = xml_file
break
if not existing_xml:
# Erstelle neues XmlFile-Objekt mit Hash
new_xml_file = XmlFile(xml=relative_xml_path, hashsum=file_hash)
xsl_node.xmls.append(new_xml_file)
added_count += 1
logger.info(f"XML-Datei '{target_xml_path.name}' zu XSL-Node '{xsl_node.bez}' hinzugefügt")
else:
logger.debug(f"XML-Datei '{target_xml_path.name}' bereits in XSL-Node '{xsl_node.bez}' vorhanden")
if added_count > 0:
# Speichere die aktualisierten Projekt-Einstellungen
self._save_project_settings()
# Aktualisiere das TreeWidget
self._load_nodes_to_tree()
# Zeige Erfolgsmeldung
success_msg = f"XML-Datei '{target_xml_path.name}' wurde erfolgreich zu {added_count} XSL-Knoten hinzugefügt."
if target_xml_path.name != xml_file_path.name:
success_msg += f"\n\nDie Datei wurde umbenannt von '{xml_file_path.name}' zu '{target_xml_path.name}'."
QMessageBox.information(self, "Erfolg", success_msg)
else:
QMessageBox.information(
self,
"Information",
f"XML-Datei '{target_xml_path.name}' war bereits in allen ausgewählten XSL-Knoten vorhanden."
)
except Exception as e:
error_msg = f"Fehler beim Verarbeiten der neuen XML-Datei: {str(e)}"
logger.error(error_msg)
QMessageBox.critical(self, "Fehler", error_msg)
def _show_filename_selection_dialog(self, original_name: str, alternative_paths: List[Path]) -> Path | None:
"""
Zeigt einen Dialog zur Auswahl eines alternativen Dateinamens.
Args:
original_name: Ursprünglicher Dateiname
alternative_paths: Liste alternativer Pfade
Returns:
Path|None: Ausgewählter Pfad oder None bei Abbruch
"""
try:
from PySide6.QtWidgets import QDialog, QVBoxLayout, QLabel, QRadioButton, QButtonGroup, QPushButton, QHBoxLayout
dialog = QDialog(self)
dialog.setWindowTitle("Dateiname auswählen")
dialog.setModal(True)
dialog.resize(400, 300)
layout = QVBoxLayout(dialog)
# Erklärungstext
info_label = QLabel(f"Eine Datei mit dem Namen '{original_name}' existiert bereits.\n\n"
"Bitte wählen Sie einen alternativen Dateinamen:")
layout.addWidget(info_label)
# Radio-Buttons für alternative Namen
button_group = QButtonGroup(dialog)
radio_buttons = []
for i, alt_path in enumerate(alternative_paths):
radio_button = QRadioButton(alt_path.name)
if i == 0: # Ersten als Standard auswählen
radio_button.setChecked(True)
button_group.addButton(radio_button, i)
radio_buttons.append(radio_button)
layout.addWidget(radio_button)
# Buttons
button_layout = QHBoxLayout()
ok_button = QPushButton("OK")
cancel_button = QPushButton("Abbrechen")
button_layout.addWidget(ok_button)
button_layout.addWidget(cancel_button)
layout.addLayout(button_layout)
# Event-Handler
ok_button.clicked.connect(dialog.accept)
cancel_button.clicked.connect(dialog.reject)
# Dialog anzeigen
if dialog.exec() == QDialog.DialogCode.Accepted:
selected_id = button_group.checkedId()
if 0 <= selected_id < len(alternative_paths):
return alternative_paths[selected_id]
return None
except Exception as e:
logger.error(f"Fehler beim Anzeigen des Dateiname-Auswahl-Dialogs: {e}")
# Fallback: Ersten alternativen Namen verwenden
return alternative_paths[0] if alternative_paths else None
2025-05-29 16:30:01 +02:00
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():
self.hash_calculator_thread.quit()
self.hash_calculator_thread.wait()
2025-05-31 21:27:58 +02:00
# PDF-Dokumente schließen ist bei QtPdf automatisch durch Garbage Collection
2025-05-29 16:30:01 +02:00
super().closeEvent(event)