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
|
2025-09-20 17:22:09 +02:00
|
|
|
|
import hashlib
|
|
|
|
|
|
import logging
|
|
|
|
|
|
from typing import List
|
2025-05-23 11:09:47 +02:00
|
|
|
|
|
2026-01-02 21:06:37 +01:00
|
|
|
|
from PySide6.QtCore import Qt, QSize, QThread, Signal, QUrl, QEvent
|
2026-01-02 20:11:56 +01:00
|
|
|
|
from PySide6.QtGui import QCursor, QPixmap, QPainter, QAction, QIcon, QDragEnterEvent, QDropEvent, QDesktopServices
|
2025-12-13 21:06:40 +01:00
|
|
|
|
from PySide6.QtWidgets import (
|
|
|
|
|
|
QLabel,
|
|
|
|
|
|
QMainWindow,
|
|
|
|
|
|
QApplication,
|
|
|
|
|
|
QStyleFactory,
|
|
|
|
|
|
QMenu,
|
|
|
|
|
|
QTreeWidgetItem,
|
|
|
|
|
|
QMessageBox,
|
|
|
|
|
|
QFileDialog,
|
|
|
|
|
|
QWidget,
|
|
|
|
|
|
QHBoxLayout,
|
|
|
|
|
|
QProgressBar,
|
|
|
|
|
|
)
|
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
|
2025-06-14 12:30:39 +02:00
|
|
|
|
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
|
2025-08-31 17:04:22 +02:00
|
|
|
|
from ui.XmlToXslAssignDialog import XmlToXslAssignDialog
|
2025-08-10 17:32:22 +02:00
|
|
|
|
from conf import app_settings, Project, ProjectData, TreeNode, XslFile, XmlFile
|
2025-12-28 16:46:39 +01:00
|
|
|
|
from transform import TransformationJob, set_saxon_worker_pool
|
|
|
|
|
|
from saxon_pool import SaxonWorkerPool
|
2025-06-16 20:30:56 +02:00
|
|
|
|
from pathlib import Path
|
2025-05-20 11:24:07 +02:00
|
|
|
|
|
|
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class XmlHashCalculatorThread(QThread):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Thread für die asynchrone Berechnung von blake2b-Hash-Werten für XML-Dateien.
|
|
|
|
|
|
"""
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
# 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
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
def __init__(self, project_dir: Path, xml_files: List[XmlFile]):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Initialisiert den Hash-Berechnungs-Thread.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
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
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
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")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
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
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
# 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)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
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}")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
self.processed_count += 1
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
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
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
# 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")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
def _calculate_blake2b_hash(self, file_path: Path) -> str | None:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Berechnet den blake2b-Hash einer XML-Datei.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
Args:
|
|
|
|
|
|
file_path: Pfad zur XML-Datei
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
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
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
# Datei binär lesen und Hash berechnen
|
2025-12-13 21:06:40 +01:00
|
|
|
|
with open(file_path, "rb") as f:
|
2025-09-20 17:22:09 +02:00
|
|
|
|
file_content = f.read()
|
|
|
|
|
|
hash_obj = hashlib.blake2b(file_content)
|
|
|
|
|
|
hash_hex = hash_obj.hexdigest()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
# Präfix hinzufügen
|
|
|
|
|
|
return f"blake2b:{hash_hex}"
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Fehler beim Berechnen des Hash für {file_path}: {e}")
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-12-27 20:31:54 +01:00
|
|
|
|
class XmlBatchProcessingThread(QThread):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Thread für die asynchrone Batch-Verarbeitung von mehreren XML-Dateien.
|
|
|
|
|
|
Verarbeitet XML-Dateien mit Hash-Berechnung, Duplikatserkennung und Dateikopieren.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
# Signale für die Kommunikation mit dem Haupt-Thread
|
|
|
|
|
|
progress_update = Signal(int, int, str) # current, total, current_file_name
|
|
|
|
|
|
file_processed = Signal(dict) # result dictionary
|
|
|
|
|
|
processing_finished = Signal(dict) # stats dictionary
|
|
|
|
|
|
error_occurred = Signal(str) # error_message
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, xml_files: list, selected_xsl_nodes: list, project_dir: Path, pdf_project):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Initialisiert den Batch-Verarbeitungs-Thread.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
xml_files: Liste von Pfaden zu XML-Dateien
|
|
|
|
|
|
selected_xsl_nodes: Liste der ausgewählten XSL-Knoten
|
|
|
|
|
|
project_dir: Pfad zum Projekt-Verzeichnis
|
|
|
|
|
|
pdf_project: ProjectData-Objekt
|
|
|
|
|
|
"""
|
|
|
|
|
|
super().__init__()
|
|
|
|
|
|
self.xml_files = xml_files
|
|
|
|
|
|
self.selected_xsl_nodes = selected_xsl_nodes
|
|
|
|
|
|
self.project_dir = project_dir
|
|
|
|
|
|
self.pdf_project = pdf_project
|
|
|
|
|
|
|
|
|
|
|
|
# Statistiken
|
|
|
|
|
|
self.stats = {
|
|
|
|
|
|
"total": len(xml_files),
|
|
|
|
|
|
"processed": 0,
|
|
|
|
|
|
"new_added": 0,
|
|
|
|
|
|
"existing_added": 0,
|
|
|
|
|
|
"already_assigned": 0,
|
|
|
|
|
|
"cancelled": 0,
|
|
|
|
|
|
"errors": 0,
|
|
|
|
|
|
"error_messages": [],
|
|
|
|
|
|
"renamed_files": [],
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Führt die Batch-Verarbeitung aller XML-Dateien aus.
|
|
|
|
|
|
"""
|
|
|
|
|
|
logger.info(f"Starte Batch-Verarbeitung für {len(self.xml_files)} XML-Dateien")
|
|
|
|
|
|
|
|
|
|
|
|
for i, xml_file_path in enumerate(self.xml_files):
|
|
|
|
|
|
try:
|
|
|
|
|
|
# Sende Progress-Update
|
|
|
|
|
|
self.progress_update.emit(i + 1, len(self.xml_files), xml_file_path.name)
|
|
|
|
|
|
|
|
|
|
|
|
# Prüfe ob die Datei existiert
|
|
|
|
|
|
if not xml_file_path.exists():
|
|
|
|
|
|
self.stats["errors"] += 1
|
|
|
|
|
|
self.stats["error_messages"].append(f"{xml_file_path.name}: Datei existiert nicht")
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# Verarbeite die XML-Datei
|
|
|
|
|
|
result = self._process_xml_file(xml_file_path)
|
|
|
|
|
|
|
|
|
|
|
|
# Aktualisiere Statistiken
|
|
|
|
|
|
self._update_stats(result)
|
|
|
|
|
|
|
|
|
|
|
|
# Sende Ergebnis
|
|
|
|
|
|
self.file_processed.emit(result)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
error_msg = f"Fehler bei {xml_file_path.name}: {str(e)}"
|
|
|
|
|
|
logger.error(error_msg)
|
|
|
|
|
|
self.stats["errors"] += 1
|
|
|
|
|
|
self.stats["error_messages"].append(error_msg)
|
|
|
|
|
|
|
|
|
|
|
|
# Sende Abschluss-Signal mit Statistiken
|
|
|
|
|
|
self.processing_finished.emit(self.stats)
|
|
|
|
|
|
logger.info(f"Batch-Verarbeitung abgeschlossen: {self.stats['processed']}/{self.stats['total']} verarbeitet")
|
|
|
|
|
|
|
|
|
|
|
|
def _process_xml_file(self, xml_file_path: Path) -> dict:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Verarbeitet eine einzelne XML-Datei.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
xml_file_path: Pfad zur XML-Datei
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
dict: Ergebnis-Dictionary mit Status
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 1. Hash 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 auf Hash-Duplikat
|
|
|
|
|
|
existing_xml = self._find_xml_file_by_hash(file_hash) if file_hash else None
|
|
|
|
|
|
|
|
|
|
|
|
if existing_xml:
|
|
|
|
|
|
# Hash-Match: Ordne vorhandene Datei zu
|
|
|
|
|
|
return self._assign_existing_xml_to_nodes(existing_xml)
|
|
|
|
|
|
else:
|
|
|
|
|
|
# Keine Duplikate: Verarbeite als neue Datei
|
|
|
|
|
|
return self._process_new_xml_file(xml_file_path, file_hash)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
return {"status": "error", "error_msg": str(e)}
|
|
|
|
|
|
|
|
|
|
|
|
def _calculate_hash_for_file(self, file_path: Path) -> str | None:
|
|
|
|
|
|
"""Berechnet blake2b Hash für eine Datei."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
if not file_path.exists():
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
with open(file_path, "rb") as f:
|
|
|
|
|
|
file_content = f.read()
|
|
|
|
|
|
hash_obj = hashlib.blake2b(file_content)
|
|
|
|
|
|
hash_hex = hash_obj.hexdigest()
|
|
|
|
|
|
|
|
|
|
|
|
return f"blake2b:{hash_hex}"
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Fehler beim Berechnen des Hash für {file_path}: {e}")
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def _find_xml_file_by_hash(self, hash_value: str) -> XmlFile | None:
|
|
|
|
|
|
"""Sucht eine XML-Datei anhand ihres Hash-Werts."""
|
|
|
|
|
|
if not hash_value or not self.pdf_project.nodes:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def search_recursive(nodes):
|
|
|
|
|
|
for node in nodes:
|
|
|
|
|
|
if isinstance(node, XslFile) and node.xmls:
|
|
|
|
|
|
for xml_file in node.xmls:
|
|
|
|
|
|
if xml_file.hashsum == hash_value:
|
|
|
|
|
|
return xml_file
|
|
|
|
|
|
elif isinstance(node, TreeNode) and node.children:
|
|
|
|
|
|
found = search_recursive(node.children)
|
|
|
|
|
|
if found:
|
|
|
|
|
|
return found
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
return search_recursive(self.pdf_project.nodes)
|
|
|
|
|
|
|
|
|
|
|
|
def _assign_existing_xml_to_nodes(self, existing_xml: XmlFile) -> dict:
|
|
|
|
|
|
"""Ordnet eine vorhandene XML-Datei den Knoten zu."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
added_count = 0
|
|
|
|
|
|
|
|
|
|
|
|
for xsl_node in self.selected_xsl_nodes:
|
|
|
|
|
|
already_assigned = any(xml_file.xml == existing_xml.xml for xml_file in xsl_node.xmls)
|
|
|
|
|
|
|
|
|
|
|
|
if not already_assigned:
|
|
|
|
|
|
new_xml_ref = XmlFile(xml=existing_xml.xml, hashsum=existing_xml.hashsum)
|
|
|
|
|
|
xsl_node.xmls.append(new_xml_ref)
|
|
|
|
|
|
added_count += 1
|
|
|
|
|
|
|
|
|
|
|
|
if added_count > 0:
|
|
|
|
|
|
return {
|
|
|
|
|
|
"status": "existing_added",
|
|
|
|
|
|
"added_count": added_count,
|
|
|
|
|
|
"existing_file": existing_xml.xml.name,
|
|
|
|
|
|
}
|
|
|
|
|
|
else:
|
|
|
|
|
|
return {"status": "already_assigned", "added_count": 0, "existing_file": existing_xml.xml.name}
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
return {"status": "error", "error_msg": str(e)}
|
|
|
|
|
|
|
|
|
|
|
|
def _process_new_xml_file(self, xml_file_path: Path, file_hash: str | None) -> dict:
|
|
|
|
|
|
"""Verarbeitet eine neue XML-Datei."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# Erstelle xml-Ordner
|
|
|
|
|
|
xml_dir = self.project_dir / "xml"
|
|
|
|
|
|
xml_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
|
|
# Bestimme Ziel-Pfad
|
|
|
|
|
|
target_xml_path = xml_dir / xml_file_path.name
|
|
|
|
|
|
|
|
|
|
|
|
# Prüfe auf Namenskonflikte und generiere ggf. alternativen Namen
|
|
|
|
|
|
original_name = xml_file_path.name
|
|
|
|
|
|
counter = 1
|
|
|
|
|
|
while target_xml_path.exists() or self._is_filename_used_in_project(Path("xml") / target_xml_path.name):
|
|
|
|
|
|
# Generiere alternativen Namen
|
|
|
|
|
|
stem = xml_file_path.stem
|
|
|
|
|
|
suffix = xml_file_path.suffix
|
|
|
|
|
|
target_xml_path = xml_dir / f"{stem}_{counter}{suffix}"
|
|
|
|
|
|
counter += 1
|
|
|
|
|
|
|
|
|
|
|
|
# Sicherheit: Maximal 1000 Versuche
|
|
|
|
|
|
if counter > 1000:
|
|
|
|
|
|
return {"status": "error", "error_msg": "Konnte keinen eindeutigen Dateinamen finden"}
|
|
|
|
|
|
|
|
|
|
|
|
# Kopiere Datei
|
|
|
|
|
|
shutil.copy2(xml_file_path, target_xml_path)
|
|
|
|
|
|
|
|
|
|
|
|
# Erstelle relatives Path
|
|
|
|
|
|
relative_xml_path = Path("xml") / target_xml_path.name
|
|
|
|
|
|
|
|
|
|
|
|
# Füge zu XSL-Knoten hinzu
|
|
|
|
|
|
added_count = 0
|
|
|
|
|
|
for xsl_node in self.selected_xsl_nodes:
|
|
|
|
|
|
existing_xml = any(xml_file.xml == relative_xml_path for xml_file in xsl_node.xmls)
|
|
|
|
|
|
|
|
|
|
|
|
if not existing_xml:
|
|
|
|
|
|
new_xml_file = XmlFile(xml=relative_xml_path, hashsum=file_hash)
|
|
|
|
|
|
xsl_node.xmls.append(new_xml_file)
|
|
|
|
|
|
added_count += 1
|
|
|
|
|
|
|
|
|
|
|
|
if added_count > 0:
|
|
|
|
|
|
return {
|
|
|
|
|
|
"status": "new_added",
|
|
|
|
|
|
"added_count": added_count,
|
|
|
|
|
|
"new_file": target_xml_path.name,
|
|
|
|
|
|
"renamed_from": original_name if target_xml_path.name != original_name else None,
|
|
|
|
|
|
}
|
|
|
|
|
|
else:
|
|
|
|
|
|
return {"status": "already_assigned", "added_count": 0, "new_file": target_xml_path.name}
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
return {"status": "error", "error_msg": str(e)}
|
|
|
|
|
|
|
|
|
|
|
|
def _is_filename_used_in_project(self, filename: Path) -> bool:
|
|
|
|
|
|
"""Prüft ob ein Dateiname bereits im Projekt verwendet wird."""
|
|
|
|
|
|
if not self.pdf_project.nodes:
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def search_recursive(nodes):
|
|
|
|
|
|
for node in nodes:
|
|
|
|
|
|
if isinstance(node, XslFile) and node.xmls:
|
|
|
|
|
|
for xml_file in node.xmls:
|
|
|
|
|
|
if xml_file.xml == filename:
|
|
|
|
|
|
return True
|
|
|
|
|
|
elif isinstance(node, TreeNode) and node.children:
|
|
|
|
|
|
if search_recursive(node.children):
|
|
|
|
|
|
return True
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
return search_recursive(self.pdf_project.nodes)
|
|
|
|
|
|
|
|
|
|
|
|
def _update_stats(self, result: dict):
|
|
|
|
|
|
"""Aktualisiert die Statistiken."""
|
|
|
|
|
|
self.stats["processed"] += 1
|
|
|
|
|
|
|
|
|
|
|
|
status = result.get("status")
|
|
|
|
|
|
if status == "new_added":
|
|
|
|
|
|
self.stats["new_added"] += 1
|
|
|
|
|
|
if result.get("renamed_from"):
|
|
|
|
|
|
self.stats["renamed_files"].append(f"{result['renamed_from']} → {result['new_file']}")
|
|
|
|
|
|
elif status == "existing_added":
|
|
|
|
|
|
self.stats["existing_added"] += 1
|
|
|
|
|
|
elif status == "already_assigned":
|
|
|
|
|
|
self.stats["already_assigned"] += 1
|
|
|
|
|
|
elif status == "error":
|
|
|
|
|
|
self.stats["errors"] += 1
|
|
|
|
|
|
self.stats["error_messages"].append(result.get("error_msg", "Unbekannter Fehler"))
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-12-11 21:26:13 +01:00
|
|
|
|
class TransformationThread(QThread):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Thread für die asynchrone Ausführung von Transformations-Jobs.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
# Signale für die Kommunikation mit dem Haupt-Thread
|
2025-12-13 21:06:40 +01:00
|
|
|
|
job_started = Signal(str, str) # xml_file_name, xsl_id_str
|
2025-12-11 21:26:13 +01:00
|
|
|
|
job_finished = Signal(dict) # result_dict
|
2025-12-13 21:06:40 +01:00
|
|
|
|
job_error = Signal(str, str, str) # xml_file_name, xsl_id_str, error_message
|
2025-12-28 12:58:39 +01:00
|
|
|
|
all_jobs_finished = Signal(int, int, float) # successful_count, total_count, total_duration
|
2025-12-11 21:26:13 +01:00
|
|
|
|
|
2025-12-28 16:46:39 +01:00
|
|
|
|
def __init__(self, jobs: list[TransformationJob], force: bool = False, max_workers: int = 8):
|
2025-12-11 21:26:13 +01:00
|
|
|
|
"""
|
|
|
|
|
|
Initialisiert den Transformations-Thread.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
jobs: Liste der TransformationJob-Objekte
|
|
|
|
|
|
force: Wenn True, werden alle Jobs ausgeführt (ignoriert Up-to-Date)
|
2025-12-28 16:46:39 +01:00
|
|
|
|
max_workers: Maximale Anzahl paralleler Worker (Standard: 8)
|
2025-12-11 21:26:13 +01:00
|
|
|
|
"""
|
|
|
|
|
|
super().__init__()
|
|
|
|
|
|
self.jobs = jobs
|
|
|
|
|
|
self.force = force
|
2025-12-28 16:46:39 +01:00
|
|
|
|
self.max_workers = max_workers
|
2025-12-11 21:26:13 +01:00
|
|
|
|
self.successful_count = 0
|
|
|
|
|
|
|
2025-12-28 16:46:39 +01:00
|
|
|
|
def _process_single_job(self, job: TransformationJob) -> dict:
|
2025-12-11 21:26:13 +01:00
|
|
|
|
"""
|
2025-12-28 16:46:39 +01:00
|
|
|
|
Verarbeitet einen einzelnen Transformations-Job (Thread-safe).
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
job: Der zu verarbeitende TransformationJob
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
dict: Ergebnis-Dictionary des Jobs
|
2025-12-11 21:26:13 +01:00
|
|
|
|
"""
|
2025-12-28 16:46:39 +01:00
|
|
|
|
try:
|
|
|
|
|
|
# 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)
|
2025-12-28 12:58:39 +01:00
|
|
|
|
|
2025-12-28 16:46:39 +01:00
|
|
|
|
# Führe Transformations-Pipeline aus
|
|
|
|
|
|
result = job.run_full_pipeline(force=self.force)
|
2025-12-11 21:26:13 +01:00
|
|
|
|
|
2025-12-28 16:46:39 +01:00
|
|
|
|
# Sende Abschluss-Signal
|
|
|
|
|
|
self.job_finished.emit(result)
|
2025-12-11 21:26:13 +01:00
|
|
|
|
|
2025-12-28 16:46:39 +01:00
|
|
|
|
return result
|
2025-12-11 21:26:13 +01:00
|
|
|
|
|
2025-12-28 16:46:39 +01:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
error_msg = f"Unerwarteter Fehler bei Transformation: {str(e)}"
|
|
|
|
|
|
logger.error(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)
|
|
|
|
|
|
return {"success": False, "error": error_msg}
|
2025-12-11 21:26:13 +01:00
|
|
|
|
|
2025-12-28 16:46:39 +01:00
|
|
|
|
def run(self):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Führt alle Transformations-Jobs parallel aus mit ThreadPoolExecutor.
|
|
|
|
|
|
"""
|
|
|
|
|
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
import threading
|
2025-12-11 21:26:13 +01:00
|
|
|
|
|
2025-12-28 16:46:39 +01:00
|
|
|
|
start_time = datetime.now()
|
|
|
|
|
|
logger.info(f"Starte parallele Transformation von {len(self.jobs)} Jobs mit {self.max_workers} Workern")
|
|
|
|
|
|
|
|
|
|
|
|
# Thread-sicherer Counter
|
|
|
|
|
|
successful_lock = threading.Lock()
|
|
|
|
|
|
|
|
|
|
|
|
# Verwende ThreadPoolExecutor für parallele Verarbeitung
|
|
|
|
|
|
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
|
|
|
|
|
|
# Starte alle Jobs
|
|
|
|
|
|
future_to_job = {executor.submit(self._process_single_job, job): job for job in self.jobs}
|
|
|
|
|
|
|
|
|
|
|
|
# Warte auf Abschluss und sammle Ergebnisse
|
|
|
|
|
|
for future in as_completed(future_to_job):
|
|
|
|
|
|
try:
|
|
|
|
|
|
result = future.result()
|
|
|
|
|
|
if result.get("success", False):
|
|
|
|
|
|
with successful_lock:
|
|
|
|
|
|
self.successful_count += 1
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Fehler beim Verarbeiten des Future: {e}")
|
2025-12-11 21:26:13 +01:00
|
|
|
|
|
2025-12-28 12:58:39 +01:00
|
|
|
|
# Berechne Gesamtdauer
|
|
|
|
|
|
total_duration = (datetime.now() - start_time).total_seconds()
|
|
|
|
|
|
|
|
|
|
|
|
# Sende Abschluss-Signal für alle Jobs mit Gesamtdauer
|
|
|
|
|
|
self.all_jobs_finished.emit(self.successful_count, len(self.jobs), total_duration)
|
|
|
|
|
|
logger.info(
|
2025-12-28 16:46:39 +01:00
|
|
|
|
f"Transformation abgeschlossen: {self.successful_count}/{len(self.jobs)} erfolgreich ({total_duration:.2f}s) "
|
|
|
|
|
|
f"[{len(self.jobs) / total_duration:.2f} Jobs/s mit {self.max_workers} Workern]"
|
2025-12-28 12:58:39 +01:00
|
|
|
|
)
|
2025-12-11 21:26:13 +01:00
|
|
|
|
|
|
|
|
|
|
|
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-12-13 21:06:40 +01:00
|
|
|
|
|
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-12-13 21:06:40 +01: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-12-15 21:10:15 +01:00
|
|
|
|
# Aktuelle Diff-PDF-Informationen (für Accept Changes)
|
|
|
|
|
|
self.current_diff_xml_path = None
|
|
|
|
|
|
self.current_diff_xsl_id = None
|
|
|
|
|
|
|
2026-01-02 20:11:56 +01:00
|
|
|
|
# Pfade zu aktuellen Ref- und New-PDFs (für System-Viewer)
|
|
|
|
|
|
self.current_ref_pdf_path = None
|
|
|
|
|
|
self.current_new_pdf_path = 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-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 17:32:22 +02:00
|
|
|
|
# Das aktuelle Projekt (Project) aus app_settings
|
|
|
|
|
|
self.project = None
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 17:32:22 +02:00
|
|
|
|
# Das aktuelle ProjectData
|
|
|
|
|
|
self.pdf_project = None
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
# Hash-Berechnungs-Thread
|
|
|
|
|
|
self.hash_calculator_thread = None
|
2025-12-11 21:26:13 +01:00
|
|
|
|
|
|
|
|
|
|
# Transformations-Thread
|
|
|
|
|
|
self.transformation_thread = None
|
|
|
|
|
|
|
2025-12-27 20:31:54 +01:00
|
|
|
|
# Batch-Processing-Thread für XML-Dateien
|
|
|
|
|
|
self.batch_processing_thread = None
|
|
|
|
|
|
|
|
|
|
|
|
# Progressbar für Batch-Verarbeitung in Statusbar
|
|
|
|
|
|
self.batch_progress_bar = None
|
|
|
|
|
|
|
|
|
|
|
|
# Progressbar für Transformationen in Statusbar
|
|
|
|
|
|
self.transformation_progress_bar = None
|
|
|
|
|
|
|
2025-12-13 21:06:40 +01:00
|
|
|
|
# Mapping: xml_file_path_str → QTreeWidgetItem (für Progress Bar und Icon Updates)
|
|
|
|
|
|
self.xml_item_map = {}
|
|
|
|
|
|
|
2025-05-27 20:48:21 +02:00
|
|
|
|
# Theme-Menü initialisieren
|
|
|
|
|
|
self._setup_theme_menu()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-06-22 11:58:57 +02:00
|
|
|
|
# Vorhandene Projekte-Menü initialisieren
|
|
|
|
|
|
self._setup_projects_menu()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
|
|
|
|
|
#
|
|
|
|
|
|
if theme := app_settings.theme:
|
2025-06-17 19:11:08 +02:00
|
|
|
|
self.change_theme(theme)
|
|
|
|
|
|
else:
|
2025-12-13 21:06:40 +01:00
|
|
|
|
self.change_theme("Fusion")
|
2025-05-27 20:48:21 +02:00
|
|
|
|
|
2025-05-20 11:24:07 +02:00
|
|
|
|
# Signale und Slots verbinden
|
|
|
|
|
|
self._connect_signals()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-03 16:31:38 +02:00
|
|
|
|
# Kontextmenü für TreeWidget einrichten
|
|
|
|
|
|
self._setup_tree_context_menu()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-14 20:32:29 +02:00
|
|
|
|
# TreeWidget Styling für größeren vertikalen Abstand
|
|
|
|
|
|
self._setup_tree_widget_styling()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:04:22 +02:00
|
|
|
|
# Drag&Drop für TreeWidget aktivieren
|
|
|
|
|
|
self._setup_drag_drop()
|
2025-05-23 11:09:47 +02:00
|
|
|
|
|
2026-01-02 21:06:37 +01:00
|
|
|
|
# Zoom per Mausrad (STRG+Mausrad) für PDF-Viewer aktivieren
|
|
|
|
|
|
self._setup_scroll_area_zoom()
|
|
|
|
|
|
|
2025-12-15 20:07:53 +01:00
|
|
|
|
# Gespeicherte UI-Zustände wiederherstellen
|
|
|
|
|
|
self._restore_ui_state()
|
|
|
|
|
|
|
|
|
|
|
|
def _restore_ui_state(self):
|
|
|
|
|
|
"""Stellt die gespeicherten UI-Zustände wieder her (Fenstergeometrie, Splitter, TreeWidget-Spalten)."""
|
|
|
|
|
|
global app_settings
|
|
|
|
|
|
|
|
|
|
|
|
# Fenstergeometrie wiederherstellen
|
|
|
|
|
|
if app_settings.window_geometry:
|
|
|
|
|
|
x, y, width, height = app_settings.window_geometry
|
|
|
|
|
|
self.setGeometry(x, y, width, height)
|
|
|
|
|
|
|
|
|
|
|
|
# Splitter-Positionen wiederherstellen
|
|
|
|
|
|
if app_settings.splitter_sizes:
|
|
|
|
|
|
self.ui.splitter.setSizes(app_settings.splitter_sizes)
|
|
|
|
|
|
|
|
|
|
|
|
# TreeWidget-Spaltenbreiten wiederherstellen
|
|
|
|
|
|
if app_settings.tree_column_widths:
|
|
|
|
|
|
for col_idx, width in enumerate(app_settings.tree_column_widths):
|
|
|
|
|
|
if col_idx < self.ui.treeWidget.columnCount():
|
|
|
|
|
|
self.ui.treeWidget.setColumnWidth(col_idx, width)
|
|
|
|
|
|
|
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-12-20 19:39:23 +01:00
|
|
|
|
logger.debug(f"Verfügbare Themes: {available_themes}")
|
|
|
|
|
|
logger.debug(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
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-06-22 11:58:57 +02:00
|
|
|
|
# Projekte vorhanden - Menü aktivieren und Untermenü erstellen
|
|
|
|
|
|
self.ui.actionVorhandene_Projekte.setEnabled(True)
|
|
|
|
|
|
self.ui.actionVorhandene_Projekte.setText("Vorhandene Projekte")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-06-22 11:58:57 +02:00
|
|
|
|
# Erstelle ein Untermenü für die Projekte
|
|
|
|
|
|
projects_menu = QMenu(self)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-06-22 11:58:57 +02:00
|
|
|
|
# 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}")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-06-22 11:58:57 +02:00
|
|
|
|
# Verbinde die Aktion mit der Projekt-Öffnen-Funktion
|
2025-12-13 21:06:40 +01:00
|
|
|
|
project_action.triggered.connect(lambda checked, proj=project: self.open_existing_project(proj))
|
|
|
|
|
|
|
2025-06-22 11:58:57 +02:00
|
|
|
|
projects_menu.addAction(project_action)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-06-22 11:58:57 +02:00
|
|
|
|
# Setze das Untermenü für die Aktion
|
|
|
|
|
|
self.ui.actionVorhandene_Projekte.setMenu(projects_menu)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.info(f"Projekte-Menü initialisiert mit {len(app_settings.pdf_projects)} Projekten")
|
2025-06-22 11:58:57 +02:00
|
|
|
|
|
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.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-06-22 11:58:57 +02:00
|
|
|
|
Args:
|
|
|
|
|
|
project: Das zu öffnende PdfProject-Objekt
|
|
|
|
|
|
"""
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.info(f"Öffne Projekt: {project.name}")
|
|
|
|
|
|
logger.debug(f"Projekt-Ordner: {project.project_dir}")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
self.project = project
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-06-22 14:47:17 +02:00
|
|
|
|
try:
|
|
|
|
|
|
# Prüfe ob project.yaml existiert und nicht leer ist
|
2025-12-13 21:06:40 +01:00
|
|
|
|
project_yaml_path = Path(project.project_dir) / "project.yaml"
|
|
|
|
|
|
|
2025-06-22 14:47:17 +02:00
|
|
|
|
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)
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.info(f"Projekt-Einstellungen aus {project_yaml_path} geladen!")
|
2025-06-22 14:47:17 +02:00
|
|
|
|
else:
|
|
|
|
|
|
# Erstelle Standard-Projekt-Einstellungen wenn Datei leer oder nicht vorhanden
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.warning("project.yaml ist leer oder nicht vorhanden, erstelle Standard-Einstellungen")
|
2025-08-10 17:32:22 +02:00
|
|
|
|
self.pdf_project = ProjectData()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-06-22 14:47:17 +02:00
|
|
|
|
# Speichere die Standard-Einstellungen in die project.yaml
|
|
|
|
|
|
self.pdf_project.writeSettings(project_dir=project.project_dir)
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.info(f"Standard-Projekt-Einstellungen in {project_yaml_path} gespeichert")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-14 21:34:37 +01:00
|
|
|
|
# Lade die Nodes in das TreeWidget (inkl. Diff-PDF-Counts und Icons)
|
2025-07-27 18:33:14 +02:00
|
|
|
|
self._load_nodes_to_tree()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
# Starte Hash-Berechnung für alle XML-Dateien
|
|
|
|
|
|
self._start_xml_hash_calculation()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-28 16:46:39 +01:00
|
|
|
|
# Initialisiere Saxon-Worker-Pool für schnellere Transformationen
|
|
|
|
|
|
self._initialize_saxon_worker_pool()
|
|
|
|
|
|
|
2026-01-04 17:24:19 +01:00
|
|
|
|
# Initialisiere FOP-Worker-Pool für schnellere PDF-Generierung
|
|
|
|
|
|
self._initialize_fop_worker_pool()
|
|
|
|
|
|
|
2025-06-22 14:47:17 +02:00
|
|
|
|
except Exception as e:
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.error(f"Fehler beim Laden des Projekts '{project.name}': {e}")
|
2025-06-22 14:47:17 +02:00
|
|
|
|
# Fallback: Erstelle Standard-Einstellungen
|
|
|
|
|
|
try:
|
2025-08-10 17:32:22 +02:00
|
|
|
|
self.pdf_project = ProjectData()
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.info("Fallback: Standard-Projekt-Einstellungen erstellt")
|
2025-07-27 18:33:14 +02:00
|
|
|
|
# Auch bei Fallback die Nodes laden
|
|
|
|
|
|
self._load_nodes_to_tree()
|
2025-06-22 14:47:17 +02:00
|
|
|
|
except Exception as fallback_error:
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.error(f"Fehler beim Erstellen der Fallback-Einstellungen: {fallback_error}")
|
2025-06-22 11:58:57 +02:00
|
|
|
|
|
2025-12-28 16:46:39 +01:00
|
|
|
|
def _initialize_saxon_worker_pool(self):
|
|
|
|
|
|
"""Initialisiert den Saxon-Worker-Pool für schnelle Transformationen."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# Shutdown vorherigen Pool falls vorhanden
|
|
|
|
|
|
self._shutdown_saxon_worker_pool()
|
|
|
|
|
|
|
2025-12-28 17:30:20 +01:00
|
|
|
|
# Prüfe ob SaxonWorkerPool aktiviert ist
|
|
|
|
|
|
if not app_settings.use_saxon_worker_pool:
|
|
|
|
|
|
logger.info("SaxonWorkerPool deaktiviert - Verwende Fallback-Modus (subprocess)")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
2025-12-28 16:46:39 +01:00
|
|
|
|
if not self.project:
|
|
|
|
|
|
logger.warning("Kein Projekt geladen, Saxon-Worker-Pool nicht initialisiert")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Hole Tool-Konfigurationen
|
|
|
|
|
|
java_vm = next((vm for vm in app_settings.java_vms if vm.id == self.project.java_vm_id), None)
|
|
|
|
|
|
saxon_jar = next((jar for jar in app_settings.saxon_jars if jar.id == self.project.saxon_jar_id), None)
|
|
|
|
|
|
|
|
|
|
|
|
if not java_vm or not saxon_jar:
|
|
|
|
|
|
logger.warning("Java VM oder Saxon JAR nicht gefunden, Pool nicht initialisiert")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Erstelle Worker-Pool
|
|
|
|
|
|
num_workers = app_settings.max_workers
|
|
|
|
|
|
log_dir = self.project.project_dir / "temp"
|
|
|
|
|
|
pool = SaxonWorkerPool(
|
|
|
|
|
|
num_workers=num_workers,
|
|
|
|
|
|
java_vm_path=java_vm.path_to_binary_file,
|
|
|
|
|
|
saxon_jar_path=saxon_jar.path_to_jar_file,
|
|
|
|
|
|
classpath_cache=TransformationJob._classpath_cache,
|
|
|
|
|
|
log_dir=log_dir,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Setze globalen Pool
|
|
|
|
|
|
set_saxon_worker_pool(pool)
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(
|
|
|
|
|
|
f"Saxon-Worker-Pool initialisiert: {num_workers} Worker "
|
|
|
|
|
|
f"(erwartet: {num_workers}x schneller für Saxon-Transformationen)"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Fehler beim Initialisieren des Saxon-Worker-Pools: {e}")
|
2025-12-28 17:30:20 +01:00
|
|
|
|
logger.info("Fallback auf subprocess-Modus")
|
2025-12-28 16:46:39 +01:00
|
|
|
|
# Kein Pool ist OK - Fallback auf subprocess
|
|
|
|
|
|
|
|
|
|
|
|
def _shutdown_saxon_worker_pool(self):
|
|
|
|
|
|
"""Beendet den Saxon-Worker-Pool sauber."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# Importiere transform um Zugriff auf globalen Pool zu haben
|
|
|
|
|
|
import transform
|
|
|
|
|
|
|
|
|
|
|
|
if transform._saxon_worker_pool:
|
|
|
|
|
|
logger.info("Beende Saxon-Worker-Pool...")
|
|
|
|
|
|
transform._saxon_worker_pool.shutdown()
|
|
|
|
|
|
set_saxon_worker_pool(None)
|
|
|
|
|
|
logger.info("Saxon-Worker-Pool beendet")
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Fehler beim Beenden des Saxon-Worker-Pools: {e}")
|
|
|
|
|
|
|
2026-01-04 17:24:19 +01:00
|
|
|
|
def _initialize_fop_worker_pool(self):
|
|
|
|
|
|
"""Initialisiert den FOP-Worker-Pool für schnelle PDF-Generierung."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# Shutdown vorherigen Pool falls vorhanden
|
|
|
|
|
|
self._shutdown_fop_worker_pool()
|
|
|
|
|
|
|
|
|
|
|
|
# Prüfe ob FopWorkerPool aktiviert ist
|
|
|
|
|
|
if not app_settings.use_fop_worker_pool:
|
|
|
|
|
|
logger.info("FopWorkerPool deaktiviert - Verwende Fallback-Modus (subprocess)")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
if not self.project:
|
|
|
|
|
|
logger.warning("Kein Projekt geladen, FOP-Worker-Pool nicht initialisiert")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Hole Tool-Konfigurationen
|
|
|
|
|
|
java_vm = next((vm for vm in app_settings.java_vms if vm.id == self.project.java_vm_id), None)
|
|
|
|
|
|
apache_fop = next((fop for fop in app_settings.apache_fops if fop.id == self.project.apache_fop_id), None)
|
|
|
|
|
|
|
|
|
|
|
|
if not java_vm or not apache_fop:
|
|
|
|
|
|
logger.warning("Java VM oder Apache FOP nicht gefunden, Pool nicht initialisiert")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# FOP-Konfigurationsdatei (falls vorhanden)
|
|
|
|
|
|
fop_config_file = None
|
|
|
|
|
|
if self.project.fop_config_dir:
|
|
|
|
|
|
fop_config_file = self.project.fop_config_dir / "fop.xconf"
|
|
|
|
|
|
else:
|
|
|
|
|
|
default_config = apache_fop.path_to_dir / "conf" / "fop.xconf"
|
|
|
|
|
|
if default_config.exists():
|
|
|
|
|
|
fop_config_file = default_config
|
|
|
|
|
|
|
|
|
|
|
|
# Importiere FopWorkerPool
|
|
|
|
|
|
from fop_pool import FopWorkerPool
|
|
|
|
|
|
from transform import set_fop_worker_pool
|
|
|
|
|
|
|
|
|
|
|
|
# Erstelle Worker-Pool
|
|
|
|
|
|
num_workers = app_settings.max_workers
|
|
|
|
|
|
log_dir = self.project.project_dir / "temp"
|
|
|
|
|
|
pool = FopWorkerPool(
|
|
|
|
|
|
num_workers=num_workers,
|
|
|
|
|
|
java_vm_path=java_vm.path_to_binary_file,
|
|
|
|
|
|
apache_fop_dir=apache_fop.path_to_dir,
|
|
|
|
|
|
fop_config_file=fop_config_file,
|
|
|
|
|
|
log_dir=log_dir,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Setze globalen Pool
|
|
|
|
|
|
set_fop_worker_pool(pool)
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(
|
|
|
|
|
|
f"FOP-Worker-Pool initialisiert: {num_workers} Worker "
|
|
|
|
|
|
f"(erwartet: {num_workers}x schneller für PDF-Generierung)"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Fehler beim Initialisieren des FOP-Worker-Pools: {e}")
|
|
|
|
|
|
logger.info("Fallback auf subprocess-Modus")
|
|
|
|
|
|
# Kein Pool ist OK - Fallback auf subprocess
|
|
|
|
|
|
|
|
|
|
|
|
def _shutdown_fop_worker_pool(self):
|
|
|
|
|
|
"""Beendet den FOP-Worker-Pool sauber."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# Importiere transform um Zugriff auf globalen Pool zu haben
|
|
|
|
|
|
import transform
|
|
|
|
|
|
|
|
|
|
|
|
if transform._fop_worker_pool:
|
|
|
|
|
|
logger.info("Beende FOP-Worker-Pool...")
|
|
|
|
|
|
transform._fop_worker_pool.shutdown()
|
|
|
|
|
|
# Importiere set_fop_worker_pool
|
|
|
|
|
|
from transform import set_fop_worker_pool
|
|
|
|
|
|
|
|
|
|
|
|
set_fop_worker_pool(None)
|
|
|
|
|
|
logger.info("FOP-Worker-Pool beendet")
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Fehler beim Beenden des FOP-Worker-Pools: {e}")
|
|
|
|
|
|
|
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
|
|
|
|
|
|
"""
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.info(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-12-20 19:39:23 +01:00
|
|
|
|
logger.info(f"Theme erfolgreich gewechselt zu: {theme_name}")
|
2025-06-17 19:11:08 +02:00
|
|
|
|
app_settings.theme = theme_name
|
|
|
|
|
|
app_settings.save()
|
2025-05-27 20:48:21 +02:00
|
|
|
|
else:
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.error(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:
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.error(f"Fehler beim Wechseln des Themes: {e}")
|
2025-05-27 20:48:21 +02:00
|
|
|
|
|
2025-05-29 16:30:01 +02:00
|
|
|
|
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-12-13 21:06:40 +01:00
|
|
|
|
|
2025-05-29 16:30:01 +02:00
|
|
|
|
Args:
|
|
|
|
|
|
pdf_filename: Name der PDF-Datei
|
|
|
|
|
|
page_num: Seitennummer (0-basiert)
|
|
|
|
|
|
"""
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.debug(f"Rendere Seite {page_num + 1} von {pdf_filename}")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-05-29 16:30:01 +02:00
|
|
|
|
if pdf_filename not in self.pdf_documents:
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.warning(f"PDF-Dokument {pdf_filename} nicht gefunden")
|
2025-05-29 16:30:01 +02:00
|
|
|
|
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-12-13 21:06:40 +01:00
|
|
|
|
|
2025-05-29 21:21:18 +02:00
|
|
|
|
# Diff-Seite laden (bestimmt die Abmessungen)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
diff_doc = docs["diff"]
|
2025-05-31 21:27:58 +02:00
|
|
|
|
page_size = diff_doc.pagePointSize(page_num)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-05-31 21:27:58 +02:00
|
|
|
|
# Hohe Auflösung für Vollbild (entspricht ca. Matrix(2.0, 2.0) in PyMuPDF)
|
|
|
|
|
|
scale_factor = 2.0
|
2025-12-13 21:06:40 +01:00
|
|
|
|
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-12-13 21:06:40 +01: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-12-13 21:06:40 +01:00
|
|
|
|
ref_doc = docs["ref"]
|
2025-05-31 21:27:58 +02:00
|
|
|
|
if page_num < ref_doc.pageCount():
|
|
|
|
|
|
ref_image = ref_doc.render(page_num, render_size)
|
|
|
|
|
|
ref_pixmap = QPixmap.fromImage(ref_image)
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.debug(f"Ref-Seite {page_num + 1} gerendert")
|
2025-05-29 21:21:18 +02:00
|
|
|
|
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-12-20 19:39:23 +01:00
|
|
|
|
logger.debug(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-12-13 21:06:40 +01:00
|
|
|
|
new_doc = docs["new"]
|
2025-05-31 21:27:58 +02:00
|
|
|
|
if page_num < new_doc.pageCount():
|
|
|
|
|
|
new_image = new_doc.render(page_num, render_size)
|
|
|
|
|
|
new_pixmap = QPixmap.fromImage(new_image)
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.debug(f"New-Seite {page_num + 1} gerendert")
|
2025-05-29 21:21:18 +02:00
|
|
|
|
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-12-20 19:39:23 +01:00
|
|
|
|
logger.debug(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
|
2025-12-13 21:06:40 +01:00
|
|
|
|
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
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.debug(f"Performance: Seite {page_num + 1} gerendert in {render_time:.3f}s")
|
2025-05-29 16:30:01 +02:00
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
2025-12-14 20:12:40 +01:00
|
|
|
|
logger.error(f"Fehler beim Rendern der Seite {page_num + 1}: {e}", exc_info=True)
|
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:
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.warning("Keine gerenderten Pixmaps verfügbar")
|
2025-05-29 19:03:19 +02:00
|
|
|
|
return
|
|
|
|
|
|
|
2025-06-16 20:30:56 +02:00
|
|
|
|
if self.fullsize_label is None:
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.warning("Fullsize-Label ist nicht verfügbar")
|
2025-06-16 20:30:56 +02:00
|
|
|
|
return
|
|
|
|
|
|
|
2025-12-14 20:12:40 +01:00
|
|
|
|
try:
|
|
|
|
|
|
# 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)
|
|
|
|
|
|
except RuntimeError as e:
|
|
|
|
|
|
# C++-Objekt wurde bereits gelöscht
|
|
|
|
|
|
logger.warning(f"Fullsize-Label wurde bereits gelöscht: {e}")
|
|
|
|
|
|
self.fullsize_label = None
|
2025-05-29 19:03:19 +02:00
|
|
|
|
|
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-12-14 13:45:20 +01:00
|
|
|
|
ref_opacity = abs(alpha_value) / 100
|
|
|
|
|
|
diff_opacity = 1.0 - abs(alpha_value) / 100.0
|
2025-05-27 18:31:45 +02:00
|
|
|
|
new_opacity = 0.0
|
|
|
|
|
|
else:
|
|
|
|
|
|
ref_opacity = 0.0
|
2025-12-14 13:45:20 +01: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)
|
2025-06-01 16:23:54 +02:00
|
|
|
|
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)
|
2025-06-01 16:23:54 +02:00
|
|
|
|
self.ui.alpha.mouseDoubleClickEvent = lambda event: self.ui.alpha.setValue(0)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-06-14 12:30:39 +02:00
|
|
|
|
# Menü-Aktionen verbinden
|
2025-06-16 20:30:56 +02:00
|
|
|
|
self.ui.actionNeu.triggered.connect(self.open_new_project_dialog)
|
2025-06-14 12:30:39 +02:00
|
|
|
|
self.ui.actionEinstellungen.triggered.connect(self.open_settings_dialog)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
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-12-15 21:10:15 +01:00
|
|
|
|
# Button "Accept Changes" verbinden
|
|
|
|
|
|
self.ui.accept_changes.clicked.connect(self._on_accept_changes_clicked)
|
|
|
|
|
|
|
2026-01-02 20:11:56 +01:00
|
|
|
|
# Buttons zum Öffnen von PDFs im System-Viewer verbinden
|
|
|
|
|
|
self.ui.view_ref_pdf.clicked.connect(self._on_view_ref_pdf_clicked)
|
|
|
|
|
|
self.ui.view_new_pdf.clicked.connect(self._on_view_new_pdf_clicked)
|
|
|
|
|
|
|
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)
|
2025-12-26 13:25:22 +01:00
|
|
|
|
|
|
|
|
|
|
# Verbinde Selection-Changed-Signal für automatisches Laden von Diff-PDFs
|
|
|
|
|
|
self.ui.treeWidget.itemSelectionChanged.connect(self._on_tree_selection_changed)
|
|
|
|
|
|
|
|
|
|
|
|
logger.debug("Kontextmenü und Selection-Handler für TreeWidget eingerichtet")
|
2025-08-03 16:31:38 +02:00
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
2025-12-20 21:16:33 +01:00
|
|
|
|
/*
|
2025-08-14 20:32:29 +02:00
|
|
|
|
QTreeWidget::item:hover {
|
|
|
|
|
|
background-color: palette(alternate-base);
|
|
|
|
|
|
}
|
2025-12-20 21:16:33 +01:00
|
|
|
|
*/
|
2025-08-14 20:32:29 +02:00
|
|
|
|
QTreeWidget::branch {
|
|
|
|
|
|
/*margin: 2px 0px;*/
|
|
|
|
|
|
}
|
|
|
|
|
|
"""
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-14 20:32:29 +02:00
|
|
|
|
# Wende das Stylesheet auf das TreeWidget an
|
|
|
|
|
|
self.ui.treeWidget.setStyleSheet(tree_stylesheet)
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.debug("TreeWidget Styling für größeren vertikalen Abstand angewendet")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-14 20:32:29 +02:00
|
|
|
|
except Exception as e:
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.error(f"Fehler beim Anwenden des TreeWidget-Stylings: {e}")
|
2025-08-14 20:32:29 +02:00
|
|
|
|
|
2025-08-03 16:31:38 +02:00
|
|
|
|
def _show_tree_context_menu(self, position):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Zeigt das Kontextmenü für das TreeWidget an.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-03 16:31:38 +02:00
|
|
|
|
Args:
|
|
|
|
|
|
position: Position des Rechtsklicks
|
|
|
|
|
|
"""
|
|
|
|
|
|
# Hole das Item an der Position
|
|
|
|
|
|
item = self.ui.treeWidget.itemAt(position)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-03 17:04:23 +02:00
|
|
|
|
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-12-13 21:06:40 +01:00
|
|
|
|
|
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)
|
|
|
|
|
|
|
2025-12-26 13:25:22 +01:00
|
|
|
|
def _on_tree_selection_changed(self):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Handler für Änderungen der Tree-Selektion.
|
|
|
|
|
|
Lädt automatisch Diff-PDFs wenn ein XML-Knoten mit Diff-PDF ausgewählt wird.
|
|
|
|
|
|
Leert den Viewer wenn ein Knoten ohne Diff-PDF ausgewählt wird.
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
logger.debug("Tree-Selektion geändert")
|
|
|
|
|
|
|
|
|
|
|
|
# Hole aktuell selektierte Items
|
|
|
|
|
|
selected_items = self.ui.treeWidget.selectedItems()
|
|
|
|
|
|
|
|
|
|
|
|
if not selected_items or not self.project:
|
|
|
|
|
|
# Keine Selektion oder kein Projekt - Viewer leeren
|
2025-12-28 12:58:39 +01:00
|
|
|
|
logger.debug(
|
|
|
|
|
|
f"Keine Selektion oder kein Projekt: selected_items={len(selected_items) if selected_items else 0}, project={self.project is not None}"
|
|
|
|
|
|
)
|
2025-12-26 13:25:22 +01:00
|
|
|
|
if self.pdf_documents:
|
|
|
|
|
|
self._clear_pdf_viewer()
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Erstes selektiertes Item verwenden
|
|
|
|
|
|
item = selected_items[0]
|
|
|
|
|
|
|
|
|
|
|
|
# Prüfe ob es ein XML-Item ist
|
|
|
|
|
|
node_type = self._get_node_type_from_item(item)
|
|
|
|
|
|
logger.debug(f"Selektierter Node-Typ: {node_type}")
|
|
|
|
|
|
|
|
|
|
|
|
if node_type == "XmlFile":
|
|
|
|
|
|
# Hole XmlFile-Objekt und XSL-ID aus UserRole
|
|
|
|
|
|
xml_file_obj = item.data(0, Qt.ItemDataRole.UserRole)
|
|
|
|
|
|
xsl_id_str = item.data(1, Qt.ItemDataRole.UserRole)
|
|
|
|
|
|
|
|
|
|
|
|
logger.debug(f"XML-File-Daten: xml_file_obj={xml_file_obj}, xsl_id_str={xsl_id_str}")
|
|
|
|
|
|
|
|
|
|
|
|
if xml_file_obj and xsl_id_str:
|
|
|
|
|
|
# Extrahiere Pfad aus XmlFile-Objekt
|
|
|
|
|
|
xml_file_path = xml_file_obj.xml
|
|
|
|
|
|
|
|
|
|
|
|
# Prüfe ob Diff-PDF existiert
|
|
|
|
|
|
xml_stem = xml_file_path.stem
|
|
|
|
|
|
pdf_basename = f"{xml_stem}_xsl_{xsl_id_str}.pdf"
|
|
|
|
|
|
diff_pdf_path = self.project.project_dir / "diff" / pdf_basename
|
|
|
|
|
|
|
|
|
|
|
|
logger.debug(f"Prüfe Diff-PDF: {diff_pdf_path}, existiert={diff_pdf_path.exists()}")
|
|
|
|
|
|
|
|
|
|
|
|
if diff_pdf_path.exists():
|
|
|
|
|
|
# Diff-PDF vorhanden - automatisch laden
|
|
|
|
|
|
logger.info(f"XML-Knoten mit Diff-PDF ausgewählt: {pdf_basename}, lade automatisch")
|
|
|
|
|
|
self._load_pdf_for_comparison(xml_file_path, xsl_id_str)
|
|
|
|
|
|
else:
|
|
|
|
|
|
# Kein Diff-PDF - Viewer leeren falls noch ein PDF geladen ist
|
|
|
|
|
|
if self.pdf_documents:
|
2025-12-27 17:44:46 +01:00
|
|
|
|
logger.debug("XML-Knoten ohne Diff-PDF ausgewählt, leere Viewer")
|
2025-12-26 13:25:22 +01:00
|
|
|
|
self._clear_pdf_viewer()
|
|
|
|
|
|
else:
|
|
|
|
|
|
logger.debug("XML-File-Daten fehlen (xml_file_obj oder xsl_id_str ist None)")
|
|
|
|
|
|
else:
|
|
|
|
|
|
# Kein XML-Item - Viewer leeren falls noch ein PDF geladen ist
|
|
|
|
|
|
if self.pdf_documents:
|
|
|
|
|
|
logger.debug(f"Nicht-XML-Knoten ausgewählt ({node_type}), leere Viewer")
|
|
|
|
|
|
self._clear_pdf_viewer()
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Fehler beim Verarbeiten der Tree-Selektion: {e}", exc_info=True)
|
|
|
|
|
|
|
2025-08-03 16:31:38 +02:00
|
|
|
|
def _get_node_type_from_item(self, item):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Bestimmt den Node-Typ basierend auf dem TreeWidgetItem.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-03 16:31:38 +02:00
|
|
|
|
Args:
|
|
|
|
|
|
item: Das TreeWidgetItem
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-03 16:31:38 +02:00
|
|
|
|
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()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-03 16:31:38 +02:00
|
|
|
|
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)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-03 16:31:38 +02:00
|
|
|
|
except Exception as e:
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.error(f"Fehler beim Bestimmen des Node-Typs: {e}")
|
2025-08-03 16:31:38 +02:00
|
|
|
|
return "Unknown"
|
|
|
|
|
|
|
|
|
|
|
|
def _determine_node_type_from_data(self, item):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Bestimmt den Node-Typ basierend auf den gespeicherten Daten im Item.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-03 16:31:38 +02:00
|
|
|
|
Args:
|
|
|
|
|
|
item: Das TreeWidgetItem
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-03 16:31:38 +02:00
|
|
|
|
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-12-13 21:06:40 +01:00
|
|
|
|
|
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-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-03 16:31:38 +02:00
|
|
|
|
return "Unknown"
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-03 16:31:38 +02:00
|
|
|
|
except Exception as e:
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.error(f"Fehler beim Bestimmen des Node-Typs aus Daten: {e}")
|
2025-08-03 16:31:38 +02:00
|
|
|
|
return "Unknown"
|
|
|
|
|
|
|
2025-12-16 20:32:33 +01:00
|
|
|
|
def _find_item_by_node(self, node_obj):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Findet ein TreeWidgetItem basierend auf einem Node-Objekt.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
node_obj: Das Node-Objekt (TreeNode, XslFile oder XmlFile)
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
QTreeWidgetItem oder None wenn nicht gefunden
|
|
|
|
|
|
"""
|
2025-12-28 12:58:39 +01:00
|
|
|
|
|
2025-12-16 20:32:33 +01:00
|
|
|
|
def search_recursive(item):
|
|
|
|
|
|
"""Rekursive Suche durch TreeWidget."""
|
|
|
|
|
|
# Prüfe aktuelles Item
|
|
|
|
|
|
item_node = item.data(0, Qt.ItemDataRole.UserRole)
|
|
|
|
|
|
if item_node is node_obj:
|
|
|
|
|
|
return item
|
|
|
|
|
|
|
|
|
|
|
|
# Durchsuche Kinder
|
|
|
|
|
|
for i in range(item.childCount()):
|
|
|
|
|
|
child = item.child(i)
|
|
|
|
|
|
result = search_recursive(child)
|
|
|
|
|
|
if result:
|
|
|
|
|
|
return result
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
# Durchsuche alle Root-Items
|
|
|
|
|
|
for i in range(self.ui.treeWidget.topLevelItemCount()):
|
|
|
|
|
|
root_item = self.ui.treeWidget.topLevelItem(i)
|
|
|
|
|
|
result = search_recursive(root_item)
|
|
|
|
|
|
if result:
|
|
|
|
|
|
return result
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
2025-08-03 16:31:38 +02:00
|
|
|
|
def _find_node_by_id(self, nodes, target_id):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Sucht rekursiv nach einem Node mit der angegebenen ID.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-03 16:31:38 +02:00
|
|
|
|
Args:
|
|
|
|
|
|
nodes: Liste der Nodes zum Durchsuchen
|
|
|
|
|
|
target_id: Die zu suchende ID
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-03 16:31:38 +02:00
|
|
|
|
Returns:
|
|
|
|
|
|
TreeNode|XslFile|None: Der gefundene Node oder None
|
|
|
|
|
|
"""
|
|
|
|
|
|
for node in nodes:
|
|
|
|
|
|
if node.id == target_id:
|
|
|
|
|
|
return node
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
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
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-03 16:31:38 +02:00
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def _create_context_menu_for_type(self, node_type, item):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Erstellt das Kontextmenü für den angegebenen Node-Typ.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-03 16:31:38 +02:00
|
|
|
|
Args:
|
|
|
|
|
|
node_type: Der Typ des Nodes ('TreeNode', 'XslFile', 'XmlFile')
|
|
|
|
|
|
item: Das TreeWidgetItem
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-03 16:31:38 +02:00
|
|
|
|
Returns:
|
|
|
|
|
|
QMenu: Das erstellte Kontextmenü oder None
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
menu = QMenu(self)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-03 16:31:38 +02:00
|
|
|
|
if node_type == "TreeNode":
|
|
|
|
|
|
# Kontextmenü für TreeNode
|
|
|
|
|
|
action_add_child = QAction("Unterknoten hinzufügen", self)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
action_add_child.setIcon(QIcon(QIcon.fromTheme("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)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-03 16:31:38 +02:00
|
|
|
|
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)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-03 16:31:38 +02:00
|
|
|
|
menu.addSeparator()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-14 20:45:53 +01:00
|
|
|
|
# Transformations-Aktionen (nur aktiv wenn XML-Dateien vorhanden)
|
|
|
|
|
|
tree_node_obj = item.data(0, Qt.ItemDataRole.UserRole) if item else None
|
2025-12-27 17:28:04 +01:00
|
|
|
|
has_xml_files = bool(tree_node_obj and self._has_xml_files_recursive(tree_node_obj))
|
2025-12-14 20:45:53 +01:00
|
|
|
|
|
|
|
|
|
|
action_transform = QAction("Alle XML-Dateien transformieren", self)
|
|
|
|
|
|
action_transform.setIcon(QIcon(QIcon.fromTheme("system-run")))
|
|
|
|
|
|
action_transform.triggered.connect(lambda: self._transform_tree_node(item))
|
|
|
|
|
|
action_transform.setEnabled(has_xml_files)
|
|
|
|
|
|
menu.addAction(action_transform)
|
|
|
|
|
|
|
|
|
|
|
|
action_transform_force = QAction("Alle XML-Dateien neu transformieren (force)", self)
|
|
|
|
|
|
action_transform_force.setIcon(QIcon(QIcon.fromTheme("view-refresh")))
|
|
|
|
|
|
action_transform_force.triggered.connect(lambda: self._transform_tree_node(item, force=True))
|
|
|
|
|
|
action_transform_force.setEnabled(has_xml_files)
|
|
|
|
|
|
menu.addAction(action_transform_force)
|
|
|
|
|
|
|
|
|
|
|
|
menu.addSeparator()
|
|
|
|
|
|
|
2025-12-16 21:38:59 +01:00
|
|
|
|
# Aktion "Alle Änderungen übernehmen" (nur aktiv wenn Diff-PDFs vorhanden)
|
|
|
|
|
|
diff_pdfs = self._collect_all_diff_pdfs_under_node(tree_node_obj, item) if tree_node_obj else []
|
|
|
|
|
|
has_diff_pdfs = len(diff_pdfs) > 0
|
|
|
|
|
|
|
|
|
|
|
|
action_accept_all = QAction("Alle Änderungen übernehmen", self)
|
|
|
|
|
|
action_accept_all.setIcon(QIcon(QIcon.fromTheme("emblem-default")))
|
|
|
|
|
|
action_accept_all.triggered.connect(lambda: self._accept_all_changes_under_node(item))
|
|
|
|
|
|
action_accept_all.setEnabled(has_diff_pdfs)
|
|
|
|
|
|
menu.addAction(action_accept_all)
|
|
|
|
|
|
|
|
|
|
|
|
menu.addSeparator()
|
|
|
|
|
|
|
2025-08-03 16:31:38 +02:00
|
|
|
|
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)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-03 16:31:38 +02:00
|
|
|
|
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)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-03 16:31:38 +02:00
|
|
|
|
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)
|
2025-12-11 21:26:13 +01:00
|
|
|
|
|
2025-08-03 16:31:38 +02:00
|
|
|
|
menu.addSeparator()
|
2025-12-11 21:26:13 +01:00
|
|
|
|
|
2025-12-14 20:54:48 +01:00
|
|
|
|
# Transformations-Aktionen (nur aktiv wenn XML-Dateien vorhanden)
|
|
|
|
|
|
xsl_file_obj = item.data(0, Qt.ItemDataRole.UserRole) if item else None
|
|
|
|
|
|
has_xml_files = bool(xsl_file_obj and xsl_file_obj.xmls)
|
|
|
|
|
|
|
2025-12-11 21:26:13 +01:00
|
|
|
|
action_transform = QAction("Alle XML-Dateien transformieren", self)
|
|
|
|
|
|
action_transform.setIcon(QIcon(QIcon.fromTheme("system-run")))
|
|
|
|
|
|
action_transform.triggered.connect(lambda: self._transform_xsl_file(item))
|
2025-12-14 20:54:48 +01:00
|
|
|
|
action_transform.setEnabled(has_xml_files)
|
2025-12-11 21:26:13 +01:00
|
|
|
|
menu.addAction(action_transform)
|
|
|
|
|
|
|
|
|
|
|
|
action_transform_force = QAction("Alle XML-Dateien neu transformieren (force)", self)
|
|
|
|
|
|
action_transform_force.setIcon(QIcon(QIcon.fromTheme("view-refresh")))
|
|
|
|
|
|
action_transform_force.triggered.connect(lambda: self._transform_xsl_file(item, force=True))
|
2025-12-14 20:54:48 +01:00
|
|
|
|
action_transform_force.setEnabled(has_xml_files)
|
2025-12-11 21:26:13 +01:00
|
|
|
|
menu.addAction(action_transform_force)
|
|
|
|
|
|
|
|
|
|
|
|
menu.addSeparator()
|
|
|
|
|
|
|
2025-12-16 21:38:59 +01:00
|
|
|
|
# Aktion "Alle Änderungen übernehmen" (nur aktiv wenn Diff-PDFs vorhanden)
|
|
|
|
|
|
diff_pdfs = self._collect_all_diff_pdfs_under_node(xsl_file_obj, item) if xsl_file_obj else []
|
|
|
|
|
|
has_diff_pdfs = len(diff_pdfs) > 0
|
|
|
|
|
|
|
|
|
|
|
|
action_accept_all = QAction("Alle Änderungen übernehmen", self)
|
|
|
|
|
|
action_accept_all.setIcon(QIcon(QIcon.fromTheme("emblem-default")))
|
|
|
|
|
|
action_accept_all.triggered.connect(lambda: self._accept_all_changes_under_node(item))
|
|
|
|
|
|
action_accept_all.setEnabled(has_diff_pdfs)
|
|
|
|
|
|
menu.addAction(action_accept_all)
|
|
|
|
|
|
|
|
|
|
|
|
menu.addSeparator()
|
|
|
|
|
|
|
2025-08-03 16:31:38 +02:00
|
|
|
|
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)
|
2025-12-11 21:26:13 +01:00
|
|
|
|
|
2025-08-03 16:31:38 +02:00
|
|
|
|
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)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-03 16:31:38 +02:00
|
|
|
|
elif node_type == "XmlFile":
|
|
|
|
|
|
# Kontextmenü für XmlFile
|
2025-12-11 21:26:13 +01:00
|
|
|
|
# Transformations-Aktionen
|
|
|
|
|
|
action_transform = QAction("Transformieren", self)
|
|
|
|
|
|
action_transform.setIcon(QIcon(QIcon.fromTheme("system-run")))
|
|
|
|
|
|
action_transform.triggered.connect(lambda: self._transform_xml_file(item))
|
|
|
|
|
|
menu.addAction(action_transform)
|
|
|
|
|
|
|
|
|
|
|
|
action_transform_force = QAction("Neu transformieren (force)", self)
|
|
|
|
|
|
action_transform_force.setIcon(QIcon(QIcon.fromTheme("view-refresh")))
|
|
|
|
|
|
action_transform_force.triggered.connect(lambda: self._transform_xml_file(item, force=True))
|
|
|
|
|
|
menu.addAction(action_transform_force)
|
|
|
|
|
|
|
|
|
|
|
|
menu.addSeparator()
|
|
|
|
|
|
|
2025-08-03 16:31:38 +02:00
|
|
|
|
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)
|
2025-12-11 21:26:13 +01:00
|
|
|
|
|
2025-08-03 16:31:38 +02:00
|
|
|
|
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)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-03 16:31:38 +02:00
|
|
|
|
else:
|
2025-08-03 17:04:23 +02:00
|
|
|
|
# Unbekannter Typ oder leerer Bereich - Menü für Root-Elemente
|
|
|
|
|
|
action_add_tree_node = QAction("Unterknoten hinzufügen", self)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
action_add_tree_node.setIcon(QIcon(QIcon.fromTheme("folder-new")))
|
2025-08-03 17:04:23 +02:00
|
|
|
|
action_add_tree_node.triggered.connect(lambda: self._add_root_tree_node())
|
|
|
|
|
|
menu.addAction(action_add_tree_node)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-03 16:31:38 +02:00
|
|
|
|
return menu
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-03 16:31:38 +02:00
|
|
|
|
except Exception as e:
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.error(f"Fehler beim Erstellen des Kontextmenüs: {e}")
|
2025-08-03 16:31:38 +02:00
|
|
|
|
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.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-05-31 21:27:58 +02:00
|
|
|
|
Args:
|
|
|
|
|
|
alpha_value: Der neue Alpha-Wert (-100 bis 100)
|
|
|
|
|
|
"""
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.debug(f"Alpha geändert auf {alpha_value}")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-05-31 21:27:58 +02:00
|
|
|
|
start_time = time.time()
|
|
|
|
|
|
# Verwende gecachte Pixmaps für schnelle Alpha-Änderungen
|
|
|
|
|
|
self.update_current_display()
|
|
|
|
|
|
alpha_time = time.time() - start_time
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.debug(f"Alpha-Update in {alpha_time:.6f}s")
|
2025-05-31 21:27:58 +02:00
|
|
|
|
|
2025-06-14 12:30:39 +02:00
|
|
|
|
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
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.info("Einstellungen wurden gespeichert")
|
2025-06-14 12:30:39 +02:00
|
|
|
|
except Exception as e:
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.error(f"Fehler beim Öffnen des Einstellungen-Dialogs: {e}")
|
2025-06-14 12:30:39 +02:00
|
|
|
|
|
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()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-06-16 20:30:56 +02:00
|
|
|
|
# Erstelle neue ID für das Projekt
|
|
|
|
|
|
new_id = max([p.id for p in app_settings.pdf_projects], default=0) + 1
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-06-16 20:30:56 +02:00
|
|
|
|
# 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,
|
2025-12-13 21:06:40 +01:00
|
|
|
|
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,
|
2025-12-26 12:45:44 +01:00
|
|
|
|
fop_config_dir=Path(project_data["fop_config_dir"]) if project_data.get("fop_config_dir") else None,
|
2025-06-16 20:30:56 +02:00
|
|
|
|
)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-06-16 20:30:56 +02:00
|
|
|
|
# Erstelle Projekt-Ordnerstruktur
|
|
|
|
|
|
self._create_project_structure(new_project)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-06-16 20:30:56 +02:00
|
|
|
|
# Füge das neue Projekt zu app_settings hinzu
|
|
|
|
|
|
app_settings.pdf_projects.append(new_project)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-06-16 20:30:56 +02:00
|
|
|
|
# Speichere app_settings
|
|
|
|
|
|
app_settings.save()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.info(f"Neues PDF-Projekt '{project_data['name']}' wurde erstellt und gespeichert")
|
|
|
|
|
|
logger.debug(f"Projekt-ID: {new_id}")
|
|
|
|
|
|
logger.debug(f"Projekt-Ordner: {project_data['project_dir']}")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-06-22 11:58:57 +02:00
|
|
|
|
# Aktualisiere das Projekte-Menü
|
|
|
|
|
|
self._setup_projects_menu()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-06-16 20:30:56 +02:00
|
|
|
|
except Exception as e:
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.error(f"Fehler beim Erstellen des neuen Projekts: {e}")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
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.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-06-16 20:30:56 +02:00
|
|
|
|
Args:
|
|
|
|
|
|
project: Das PdfProject-Objekt
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
project_dir = Path(project.project_dir)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-06-16 20:30:56 +02:00
|
|
|
|
# Erstelle Unterordner
|
2025-12-13 21:06:40 +01:00
|
|
|
|
subdirs = ["xml", "new", "diff", "ref", "tmp"]
|
2025-06-16 20:30:56 +02:00
|
|
|
|
for subdir in subdirs:
|
|
|
|
|
|
subdir_path = project_dir / subdir
|
|
|
|
|
|
subdir_path.mkdir(parents=True, exist_ok=True)
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.debug(f"Ordner erstellt: {subdir_path}")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
|
|
|
|
|
project_yaml_path = project_dir / "project.yaml"
|
|
|
|
|
|
|
2025-06-22 14:47:17 +02:00
|
|
|
|
# Erstelle Standard-Projekt-Einstellungen und speichere sie
|
2025-06-16 20:30:56 +02:00
|
|
|
|
if not project_yaml_path.exists():
|
2025-06-22 14:47:17 +02:00
|
|
|
|
# Erstelle Standard-PdfProjectSettings
|
2025-08-10 17:32:22 +02:00
|
|
|
|
default_settings = ProjectData()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-06-22 14:47:17 +02:00
|
|
|
|
# Speichere die Standard-Einstellungen in die project.yaml
|
|
|
|
|
|
default_settings.writeSettings(project_dir=project_dir)
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.info(f"project.yaml mit Standard-Einstellungen erstellt: {project_yaml_path}")
|
2025-06-22 14:47:17 +02:00
|
|
|
|
else:
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.debug(f"project.yaml existiert bereits: {project_yaml_path}")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-06-16 20:30:56 +02:00
|
|
|
|
except Exception as e:
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.error(f"Fehler beim Erstellen der Projekt-Struktur: {e}")
|
2025-06-16 20:30:56 +02:00
|
|
|
|
raise
|
|
|
|
|
|
|
2025-05-22 19:10:39 +02:00
|
|
|
|
def on_button_clicked(self):
|
|
|
|
|
|
"""Wird ausgeführt, wenn der Button geklickt wird."""
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.debug("Button wurde geklickt!")
|
2025-05-23 11:09:47 +02:00
|
|
|
|
|
2025-05-22 19:10:39 +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
|
|
|
|
|
2025-05-22 19:10:39 +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:
|
2025-12-13 21:06:40 +01:00
|
|
|
|
pdf_filename = page_info["pdf_filename"]
|
|
|
|
|
|
page_num = page_info["page_num"]
|
|
|
|
|
|
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.debug(f"Thumbnail für Seite {page_num + 1} von {pdf_filename} wurde angeklickt")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-05-29 16:30:01 +02:00
|
|
|
|
# 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
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.debug(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
|
2025-05-29 20:14:37 +02:00
|
|
|
|
fullsize_label.setCursor(QCursor(Qt.CursorShape.OpenHandCursor))
|
2025-05-29 16:30:01 +02:00
|
|
|
|
|
2025-07-27 18:33:14 +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.
|
2025-07-27 18:33:14 +02:00
|
|
|
|
"""
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.info("Lade Nodes in TreeWidget...")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-07-27 18:33:14 +02:00
|
|
|
|
try:
|
|
|
|
|
|
# TreeWidget leeren
|
|
|
|
|
|
self.ui.treeWidget.clear()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
|
|
|
|
|
# Lösche XML-Item-Map
|
|
|
|
|
|
self.xml_item_map.clear()
|
|
|
|
|
|
|
2025-07-27 18:33:14 +02:00
|
|
|
|
# Prüfe ob pdf_project existiert und Nodes hat
|
2025-12-13 21:06:40 +01:00
|
|
|
|
if not hasattr(self, "pdf_project") or not self.pdf_project:
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.warning("Keine Projekt-Einstellungen verfügbar")
|
2025-07-27 18:33:14 +02:00
|
|
|
|
return
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-07-27 18:33:14 +02:00
|
|
|
|
if not self.pdf_project.nodes:
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.warning("Keine Nodes in den Projekt-Einstellungen gefunden")
|
2025-07-27 18:33:14 +02:00
|
|
|
|
return
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
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)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-19 20:29:56 +02:00
|
|
|
|
# Lade alle Root-Nodes (sortiert)
|
|
|
|
|
|
for node in sorted_nodes:
|
2025-07-27 18:33:14 +02:00
|
|
|
|
tree_item = self._create_tree_item_from_node(node)
|
|
|
|
|
|
self.ui.treeWidget.addTopLevelItem(tree_item)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.info(f"{len(self.pdf_project.nodes)} Root-Nodes in TreeWidget geladen (alphabetisch sortiert)")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-14 21:34:37 +01:00
|
|
|
|
# Aktualisiere Diff-PDF-Anzahl und Icons nach dem Laden
|
2025-12-14 21:11:40 +01:00
|
|
|
|
self._update_all_diff_pdf_counts()
|
2025-12-14 21:34:37 +01:00
|
|
|
|
self._update_diff_icons_for_existing_pdfs()
|
2025-12-14 21:11:40 +01:00
|
|
|
|
|
2025-07-27 18:33:14 +02:00
|
|
|
|
except Exception as e:
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.error(f"Fehler beim Laden der Nodes in TreeWidget: {e}")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-07-27 18:33:14 +02:00
|
|
|
|
def _create_tree_item_from_node(self, node):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Erstellt ein QTreeWidgetItem aus einem TreeNode oder XslFile.
|
2025-08-12 20:08:26 +02:00
|
|
|
|
Speichert die vollständigen Node-Daten für spätere Verwendung.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-07-27 18:33:14 +02:00
|
|
|
|
Args:
|
|
|
|
|
|
node: TreeNode oder XslFile Objekt
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-07-27 18:33:14 +02:00
|
|
|
|
Returns:
|
2025-08-12 20:08:26 +02:00
|
|
|
|
QTreeWidgetItem: Das erstellte Tree-Item mit vollständigen Node-Daten
|
2025-07-27 18:33:14 +02:00
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# Erstelle Tree-Item
|
|
|
|
|
|
item = QTreeWidgetItem()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-03 16:31:38 +02:00
|
|
|
|
# Setze die Bezeichnung in Spalte 0
|
2025-07-27 18:33:14 +02:00
|
|
|
|
bez_text = str(node.bez) if node.bez else ""
|
|
|
|
|
|
item.setText(0, bez_text)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-12 20:08:26 +02:00
|
|
|
|
# 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)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-07-27 18:33:14 +02:00
|
|
|
|
# Setze zusätzliche Informationen in Spalte 1
|
|
|
|
|
|
if isinstance(node, TreeNode):
|
2025-08-14 20:37:03 +02:00
|
|
|
|
# TreeNode: Zeige Anzahl der Knoten
|
2025-07-27 18:33:14 +02:00
|
|
|
|
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")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-12 20:08:26 +02:00
|
|
|
|
# Speichere zusätzlich die Node-ID in UserRole+1 für Kompatibilität
|
|
|
|
|
|
item.setData(0, Qt.ItemDataRole.UserRole + 1, node.id)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-19 20:29:56 +02:00
|
|
|
|
# Lade Knoten rekursiv (sortiert nach ID)
|
2025-07-27 18:33:14 +02:00
|
|
|
|
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:
|
2025-07-27 18:33:14 +02:00
|
|
|
|
child_item = self._create_tree_item_from_node(child)
|
|
|
|
|
|
item.addChild(child_item)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-14 21:11:40 +01:00
|
|
|
|
# Setze Diff-PDF-Anzahl in Spalte 2 (wird später aktualisiert)
|
|
|
|
|
|
diff_count = self._count_diff_pdfs_under_node(node, item)
|
|
|
|
|
|
if diff_count > 0:
|
|
|
|
|
|
item.setText(2, str(diff_count))
|
|
|
|
|
|
|
2025-07-27 18:33:14 +02:00
|
|
|
|
elif isinstance(node, XslFile):
|
|
|
|
|
|
# XslFile: Zeige XSL-Datei-Pfad
|
|
|
|
|
|
item.setText(1, str(node.xsl_file))
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-12 20:08:26 +02:00
|
|
|
|
# Speichere zusätzlich die Node-ID in UserRole+1 für Kompatibilität
|
|
|
|
|
|
item.setData(0, Qt.ItemDataRole.UserRole + 1, node.id)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-14 21:11:40 +01:00
|
|
|
|
# Setze Diff-PDF-Anzahl in Spalte 2 (wird später aktualisiert)
|
|
|
|
|
|
diff_count = self._count_diff_pdfs_under_node(node, item)
|
|
|
|
|
|
if diff_count > 0:
|
|
|
|
|
|
item.setText(2, str(diff_count))
|
|
|
|
|
|
|
2025-08-14 20:37:03 +02:00
|
|
|
|
# Lade XML-Dateien als Knoten
|
2025-07-27 18:33:14 +02:00
|
|
|
|
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))
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-12 20:08:26 +02:00
|
|
|
|
# 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}")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-26 13:25:22 +01:00
|
|
|
|
# Speichere XSL-ID in Spalte 1, UserRole für einfachen Zugriff
|
|
|
|
|
|
xsl_id_str = "_".join(str(x) for x in node.id)
|
|
|
|
|
|
xml_item.setData(1, Qt.ItemDataRole.UserRole, xsl_id_str)
|
|
|
|
|
|
|
2025-07-27 18:33:14 +02:00
|
|
|
|
item.addChild(xml_item)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
|
|
|
|
|
# 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}'")
|
|
|
|
|
|
|
2025-07-27 18:33:14 +02:00
|
|
|
|
return item
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-07-27 18:33:14 +02:00
|
|
|
|
except Exception as e:
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.error(f"Fehler beim Erstellen des Tree-Items: {e}")
|
2025-07-27 18:33:14 +02:00
|
|
|
|
# Fallback: Erstelle einfaches Item
|
|
|
|
|
|
fallback_item = QTreeWidgetItem()
|
|
|
|
|
|
fallback_item.setText(0, "Fehler beim Laden")
|
|
|
|
|
|
fallback_item.setText(1, str(e))
|
|
|
|
|
|
return fallback_item
|
|
|
|
|
|
|
2025-12-13 21:06:40 +01:00
|
|
|
|
def _create_centered_progress_bar(self) -> tuple[QWidget, QProgressBar]:
|
|
|
|
|
|
"""
|
2025-12-14 20:32:40 +01:00
|
|
|
|
Erstellt eine linksbündige Progress Bar in einem Container-Widget.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
tuple: (container_widget, progress_bar)
|
|
|
|
|
|
"""
|
|
|
|
|
|
# Container-Widget erstellen
|
|
|
|
|
|
container = QWidget()
|
|
|
|
|
|
layout = QHBoxLayout(container)
|
|
|
|
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
2025-12-14 20:32:40 +01:00
|
|
|
|
layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
|
|
2025-12-14 13:45:20 +01:00
|
|
|
|
def _create_centered_diff_icon(self, xml_file_path: Path, xsl_id_str: str) -> QWidget:
|
2025-12-13 21:06:40 +01:00
|
|
|
|
"""
|
2025-12-26 13:25:22 +01:00
|
|
|
|
Erstellt ein linksbündiges, nicht-klickbares Icon für Diff-PDF.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
|
|
|
|
|
Args:
|
2025-12-14 13:45:20 +01:00
|
|
|
|
xml_file_path: Pfad zur XML-Datei (relativ)
|
|
|
|
|
|
xsl_id_str: XSL-ID als String (z.B. "2002_1_128")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
|
|
|
|
|
Returns:
|
2025-12-26 13:25:22 +01:00
|
|
|
|
QWidget: Container mit Icon
|
2025-12-13 21:06:40 +01:00
|
|
|
|
"""
|
|
|
|
|
|
# Container-Widget
|
|
|
|
|
|
container = QWidget()
|
|
|
|
|
|
layout = QHBoxLayout(container)
|
|
|
|
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
2025-12-14 20:32:40 +01:00
|
|
|
|
layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
|
|
|
|
|
# Icon-Label
|
|
|
|
|
|
icon_label = QLabel()
|
2025-12-20 13:56:38 +01:00
|
|
|
|
# Icon für Diff-View mit Fallbacks
|
|
|
|
|
|
icon = QIcon.fromTheme("view-split-left-right")
|
|
|
|
|
|
if icon.isNull():
|
|
|
|
|
|
icon = QIcon.fromTheme("vcs-diff")
|
|
|
|
|
|
if icon.isNull():
|
|
|
|
|
|
icon = QIcon.fromTheme("system-search") # Letzter Fallback
|
|
|
|
|
|
icon_label.setPixmap(icon.pixmap(16, 16))
|
2025-12-26 13:25:22 +01:00
|
|
|
|
icon_label.setToolTip("Diff-PDF vorhanden (wird automatisch geladen bei Selektion)")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
|
|
|
|
|
layout.addWidget(icon_label)
|
|
|
|
|
|
|
|
|
|
|
|
return container
|
|
|
|
|
|
|
2025-12-14 13:45:20 +01:00
|
|
|
|
def _load_pdf_for_comparison(self, xml_file_path: Path, xsl_id_str: str):
|
2025-12-13 21:06:40 +01:00
|
|
|
|
"""
|
2025-12-14 13:45:20 +01:00
|
|
|
|
Lädt die PDFs (diff, ref, new) einer Transformation in den Vergleichs-Viewer.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
xml_file_path: Pfad zur XML-Datei (relativ)
|
2025-12-14 13:45:20 +01:00
|
|
|
|
xsl_id_str: XSL-ID als String (z.B. "2002_1_128")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
"""
|
|
|
|
|
|
try:
|
2025-12-14 13:45:20 +01:00
|
|
|
|
if not self.project:
|
|
|
|
|
|
QMessageBox.warning(self, "Fehler", "Kein Projekt geöffnet")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Ermittle PDF-Dateinamen basierend auf XML und XSL-ID
|
2025-12-13 21:06:40 +01:00
|
|
|
|
xml_stem = xml_file_path.stem
|
2025-12-14 13:45:20 +01:00
|
|
|
|
pdf_basename = f"{xml_stem}_xsl_{xsl_id_str}.pdf"
|
|
|
|
|
|
|
|
|
|
|
|
# Pfade zu den drei PDFs
|
2025-12-13 21:06:40 +01:00
|
|
|
|
diff_dir = self.project.project_dir / "diff"
|
2025-12-14 13:45:20 +01:00
|
|
|
|
ref_dir = self.project.project_dir / "ref"
|
|
|
|
|
|
new_dir = self.project.project_dir / "new"
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-14 13:45:20 +01:00
|
|
|
|
diff_pdf_path = diff_dir / pdf_basename
|
|
|
|
|
|
ref_pdf_path = ref_dir / pdf_basename
|
|
|
|
|
|
new_pdf_path = new_dir / pdf_basename
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-14 13:45:20 +01:00
|
|
|
|
# Prüfe ob PDFs existieren
|
|
|
|
|
|
if not diff_pdf_path.exists():
|
|
|
|
|
|
QMessageBox.information(self, "Keine Diff-PDF", f"Diff-PDF nicht gefunden:\n{pdf_basename}")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
return
|
|
|
|
|
|
|
2025-12-14 13:45:20 +01:00
|
|
|
|
if not ref_pdf_path.exists() or not new_pdf_path.exists():
|
|
|
|
|
|
QMessageBox.warning(
|
|
|
|
|
|
self,
|
|
|
|
|
|
"Fehlende PDFs",
|
|
|
|
|
|
f"Ref-PDF oder New-PDF nicht gefunden:\n{pdf_basename}\n\nNur Diff-PDF vorhanden.",
|
|
|
|
|
|
)
|
|
|
|
|
|
return
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-14 13:45:20 +01:00
|
|
|
|
logger.info(f"Lade PDFs für Vergleich: {pdf_basename}")
|
|
|
|
|
|
|
|
|
|
|
|
# Entferne bestehende Widgets aus den Layouts
|
|
|
|
|
|
self._clear_layout(self.ui.verticalLayout_2)
|
|
|
|
|
|
self._clear_layout(self.ui.verticalLayout_3)
|
|
|
|
|
|
|
|
|
|
|
|
# Dicts zurücksetzen
|
|
|
|
|
|
self.thumbnail_to_page = {}
|
|
|
|
|
|
self.pdf_documents = {}
|
|
|
|
|
|
self.current_rendered_pixmaps = None
|
2025-12-14 20:12:40 +01:00
|
|
|
|
self.fullsize_label = None # Label wurde durch _clear_layout gelöscht
|
2025-12-14 13:45:20 +01:00
|
|
|
|
|
|
|
|
|
|
# Alle drei PDF-Dateien öffnen mit QtPdf
|
|
|
|
|
|
diff_doc = QPdfDocument()
|
|
|
|
|
|
ref_doc = QPdfDocument()
|
|
|
|
|
|
new_doc = QPdfDocument()
|
|
|
|
|
|
|
|
|
|
|
|
# PDF-Dateien laden
|
|
|
|
|
|
diff_doc.load(str(diff_pdf_path))
|
|
|
|
|
|
ref_doc.load(str(ref_pdf_path))
|
|
|
|
|
|
new_doc.load(str(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
|
|
|
|
|
|
):
|
|
|
|
|
|
QMessageBox.critical(self, "Fehler", f"Fehler beim Laden der PDFs:\n{pdf_basename}")
|
|
|
|
|
|
return
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-14 13:45:20 +01:00
|
|
|
|
# PDF-Dokumente speichern
|
|
|
|
|
|
self.pdf_documents[pdf_basename] = {"diff": diff_doc, "ref": ref_doc, "new": new_doc}
|
|
|
|
|
|
|
2026-01-02 20:11:56 +01:00
|
|
|
|
# PDF-Pfade für System-Viewer speichern
|
|
|
|
|
|
self.current_ref_pdf_path = ref_pdf_path
|
|
|
|
|
|
self.current_new_pdf_path = new_pdf_path
|
|
|
|
|
|
|
|
|
|
|
|
# Buttons zum Öffnen der PDFs im System-Viewer aktivieren
|
|
|
|
|
|
self.ui.view_ref_pdf.setEnabled(True)
|
|
|
|
|
|
self.ui.view_new_pdf.setEnabled(True)
|
|
|
|
|
|
|
2026-01-02 20:22:29 +01:00
|
|
|
|
# Slider aktivieren
|
|
|
|
|
|
self.ui.alpha.setEnabled(True)
|
|
|
|
|
|
self.ui.zoom.setEnabled(True)
|
|
|
|
|
|
|
2025-12-14 13:45:20 +01:00
|
|
|
|
logger.info(f"PDFs geladen: {pdf_basename}")
|
|
|
|
|
|
logger.info(f" diff: {diff_doc.pageCount()} Seiten")
|
|
|
|
|
|
logger.info(f" ref: {ref_doc.pageCount()} Seiten")
|
|
|
|
|
|
logger.info(f" new: {new_doc.pageCount()} Seiten")
|
|
|
|
|
|
|
|
|
|
|
|
# Nehme die Seitenzahl der diff-PDF als Basis
|
|
|
|
|
|
max_pages = diff_doc.pageCount()
|
|
|
|
|
|
|
|
|
|
|
|
# Erstelle Thumbnails für alle Seiten
|
|
|
|
|
|
for page_num in range(max_pages):
|
|
|
|
|
|
# Nur diff-Seite für Thumbnail rendern
|
|
|
|
|
|
page_size = diff_doc.pagePointSize(page_num)
|
|
|
|
|
|
|
|
|
|
|
|
# Skalierung für Thumbnail
|
|
|
|
|
|
scale_factor = 200.0 / page_size.width() # 200 Pixel Breite
|
|
|
|
|
|
|
|
|
|
|
|
# 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)
|
|
|
|
|
|
|
|
|
|
|
|
# Thumbnail erstellen und zur linken Spalte hinzufügen
|
|
|
|
|
|
thumbnail = QLabel()
|
|
|
|
|
|
thumbnail.setObjectName(f"thumbnail_{pdf_basename}_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)
|
|
|
|
|
|
|
|
|
|
|
|
# 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)
|
|
|
|
|
|
|
|
|
|
|
|
# Beziehung zwischen Thumbnail und Seitennummer speichern
|
|
|
|
|
|
self.thumbnail_to_page[thumbnail] = {"pdf_filename": pdf_basename, "page_num": page_num}
|
|
|
|
|
|
|
|
|
|
|
|
# Click-Event für das Thumbnail einrichten
|
|
|
|
|
|
thumbnail.mousePressEvent = lambda event, t=thumbnail: self.on_thumbnail_clicked(event, t)
|
|
|
|
|
|
|
|
|
|
|
|
# Erstelle das Vollbild-Label für die rechte Spalte (falls noch nicht vorhanden)
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Setze die aktuelle PDF
|
|
|
|
|
|
self.current_pdf = pdf_basename
|
|
|
|
|
|
|
2025-12-15 21:10:15 +01:00
|
|
|
|
# Speichere Diff-PDF-Informationen für Accept Changes
|
|
|
|
|
|
self.current_diff_xml_path = xml_file_path
|
|
|
|
|
|
self.current_diff_xsl_id = xsl_id_str
|
|
|
|
|
|
|
|
|
|
|
|
# Aktiviere Accept-Changes-Button
|
|
|
|
|
|
self.ui.accept_changes.setEnabled(True)
|
|
|
|
|
|
|
2025-12-14 13:45:20 +01:00
|
|
|
|
# Zeige die erste Seite initial an
|
|
|
|
|
|
self.render_and_display_page(pdf_basename, 0)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-14 13:45:20 +01:00
|
|
|
|
logger.info(f"PDF-Vergleich geladen: {pdf_basename}")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
2025-12-14 13:45:20 +01:00
|
|
|
|
logger.error(f"Fehler beim Laden der PDFs für Vergleich: {e}")
|
|
|
|
|
|
QMessageBox.critical(self, "Fehler", f"Konnte PDFs nicht laden:\n{str(e)}")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
|
|
|
|
|
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
|
2025-12-14 13:45:20 +01:00
|
|
|
|
icon_widget = self._create_centered_diff_icon(xml_path, xsl_id_str)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
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")
|
|
|
|
|
|
|
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."""
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.debug(f"Unterknoten zu TreeNode hinzufügen: {parent_item.text(0)}")
|
2025-08-03 16:31:38 +02:00
|
|
|
|
# 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."""
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.debug(f"XSL-Datei zu TreeNode hinzufügen: {parent_item.text(0)}")
|
2025-08-03 16:31:38 +02:00
|
|
|
|
# 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.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-12 20:56:26 +02:00
|
|
|
|
Args:
|
|
|
|
|
|
item: Das TreeWidgetItem des TreeNode
|
|
|
|
|
|
"""
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.debug(f"TreeNode bearbeiten: {item.text(0)}")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
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
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:34:29 +01:00
|
|
|
|
# Prüfe ob Projekt verfügbar ist
|
|
|
|
|
|
if not self.pdf_project:
|
|
|
|
|
|
QMessageBox.warning(self, "Warnung", "Keine Projekt-Einstellungen verfügbar.")
|
|
|
|
|
|
return
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-12 20:56:26 +02:00
|
|
|
|
# Sammle Eltern-Parameter
|
|
|
|
|
|
parent_params = self._collect_parent_params(item)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-12 20:56:26 +02:00
|
|
|
|
# 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
|
2025-12-13 21:06:40 +01:00
|
|
|
|
node.bez = data["bez"]
|
|
|
|
|
|
node.xslt_params = data["xslt_params"]
|
|
|
|
|
|
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.info(f"TreeNode '{node.bez}' wurde aktualisiert")
|
|
|
|
|
|
logger.debug(f"XSLT-Parameter: {node.xslt_params}")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-12 20:56:26 +02:00
|
|
|
|
# Speichere die Änderungen
|
|
|
|
|
|
self._save_project_settings()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-12 20:56:26 +02:00
|
|
|
|
# Aktualisiere das TreeWidget
|
|
|
|
|
|
self._load_nodes_to_tree()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-16 20:32:33 +01:00
|
|
|
|
# Wenn Force-Transformation gewünscht, führe sie aus
|
|
|
|
|
|
if data.get("force_transform", False):
|
|
|
|
|
|
# Finde das neue Item nach dem Neuladen
|
|
|
|
|
|
new_item = self._find_item_by_node(node)
|
|
|
|
|
|
if new_item:
|
|
|
|
|
|
logger.info(f"Starte Force-Transformation für TreeNode '{node.bez}'")
|
|
|
|
|
|
self._transform_tree_node(new_item, force=True)
|
|
|
|
|
|
else:
|
|
|
|
|
|
logger.warning(f"Konnte Item für TreeNode '{node.bez}' nicht finden")
|
|
|
|
|
|
|
2025-08-14 20:47:48 +02:00
|
|
|
|
# QMessageBox.information(self, "Erfolg", "TreeNode wurde erfolgreich aktualisiert.")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-12 20:56:26 +02:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
error_msg = f"Fehler beim Bearbeiten des TreeNode: {str(e)}"
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.error(error_msg)
|
2025-08-12 20:56:26 +02:00
|
|
|
|
QMessageBox.critical(self, "Fehler", error_msg)
|
2025-08-03 16:31:38 +02:00
|
|
|
|
|
|
|
|
|
|
def _delete_tree_node(self, item):
|
|
|
|
|
|
"""Löscht einen TreeNode."""
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.debug(f"TreeNode löschen: {item.text(0)}")
|
2025-08-03 16:31:38 +02:00
|
|
|
|
# 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.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 17:32:22 +02:00
|
|
|
|
Args:
|
|
|
|
|
|
parent_item: Das TreeWidgetItem des XslFile-Nodes
|
|
|
|
|
|
"""
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.debug(f"XML-Datei zu XslFile hinzufügen: {parent_item.text(0)}")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 17:32:22 +02:00
|
|
|
|
try:
|
|
|
|
|
|
# Prüfe ob ein Projekt geladen ist
|
2025-12-13 21:06:40 +01:00
|
|
|
|
if not hasattr(self, "project") or not self.project:
|
2025-08-10 17:32:22 +02:00
|
|
|
|
QMessageBox.warning(self, "Warnung", "Kein Projekt geladen. Bitte öffnen Sie zuerst ein Projekt.")
|
|
|
|
|
|
return
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
|
|
|
|
|
if not hasattr(self, "pdf_project") or not self.pdf_project:
|
2025-08-10 17:32:22 +02:00
|
|
|
|
QMessageBox.warning(self, "Warnung", "Keine Projekt-Einstellungen geladen.")
|
|
|
|
|
|
return
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
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
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 17:32:22 +02:00
|
|
|
|
# Öffne Datei-Dialog zum Auswählen der XML-Datei
|
|
|
|
|
|
xml_file_path, _ = QFileDialog.getOpenFileName(
|
2025-12-13 21:06:40 +01:00
|
|
|
|
self, "XML-Datei auswählen", "", "XML-Dateien (*.xml);;Alle Dateien (*)"
|
2025-08-10 17:32:22 +02:00
|
|
|
|
)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 17:32:22 +02:00
|
|
|
|
if not xml_file_path:
|
|
|
|
|
|
# Benutzer hat abgebrochen
|
|
|
|
|
|
return
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 17:32:22 +02:00
|
|
|
|
xml_file_path = Path(xml_file_path)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 17:32:22 +02:00
|
|
|
|
# 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
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 17:32:22 +02:00
|
|
|
|
# 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)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 17:32:22 +02:00
|
|
|
|
# Bestimme den Ziel-Pfad in xml-Ordner
|
|
|
|
|
|
target_xml_path = xml_dir / xml_file_path.name
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 17:32:22 +02:00
|
|
|
|
# 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,
|
2025-12-13 21:06:40 +01:00
|
|
|
|
QMessageBox.StandardButton.No,
|
2025-08-10 17:32:22 +02:00
|
|
|
|
)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 17:32:22 +02:00
|
|
|
|
if reply != QMessageBox.StandardButton.Yes:
|
|
|
|
|
|
return
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 17:32:22 +02:00
|
|
|
|
# Kopiere die XML-Datei in den xml-Ordner
|
|
|
|
|
|
shutil.copy2(xml_file_path, target_xml_path)
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.info(f"XML-Datei kopiert: {xml_file_path} -> {target_xml_path}")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 17:32:22 +02:00
|
|
|
|
# Erstelle relatives Path zur XML-Datei (relativ zum xml-Ordner)
|
|
|
|
|
|
relative_xml_path = Path("xml") / xml_file_path.name
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 17:32:22 +02:00
|
|
|
|
# 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
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 17:32:22 +02:00
|
|
|
|
if existing_xml:
|
|
|
|
|
|
QMessageBox.information(
|
|
|
|
|
|
self,
|
|
|
|
|
|
"XML-Datei bereits vorhanden",
|
2025-12-13 21:06:40 +01:00
|
|
|
|
f"Die XML-Datei '{xml_file_path.name}' ist bereits in dieser XSL-Datei enthalten.",
|
2025-08-10 17:32:22 +02:00
|
|
|
|
)
|
|
|
|
|
|
return
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 17:32:22 +02:00
|
|
|
|
# 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)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.info(f"XML-Datei '{xml_file_path.name}' zu XslFile-Node '{xsl_node.bez}' hinzugefügt")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
# Berechne Hash für die neue XML-Datei
|
|
|
|
|
|
self._calculate_hash_for_xml_file(new_xml_file)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 17:32:22 +02:00
|
|
|
|
# Speichere die aktualisierten Projekt-Einstellungen
|
|
|
|
|
|
self._save_project_settings()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 17:32:22 +02:00
|
|
|
|
# Aktualisiere das TreeWidget
|
|
|
|
|
|
self._load_nodes_to_tree()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-14 20:47:48 +02:00
|
|
|
|
# QMessageBox.information(
|
|
|
|
|
|
# self,
|
|
|
|
|
|
# "Erfolg",
|
|
|
|
|
|
# f"XML-Datei '{xml_file_path.name}' wurde erfolgreich hinzugefügt und in den xml-Ordner kopiert."
|
|
|
|
|
|
# )
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 17:32:22 +02:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
error_msg = f"Fehler beim Hinzufügen der XML-Datei: {str(e)}"
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.error(error_msg)
|
2025-08-10 17:32:22 +02:00
|
|
|
|
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.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-12 20:56:26 +02:00
|
|
|
|
Args:
|
|
|
|
|
|
item: Das TreeWidgetItem des XslFile
|
|
|
|
|
|
"""
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.debug(f"XslFile bearbeiten: {item.text(0)}")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
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
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-12 20:56:26 +02:00
|
|
|
|
# Sammle Eltern-Parameter
|
|
|
|
|
|
parent_params = self._collect_parent_params(item)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-12 20:56:26 +02:00
|
|
|
|
# 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
|
2025-12-13 21:06:40 +01:00
|
|
|
|
node.bez = data["bez"]
|
|
|
|
|
|
node.xslt_params = data["xslt_params"]
|
|
|
|
|
|
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.info(f"XslFile '{node.bez}' wurde aktualisiert")
|
|
|
|
|
|
logger.debug(f"XSLT-Parameter: {node.xslt_params}")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-12 20:56:26 +02:00
|
|
|
|
# Speichere die Änderungen
|
|
|
|
|
|
self._save_project_settings()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-12 20:56:26 +02:00
|
|
|
|
# Aktualisiere das TreeWidget
|
|
|
|
|
|
self._load_nodes_to_tree()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-16 20:32:33 +01:00
|
|
|
|
# Wenn Force-Transformation gewünscht, führe sie aus
|
|
|
|
|
|
if data.get("force_transform", False):
|
|
|
|
|
|
# Finde das neue Item nach dem Neuladen
|
|
|
|
|
|
new_item = self._find_item_by_node(node)
|
|
|
|
|
|
if new_item:
|
|
|
|
|
|
logger.info(f"Starte Force-Transformation für XslFile '{node.bez}'")
|
|
|
|
|
|
self._transform_xsl_file(new_item, force=True)
|
|
|
|
|
|
else:
|
|
|
|
|
|
logger.warning(f"Konnte Item für XslFile '{node.bez}' nicht finden")
|
|
|
|
|
|
|
2025-08-14 20:47:48 +02:00
|
|
|
|
# QMessageBox.information(self, "Erfolg", "XSL-Datei wurde erfolgreich aktualisiert.")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-12 20:56:26 +02:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
error_msg = f"Fehler beim Bearbeiten der XSL-Datei: {str(e)}"
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.error(error_msg)
|
2025-08-12 20:56:26 +02:00
|
|
|
|
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."""
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.debug(f"XslFile löschen: {item.text(0)}")
|
2025-08-03 16:31:38 +02:00
|
|
|
|
# TODO: Bestätigungsdialog und Löschung implementieren
|
|
|
|
|
|
|
|
|
|
|
|
# Kontextmenü-Aktionen für XmlFile
|
|
|
|
|
|
def _edit_xml_file(self, item):
|
|
|
|
|
|
"""Bearbeitet eine XML-Datei."""
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.debug(f"XmlFile bearbeiten: {item.text(0)}")
|
2025-08-03 16:31:38 +02:00
|
|
|
|
# TODO: Dialog zum Bearbeiten der XML-Datei öffnen
|
|
|
|
|
|
|
|
|
|
|
|
def _delete_xml_file(self, item):
|
2025-08-31 17:50:06 +02:00
|
|
|
|
"""
|
|
|
|
|
|
Löscht eine XML-Datei aus einem XSL-Knoten.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:50:06 +02:00
|
|
|
|
Args:
|
|
|
|
|
|
item: Das TreeWidgetItem der XML-Datei
|
|
|
|
|
|
"""
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.debug(f"XmlFile löschen: {item.text(0)}")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:50:06 +02:00
|
|
|
|
try:
|
|
|
|
|
|
# Prüfe ob ein Projekt geladen ist
|
2025-12-13 21:06:40 +01:00
|
|
|
|
if not hasattr(self, "project") or not self.project:
|
2025-08-31 17:50:06 +02:00
|
|
|
|
QMessageBox.warning(self, "Warnung", "Kein Projekt geladen. Bitte öffnen Sie zuerst ein Projekt.")
|
|
|
|
|
|
return
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
|
|
|
|
|
if not hasattr(self, "pdf_project") or not self.pdf_project:
|
2025-08-31 17:50:06 +02:00
|
|
|
|
QMessageBox.warning(self, "Warnung", "Keine Projekt-Einstellungen geladen.")
|
|
|
|
|
|
return
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:50:06 +02:00
|
|
|
|
# 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
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:50:06 +02:00
|
|
|
|
# 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
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:50:06 +02:00
|
|
|
|
# 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
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:50:06 +02:00
|
|
|
|
# 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,
|
2025-12-13 21:06:40 +01:00
|
|
|
|
QMessageBox.StandardButton.No,
|
2025-08-31 17:50:06 +02:00
|
|
|
|
)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:50:06 +02:00
|
|
|
|
if reply != QMessageBox.StandardButton.Yes:
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.debug("Löschung abgebrochen")
|
2025-08-31 17:50:06 +02:00
|
|
|
|
return
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:50:06 +02:00
|
|
|
|
# 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)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:50:06 +02:00
|
|
|
|
if xml_files_before == xml_files_after:
|
|
|
|
|
|
QMessageBox.warning(self, "Warnung", "XML-Datei konnte nicht aus der XSL-Datei entfernt werden.")
|
|
|
|
|
|
return
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.info(f"XML-Datei '{xml_filename}' aus XSL-Datei '{xsl_file_obj.bez}' entfernt")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:50:06 +02:00
|
|
|
|
# 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)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:50:06 +02:00
|
|
|
|
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,
|
2025-12-13 21:06:40 +01:00
|
|
|
|
QMessageBox.StandardButton.No,
|
2025-08-31 17:50:06 +02:00
|
|
|
|
)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:50:06 +02:00
|
|
|
|
if delete_reply == QMessageBox.StandardButton.Yes:
|
|
|
|
|
|
try:
|
|
|
|
|
|
xml_file_path.unlink()
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.info(f"Physische XML-Datei gelöscht: {xml_file_path}")
|
2025-08-31 17:50:06 +02:00
|
|
|
|
except Exception as e:
|
2025-12-13 21:06:40 +01:00
|
|
|
|
QMessageBox.warning(self, "Warnung", f"Fehler beim Löschen der physischen Datei:\n{str(e)}")
|
2025-08-31 17:50:06 +02:00
|
|
|
|
else:
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.info(
|
2025-12-13 21:06:40 +01:00
|
|
|
|
f"XML-Datei '{xml_filename}' wird noch in anderen XSL-Dateien verwendet - physische Datei nicht gelöscht"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-08-31 17:50:06 +02:00
|
|
|
|
# Speichere die aktualisierten Projekt-Einstellungen
|
|
|
|
|
|
self._save_project_settings()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:50:06 +02:00
|
|
|
|
# Aktualisiere das TreeWidget
|
|
|
|
|
|
self._load_nodes_to_tree()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.info(f"XML-Datei '{xml_filename}' erfolgreich entfernt")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:50:06 +02:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
error_msg = f"Fehler beim Löschen der XML-Datei: {str(e)}"
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.error(error_msg)
|
2025-08-31 17:50:06 +02:00
|
|
|
|
QMessageBox.critical(self, "Fehler", error_msg)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:50:06 +02:00
|
|
|
|
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.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:50:06 +02:00
|
|
|
|
Args:
|
|
|
|
|
|
xml_path: Pfad zur XML-Datei (relativ)
|
|
|
|
|
|
exclude_xsl_file: XSL-Datei die ausgeschlossen werden soll
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:50:06 +02:00
|
|
|
|
Returns:
|
|
|
|
|
|
bool: True wenn die XML-Datei noch anderswo verwendet wird
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
2025-12-07 20:34:29 +01:00
|
|
|
|
# Prüfe ob pdf_project und nodes existieren
|
|
|
|
|
|
if not self.pdf_project or not self.pdf_project.nodes:
|
|
|
|
|
|
return False # Keine Nodes vorhanden, also nicht verwendet
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:50:06 +02:00
|
|
|
|
return self._check_xml_usage_recursive(self.pdf_project.nodes, xml_path, exclude_xsl_file)
|
|
|
|
|
|
except Exception as e:
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.error(f"Fehler beim Prüfen der XML-Datei-Verwendung: {e}")
|
2025-08-31 17:50:06 +02:00
|
|
|
|
return True # Im Zweifelsfall annehmen, dass sie verwendet wird
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:50:06 +02:00
|
|
|
|
def _check_xml_usage_recursive(self, nodes, xml_path, exclude_xsl_file):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Prüft rekursiv ob eine XML-Datei in den Nodes verwendet wird.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:50:06 +02:00
|
|
|
|
Args:
|
|
|
|
|
|
nodes: Liste der zu prüfenden Nodes
|
|
|
|
|
|
xml_path: Pfad zur XML-Datei (relativ)
|
|
|
|
|
|
exclude_xsl_file: XSL-Datei die ausgeschlossen werden soll
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:50:06 +02:00
|
|
|
|
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
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:50:06 +02:00
|
|
|
|
return False
|
2025-08-03 16:31:38 +02:00
|
|
|
|
|
2025-08-03 17:04:23 +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."""
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.debug("Neuen TreeNode als Root-Element hinzufügen")
|
2025-08-03 17:04:23 +02:00
|
|
|
|
# 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.
|
|
|
|
|
|
"""
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.debug("Button 'lade aus FN2' wurde geklickt!")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
try:
|
|
|
|
|
|
# Prüfe ob ein Projekt geladen ist
|
2025-12-13 21:06:40 +01:00
|
|
|
|
if not hasattr(self, "project") or not self.project:
|
2025-08-10 14:03:15 +02:00
|
|
|
|
QMessageBox.warning(self, "Warnung", "Kein Projekt geladen. Bitte öffnen Sie zuerst ein Projekt.")
|
|
|
|
|
|
return
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
# Hole das aktuelle Projekt aus app_settings
|
|
|
|
|
|
if not self.project:
|
|
|
|
|
|
QMessageBox.warning(self, "Warnung", "Aktuelles Projekt nicht in den Einstellungen gefunden.")
|
|
|
|
|
|
return
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
# 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
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
# Führe SQL-Abfrage aus
|
|
|
|
|
|
df = self._execute_sql_query(db_config)
|
|
|
|
|
|
if df is None:
|
|
|
|
|
|
return # Fehler bereits angezeigt
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
# Verarbeite die Daten wie in readCsv.py
|
|
|
|
|
|
new_nodes = self._process_sql_data(df)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
# Merge mit vorhandenen Nodes
|
|
|
|
|
|
self._merge_nodes_with_existing(new_nodes)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
# Speichere die aktualisierten Projekt-Einstellungen
|
|
|
|
|
|
self._save_project_settings()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
# Lade das Projekt neu
|
|
|
|
|
|
self._load_nodes_to_tree()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-14 20:47:48 +02:00
|
|
|
|
# QMessageBox.information(self, "Erfolg", "Daten erfolgreich aus FN2 geladen und Projekt aktualisiert!")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
except Exception as e:
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.error(f"Fehler beim Laden aus FN2: {e}")
|
2025-08-10 14:03:15 +02:00
|
|
|
|
QMessageBox.critical(self, "Fehler", f"Fehler beim Laden aus FN2:\n{str(e)}")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
def _get_database_config(self, db_id):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Holt die Datenbank-Konfiguration anhand der ID.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
Args:
|
|
|
|
|
|
db_id: ID der PostgreSQL-Datenbank
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
Returns:
|
|
|
|
|
|
PostgreSqlDb|None: Die Datenbank-Konfiguration oder None
|
|
|
|
|
|
"""
|
|
|
|
|
|
for db in app_settings.postgresql_dbs:
|
|
|
|
|
|
if db.id == db_id:
|
|
|
|
|
|
return db
|
|
|
|
|
|
return None
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
def _execute_sql_query(self, db_config):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Führt die SQL-Abfrage aus der data.sql Datei aus.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
Args:
|
|
|
|
|
|
db_config: PostgreSQL-Datenbank-Konfiguration
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
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
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
|
|
|
|
|
with open(sql_file_path, "r", encoding="utf-8") as f:
|
2025-08-10 14:03:15 +02:00
|
|
|
|
sql_query = f.read()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.debug(f"SQL-Abfrage geladen: {len(sql_query)} Zeichen")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
# Verbindung zur PostgreSQL-Datenbank herstellen
|
2025-12-13 21:06:40 +01:00
|
|
|
|
connection_string = (
|
|
|
|
|
|
"postgresql://"
|
2025-08-10 14:03:15 +02:00
|
|
|
|
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}"
|
|
|
|
|
|
)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.info(f"Verbinde zu PostgreSQL: {db_config.host}:{db_config.port}/{db_config.database}")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
|
|
|
|
|
df = pl.read_database_uri(sql_query, connection_string, engine="connectorx").sort(
|
|
|
|
|
|
["reporttyp_bez", "report_bez", "repfile_bez"]
|
|
|
|
|
|
)
|
2025-08-10 14:03:15 +02:00
|
|
|
|
return df
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
error_msg = f"Fehler beim Ausführen der SQL-Abfrage: {str(e)}"
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.error(error_msg)
|
2025-08-10 14:03:15 +02:00
|
|
|
|
QMessageBox.critical(self, "Fehler", error_msg)
|
|
|
|
|
|
return None
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
def _process_sql_data(self, df):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Verarbeitet die SQL-Daten wie in readCsv.py und erstellt Node-Struktur.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
Args:
|
|
|
|
|
|
df: Polars DataFrame mit den SQL-Ergebnissen
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
Returns:
|
|
|
|
|
|
list[TreeNode]: Liste der erstellten Root-Nodes
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
start_time = time.time()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
# 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()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
group_time = time.time() - start_time
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.debug(f"Performance: Gruppierung in {group_time:.3f}s")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
new_nodes = []
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
start_time = time.time()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
# 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=[])
|
2025-12-13 21:06:40 +01:00
|
|
|
|
r2_children = ebene_3.filter(
|
|
|
|
|
|
(pl.col("reporttyp") == r1["reporttyp"]) & (pl.col("report") == r2["report"])
|
|
|
|
|
|
)
|
2025-08-10 14:03:15 +02:00
|
|
|
|
|
|
|
|
|
|
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"]),
|
2025-12-13 21:06:40 +01:00
|
|
|
|
xmls=[],
|
2025-08-10 14:03:15 +02:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
tn_2.children.append(x)
|
|
|
|
|
|
tn_1.children.append(tn_2)
|
|
|
|
|
|
new_nodes.append(tn_1)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
nodes_time = time.time() - start_time
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.debug(f"Performance: Node-Erstellung in {nodes_time:.3f}s")
|
|
|
|
|
|
logger.info(f"Erstellt: {len(new_nodes)} Root-Nodes")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
return new_nodes
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
except Exception as e:
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.error(f"Fehler beim Verarbeiten der SQL-Daten: {e}")
|
2025-08-10 14:03:15 +02:00
|
|
|
|
raise
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
def _merge_nodes_with_existing(self, new_nodes):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Merged neue Nodes mit vorhandenen Nodes basierend auf IDs.
|
|
|
|
|
|
Überschreibt nur einzelne Eigenschaften, nicht ganze Nodes.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
Args:
|
|
|
|
|
|
new_nodes: Liste der neuen Nodes
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.info("Merge neue Nodes mit vorhandenen...")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
# Erstelle ein Dictionary der neuen Nodes für schnellen Zugriff
|
|
|
|
|
|
new_nodes_dict = {}
|
|
|
|
|
|
self._build_nodes_dict(new_nodes, new_nodes_dict)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.debug(f"Neue Nodes Dictionary erstellt: {len(new_nodes_dict)} Einträge")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
# Merge mit vorhandenen Nodes
|
2025-12-07 20:34:29 +01:00
|
|
|
|
if self.pdf_project and self.pdf_project.nodes:
|
2025-08-10 14:03:15 +02:00
|
|
|
|
self._merge_nodes_recursive(self.pdf_project.nodes, new_nodes_dict)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
# Füge komplett neue Root-Nodes hinzu
|
2025-12-07 20:34:29 +01:00
|
|
|
|
if self.pdf_project and self.pdf_project.nodes:
|
|
|
|
|
|
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)
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.info(f"Neue Root-Node hinzugefügt: {new_node.bez}")
|
2025-12-07 20:34:29 +01:00
|
|
|
|
elif self.pdf_project:
|
|
|
|
|
|
# Wenn keine Nodes vorhanden sind, füge alle neuen Nodes hinzu
|
|
|
|
|
|
self.pdf_project.nodes = new_nodes
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.info(f"Alle {len(new_nodes)} Root-Nodes hinzugefügt (keine vorhandenen Nodes)")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.info("Merge abgeschlossen")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
except Exception as e:
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.error(f"Fehler beim Mergen der Nodes: {e}")
|
2025-08-10 14:03:15 +02:00
|
|
|
|
raise
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
def _build_nodes_dict(self, nodes, nodes_dict):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Erstellt rekursiv ein Dictionary aller Nodes für schnellen ID-basierten Zugriff.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
Args:
|
|
|
|
|
|
nodes: Liste der Nodes
|
|
|
|
|
|
nodes_dict: Dictionary zum Füllen
|
|
|
|
|
|
"""
|
|
|
|
|
|
for node in nodes:
|
|
|
|
|
|
nodes_dict[node.id] = node
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
if isinstance(node, TreeNode) and node.children:
|
|
|
|
|
|
self._build_nodes_dict(node.children, nodes_dict)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
def _merge_nodes_recursive(self, existing_nodes, new_nodes_dict):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Merged rekursiv vorhandene Nodes mit neuen Nodes.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
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]
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
# Aktualisiere nur die Bezeichnung, falls sie sich geändert hat
|
|
|
|
|
|
if existing_node.bez != new_node.bez:
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.info(
|
2025-12-13 21:06:40 +01:00
|
|
|
|
f"Aktualisiere Bezeichnung für Node {existing_node.id}: '{existing_node.bez}' -> '{new_node.bez}'"
|
|
|
|
|
|
)
|
2025-08-10 14:03:15 +02:00
|
|
|
|
existing_node.bez = new_node.bez
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
# 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:
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.info(
|
2025-12-13 21:06:40 +01:00
|
|
|
|
f"Aktualisiere XSL-Datei für Node {existing_node.id}: '{existing_node.xsl_file}' -> '{new_node.xsl_file}'"
|
|
|
|
|
|
)
|
2025-08-10 14:03:15 +02:00
|
|
|
|
existing_node.xsl_file = new_node.xsl_file
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
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-12-13 21:06:40 +01:00
|
|
|
|
|
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)
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.info(f"Neues Kind hinzugefügt zu Node {existing_node.id}: {new_child.bez}")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-12 20:56:26 +02:00
|
|
|
|
def _collect_parent_params(self, item):
|
|
|
|
|
|
"""
|
2025-12-14 15:59:37 +01:00
|
|
|
|
Sammelt die XSLT-Parameter aller Eltern-Nodes von der Wurzel bis zum angegebenen Item.
|
|
|
|
|
|
|
|
|
|
|
|
Parameter werden von oben nach unten gesammelt, wobei tiefere Ebenen höhere Priorität haben.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-12 20:56:26 +02:00
|
|
|
|
Args:
|
2025-12-14 15:59:37 +01:00
|
|
|
|
item: Das TreeWidgetItem (kann TreeNode oder XslFile sein)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-12 20:56:26 +02:00
|
|
|
|
Returns:
|
2025-12-14 15:59:37 +01:00
|
|
|
|
dict: Dictionary mit allen gesammelten Parametern (tiefere Ebenen überschreiben höhere)
|
2025-08-12 20:56:26 +02:00
|
|
|
|
"""
|
|
|
|
|
|
parent_params = {}
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-12 20:56:26 +02:00
|
|
|
|
try:
|
2025-12-14 15:59:37 +01:00
|
|
|
|
# Sammle alle Eltern-Items in einer Liste (von unten nach oben)
|
|
|
|
|
|
parents = []
|
2025-08-12 20:56:26 +02:00
|
|
|
|
current_item = item.parent()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-12 20:56:26 +02:00
|
|
|
|
while current_item:
|
2025-12-14 15:59:37 +01:00
|
|
|
|
parents.append(current_item)
|
|
|
|
|
|
current_item = current_item.parent()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-14 15:59:37 +01:00
|
|
|
|
# Kehre Liste um, sodass wir von Wurzel zu Kind iterieren
|
|
|
|
|
|
parents.reverse()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-14 15:59:37 +01:00
|
|
|
|
# Sammle Parameter von Wurzel zu Kind (Kind überschreibt Eltern)
|
|
|
|
|
|
for parent_item in parents:
|
|
|
|
|
|
parent_node = parent_item.data(0, Qt.ItemDataRole.UserRole)
|
|
|
|
|
|
|
|
|
|
|
|
if parent_node and hasattr(parent_node, "xslt_params") and parent_node.xslt_params:
|
|
|
|
|
|
# Update überschreibt vorherige Werte (höhere Priorität für tiefere Ebenen)
|
|
|
|
|
|
parent_params.update(parent_node.xslt_params)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-14 15:59:37 +01:00
|
|
|
|
logger.debug(f"Gesammelte Eltern-Parameter: {parent_params}")
|
2025-08-12 20:56:26 +02:00
|
|
|
|
return parent_params
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-12 20:56:26 +02:00
|
|
|
|
except Exception as e:
|
2025-12-14 15:59:37 +01:00
|
|
|
|
logger.error(f"Fehler beim Sammeln der Eltern-Parameter: {e}")
|
2025-08-12 20:56:26 +02:00
|
|
|
|
return {}
|
|
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
def _save_project_settings(self):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Speichert die aktualisierten Projekt-Einstellungen.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
Args:
|
|
|
|
|
|
current_project: Das aktuelle Projekt
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
2025-12-07 20:34:29 +01:00
|
|
|
|
# Prüfe ob pdf_project und project existieren
|
|
|
|
|
|
if not self.pdf_project:
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.warning("Keine Projekt-Einstellungen zum Speichern verfügbar")
|
2025-12-07 20:34:29 +01:00
|
|
|
|
return
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:34:29 +01:00
|
|
|
|
if not self.project or not self.project.project_dir:
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.warning("Kein Projekt-Verzeichnis zum Speichern verfügbar")
|
2025-12-07 20:34:29 +01:00
|
|
|
|
return
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
start_time = time.time()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
# Speichere in project.yaml im Projekt-Verzeichnis
|
|
|
|
|
|
self.pdf_project.writeSettings(project_dir=self.project.project_dir)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
dump_time = time.time() - start_time
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.debug(f"Performance: Projekt-Einstellungen gespeichert in {dump_time:.3f}s")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
|
except Exception as e:
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.error(f"Fehler beim Speichern der Projekt-Einstellungen: {e}")
|
2025-08-10 14:03:15 +02:00
|
|
|
|
raise
|
|
|
|
|
|
|
2025-08-31 17:04:22 +02:00
|
|
|
|
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)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:04:22 +02:00
|
|
|
|
# Ü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
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.debug("Drag&Drop für TreeWidget aktiviert")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:04:22 +02:00
|
|
|
|
except Exception as e:
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.error(f"Fehler beim Aktivieren von Drag&Drop: {e}")
|
2025-08-31 17:04:22 +02:00
|
|
|
|
|
2026-01-02 21:06:37 +01:00
|
|
|
|
def _setup_scroll_area_zoom(self):
|
|
|
|
|
|
"""Aktiviert Zoom per STRG+Mausrad für die PDF-ScrollArea."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# Installiere Event-Filter für scrollArea_2 (PDF-Viewer)
|
|
|
|
|
|
self.ui.scrollArea_2.installEventFilter(self)
|
|
|
|
|
|
logger.debug("Zoom per STRG+Mausrad für PDF-Viewer aktiviert")
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Fehler beim Aktivieren von Scroll-Area-Zoom: {e}")
|
|
|
|
|
|
|
2025-08-31 17:04:22 +02:00
|
|
|
|
def tree_drag_enter_event(self, event: QDragEnterEvent):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Wird ausgeführt, wenn ein Drag-Vorgang über das TreeWidget beginnt.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:04:22 +02:00
|
|
|
|
Args:
|
|
|
|
|
|
event: Das Drag-Enter-Event
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# Prüfe ob URLs (Dateien) gedraggt werden
|
|
|
|
|
|
if event.mimeData().hasUrls():
|
|
|
|
|
|
urls = event.mimeData().urls()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:04:22 +02:00
|
|
|
|
# Prüfe ob mindestens eine XML-Datei dabei ist
|
2025-12-13 21:06:40 +01:00
|
|
|
|
xml_files = [url.toLocalFile() for url in urls if url.toLocalFile().lower().endswith(".xml")]
|
|
|
|
|
|
|
2025-08-31 17:04:22 +02:00
|
|
|
|
if xml_files:
|
|
|
|
|
|
event.acceptProposedAction()
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.debug(f"Drag-Enter akzeptiert: {len(xml_files)} XML-Dateien")
|
2025-08-31 17:04:22 +02:00
|
|
|
|
else:
|
|
|
|
|
|
event.ignore()
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.debug("Drag-Enter ignoriert: Keine XML-Dateien")
|
2025-08-31 17:04:22 +02:00
|
|
|
|
else:
|
|
|
|
|
|
event.ignore()
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.debug("Drag-Enter ignoriert: Keine URLs")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:04:22 +02:00
|
|
|
|
except Exception as e:
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.error(f"Fehler in tree_drag_enter_event: {e}")
|
2025-08-31 17:04:22 +02:00
|
|
|
|
event.ignore()
|
|
|
|
|
|
|
|
|
|
|
|
def tree_drag_move_event(self, event):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Wird ausgeführt, wenn ein Drag-Vorgang über das TreeWidget bewegt wird.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:04:22 +02:00
|
|
|
|
Args:
|
|
|
|
|
|
event: Das Drag-Move-Event
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# Prüfe ob URLs (Dateien) gedraggt werden
|
|
|
|
|
|
if event.mimeData().hasUrls():
|
|
|
|
|
|
urls = event.mimeData().urls()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:04:22 +02:00
|
|
|
|
# Prüfe ob mindestens eine XML-Datei dabei ist
|
2025-12-13 21:06:40 +01:00
|
|
|
|
xml_files = [url.toLocalFile() for url in urls if url.toLocalFile().lower().endswith(".xml")]
|
|
|
|
|
|
|
2025-08-31 17:04:22 +02:00
|
|
|
|
if xml_files:
|
|
|
|
|
|
event.acceptProposedAction()
|
|
|
|
|
|
else:
|
|
|
|
|
|
event.ignore()
|
|
|
|
|
|
else:
|
|
|
|
|
|
event.ignore()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:04:22 +02:00
|
|
|
|
except Exception as e:
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.error(f"Fehler in tree_drag_move_event: {e}")
|
2025-08-31 17:04:22 +02:00
|
|
|
|
event.ignore()
|
|
|
|
|
|
|
|
|
|
|
|
def tree_drop_event(self, event: QDropEvent):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Wird ausgeführt, wenn Dateien auf das TreeWidget gedroppt werden.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:04:22 +02:00
|
|
|
|
Args:
|
|
|
|
|
|
event: Das Drop-Event
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# Prüfe ob ein Projekt geladen ist
|
2025-12-13 21:06:40 +01:00
|
|
|
|
if not hasattr(self, "project") or not self.project:
|
2025-08-31 17:04:22 +02:00
|
|
|
|
QMessageBox.warning(self, "Warnung", "Kein Projekt geladen. Bitte öffnen Sie zuerst ein Projekt.")
|
|
|
|
|
|
event.ignore()
|
|
|
|
|
|
return
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
|
|
|
|
|
if not hasattr(self, "pdf_project") or not self.pdf_project:
|
2025-08-31 17:04:22 +02:00
|
|
|
|
QMessageBox.warning(self, "Warnung", "Keine Projekt-Einstellungen geladen.")
|
|
|
|
|
|
event.ignore()
|
|
|
|
|
|
return
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:04:22 +02:00
|
|
|
|
# Hole die URLs aus dem Drop-Event
|
|
|
|
|
|
if not event.mimeData().hasUrls():
|
|
|
|
|
|
event.ignore()
|
|
|
|
|
|
return
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:04:22 +02:00
|
|
|
|
urls = event.mimeData().urls()
|
|
|
|
|
|
xml_files = []
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:04:22 +02:00
|
|
|
|
# Sammle alle XML-Dateien
|
|
|
|
|
|
for url in urls:
|
|
|
|
|
|
file_path = url.toLocalFile()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
if file_path.lower().endswith(".xml"):
|
2025-08-31 17:04:22 +02:00
|
|
|
|
xml_files.append(Path(file_path))
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:04:22 +02:00
|
|
|
|
if not xml_files:
|
|
|
|
|
|
QMessageBox.information(self, "Information", "Keine XML-Dateien zum Hinzufügen gefunden.")
|
|
|
|
|
|
event.ignore()
|
|
|
|
|
|
return
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.info(f"Drop-Event: {len(xml_files)} XML-Dateien erkannt")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-27 20:01:46 +01:00
|
|
|
|
# Verarbeite alle XML-Dateien mit optionalem "Alle zuordnen" Feature
|
|
|
|
|
|
self._handle_multiple_xml_files_drop(xml_files)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:04:22 +02:00
|
|
|
|
event.acceptProposedAction()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:04:22 +02:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
error_msg = f"Fehler beim Verarbeiten des Drop-Events: {str(e)}"
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.error(error_msg)
|
2025-08-31 17:04:22 +02:00
|
|
|
|
QMessageBox.critical(self, "Fehler", error_msg)
|
|
|
|
|
|
event.ignore()
|
|
|
|
|
|
|
2025-12-27 20:01:46 +01:00
|
|
|
|
def _handle_multiple_xml_files_drop(self, xml_files: list):
|
|
|
|
|
|
"""
|
2025-12-27 20:31:54 +01:00
|
|
|
|
Verarbeitet mehrere XML-Dateien asynchron per Drag&Drop.
|
|
|
|
|
|
Zeigt einen Dialog zur Auswahl der XSL-Knoten und startet dann die Batch-Verarbeitung im Hintergrund.
|
2025-12-27 20:01:46 +01:00
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
xml_files: Liste von Pfaden zu XML-Dateien
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not xml_files:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
# Prüfe ob Projekt-Nodes verfügbar sind
|
|
|
|
|
|
if not self.pdf_project or not self.pdf_project.nodes:
|
|
|
|
|
|
QMessageBox.warning(self, "Warnung", "Keine Projekt-Nodes verfügbar für die Zuordnung.")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
2025-12-27 20:31:54 +01:00
|
|
|
|
# Zeige Dialog für die erste Datei
|
2025-12-28 12:58:39 +01:00
|
|
|
|
dialog = XmlToXslAssignDialog(parent=self, xml_file_path=xml_files[0], project_nodes=self.pdf_project.nodes)
|
2025-12-27 20:01:46 +01:00
|
|
|
|
|
2025-12-27 20:31:54 +01:00
|
|
|
|
if dialog.exec() != XmlToXslAssignDialog.DialogCode.Accepted:
|
|
|
|
|
|
logger.debug("Dialog abgebrochen - keine Dateien verarbeitet")
|
|
|
|
|
|
return
|
2025-12-27 20:01:46 +01:00
|
|
|
|
|
2025-12-27 20:31:54 +01:00
|
|
|
|
# Hole die ausgewählten XSL-Knoten
|
|
|
|
|
|
selected_xsl_nodes = dialog.get_selected_xsl_nodes()
|
2025-12-27 20:01:46 +01:00
|
|
|
|
|
2025-12-27 20:31:54 +01:00
|
|
|
|
if not selected_xsl_nodes:
|
|
|
|
|
|
logger.warning("Keine XSL-Knoten ausgewählt")
|
|
|
|
|
|
return
|
2025-12-27 20:01:46 +01:00
|
|
|
|
|
2025-12-27 20:31:54 +01:00
|
|
|
|
# Prüfe ob "Alle zuordnen" aktiviert wurde
|
|
|
|
|
|
apply_to_all = dialog.is_apply_to_all()
|
2025-12-27 20:01:46 +01:00
|
|
|
|
|
2025-12-27 20:31:54 +01:00
|
|
|
|
# Bestimme welche Dateien verarbeitet werden sollen
|
|
|
|
|
|
files_to_process = xml_files if apply_to_all else [xml_files[0]]
|
2025-12-27 20:01:46 +01:00
|
|
|
|
|
2025-12-27 20:31:54 +01:00
|
|
|
|
# Stoppe vorherigen Batch-Thread falls noch aktiv
|
|
|
|
|
|
if self.batch_processing_thread and self.batch_processing_thread.isRunning():
|
|
|
|
|
|
self.batch_processing_thread.quit()
|
|
|
|
|
|
self.batch_processing_thread.wait()
|
|
|
|
|
|
|
2025-12-28 13:06:12 +01:00
|
|
|
|
# Zusätzliche Sicherheitsprüfung für project_dir
|
|
|
|
|
|
if not self.project or not self.project.project_dir:
|
|
|
|
|
|
QMessageBox.warning(self, "Fehler", "Projekt-Verzeichnis ist nicht verfügbar")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
2025-12-27 20:31:54 +01:00
|
|
|
|
# Erstelle und starte neuen Batch-Verarbeitungs-Thread
|
|
|
|
|
|
self.batch_processing_thread = XmlBatchProcessingThread(
|
|
|
|
|
|
xml_files=files_to_process,
|
|
|
|
|
|
selected_xsl_nodes=selected_xsl_nodes,
|
|
|
|
|
|
project_dir=Path(self.project.project_dir),
|
|
|
|
|
|
pdf_project=self.pdf_project,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Verbinde Signale
|
|
|
|
|
|
self.batch_processing_thread.progress_update.connect(self._on_batch_progress_update)
|
|
|
|
|
|
self.batch_processing_thread.processing_finished.connect(self._on_batch_processing_finished)
|
|
|
|
|
|
|
|
|
|
|
|
# Zeige Progressbar
|
|
|
|
|
|
self._show_batch_progress_bar(len(files_to_process))
|
|
|
|
|
|
|
|
|
|
|
|
# Starte Thread
|
|
|
|
|
|
self.batch_processing_thread.start()
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(
|
|
|
|
|
|
f"Batch-Verarbeitung von {len(files_to_process)} Datei(en) gestartet (apply_to_all={apply_to_all})"
|
|
|
|
|
|
)
|
2025-12-27 20:01:46 +01:00
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
2025-12-27 20:31:54 +01:00
|
|
|
|
error_msg = f"Fehler beim Starten der Batch-Verarbeitung: {str(e)}"
|
2025-12-27 20:01:46 +01:00
|
|
|
|
logger.error(error_msg)
|
|
|
|
|
|
QMessageBox.critical(self, "Fehler", error_msg)
|
|
|
|
|
|
|
2025-12-27 20:31:54 +01:00
|
|
|
|
def _show_batch_progress_bar(self, total_files: int):
|
2025-12-27 20:01:46 +01:00
|
|
|
|
"""
|
2025-12-27 20:31:54 +01:00
|
|
|
|
Zeigt einen Progressbar in der Statusbar für die Batch-Verarbeitung.
|
2025-12-27 20:01:46 +01:00
|
|
|
|
|
|
|
|
|
|
Args:
|
2025-12-27 20:31:54 +01:00
|
|
|
|
total_files: Gesamtanzahl der zu verarbeitenden Dateien
|
2025-12-27 20:01:46 +01:00
|
|
|
|
"""
|
2025-12-27 20:31:54 +01:00
|
|
|
|
if self.batch_progress_bar is None:
|
|
|
|
|
|
self.batch_progress_bar = QProgressBar()
|
|
|
|
|
|
self.batch_progress_bar.setMaximumHeight(20)
|
|
|
|
|
|
self.batch_progress_bar.setMaximumWidth(300)
|
2025-12-27 20:01:46 +01:00
|
|
|
|
|
2025-12-27 20:31:54 +01:00
|
|
|
|
self.batch_progress_bar.setMinimum(0)
|
|
|
|
|
|
self.batch_progress_bar.setMaximum(total_files)
|
|
|
|
|
|
self.batch_progress_bar.setValue(0)
|
|
|
|
|
|
self.batch_progress_bar.setFormat("%v/%m Dateien")
|
|
|
|
|
|
|
|
|
|
|
|
# Füge Progressbar zur Statusbar hinzu
|
|
|
|
|
|
self.statusBar().addPermanentWidget(self.batch_progress_bar)
|
|
|
|
|
|
self.batch_progress_bar.show()
|
|
|
|
|
|
|
|
|
|
|
|
def _hide_batch_progress_bar(self):
|
|
|
|
|
|
"""Versteckt und entfernt den Progressbar aus der Statusbar."""
|
|
|
|
|
|
if self.batch_progress_bar:
|
|
|
|
|
|
self.statusBar().removeWidget(self.batch_progress_bar)
|
|
|
|
|
|
self.batch_progress_bar.hide()
|
|
|
|
|
|
|
|
|
|
|
|
def _on_batch_progress_update(self, current: int, total: int, current_file: str):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Wird aufgerufen wenn der Batch-Thread einen Fortschritt meldet.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
current: Aktuelle Dateinummer
|
|
|
|
|
|
total: Gesamtanzahl der Dateien
|
|
|
|
|
|
current_file: Name der aktuellen Datei
|
|
|
|
|
|
"""
|
|
|
|
|
|
if self.batch_progress_bar:
|
|
|
|
|
|
self.batch_progress_bar.setValue(current)
|
|
|
|
|
|
|
|
|
|
|
|
self.statusBar().showMessage(f"Verarbeite: {current_file} ({current}/{total})")
|
|
|
|
|
|
|
|
|
|
|
|
def _on_batch_processing_finished(self, stats: dict):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Wird aufgerufen wenn die Batch-Verarbeitung abgeschlossen ist.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
stats: Statistik-Dictionary mit Verarbeitungsergebnissen
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# Verstecke Progressbar
|
|
|
|
|
|
self._hide_batch_progress_bar()
|
|
|
|
|
|
|
|
|
|
|
|
# Speichere Projekt-Einstellungen
|
|
|
|
|
|
if stats["processed"] > 0:
|
|
|
|
|
|
self._save_project_settings()
|
|
|
|
|
|
|
|
|
|
|
|
# Aktualisiere Tree
|
|
|
|
|
|
self._load_nodes_to_tree()
|
|
|
|
|
|
|
|
|
|
|
|
# Zeige Zusammenfassungsdialog
|
|
|
|
|
|
self._show_drop_summary_dialog(stats)
|
|
|
|
|
|
|
|
|
|
|
|
# Statusbar-Nachricht
|
|
|
|
|
|
self.statusBar().showMessage(
|
|
|
|
|
|
f"Batch-Verarbeitung abgeschlossen: {stats['processed']}/{stats['total']} Dateien", 5000
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Fehler beim Abschließen der Batch-Verarbeitung: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
def _show_transformation_progress_bar(self, total_jobs: int):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Zeigt einen Progressbar in der Statusbar für Transformationen.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
total_jobs: Gesamtanzahl der Transformations-Jobs
|
|
|
|
|
|
"""
|
|
|
|
|
|
if self.transformation_progress_bar is None:
|
|
|
|
|
|
self.transformation_progress_bar = QProgressBar()
|
|
|
|
|
|
self.transformation_progress_bar.setMaximumHeight(20)
|
|
|
|
|
|
self.transformation_progress_bar.setMaximumWidth(300)
|
|
|
|
|
|
|
|
|
|
|
|
self.transformation_progress_bar.setMinimum(0)
|
|
|
|
|
|
self.transformation_progress_bar.setMaximum(total_jobs)
|
|
|
|
|
|
self.transformation_progress_bar.setValue(0)
|
|
|
|
|
|
self.transformation_progress_bar.setFormat("%v/%m Jobs")
|
|
|
|
|
|
|
|
|
|
|
|
# Füge Progressbar zur Statusbar hinzu
|
|
|
|
|
|
self.statusBar().addPermanentWidget(self.transformation_progress_bar)
|
|
|
|
|
|
self.transformation_progress_bar.show()
|
|
|
|
|
|
|
|
|
|
|
|
def _hide_transformation_progress_bar(self):
|
|
|
|
|
|
"""Versteckt und entfernt den Transformation-Progressbar aus der Statusbar."""
|
|
|
|
|
|
if self.transformation_progress_bar:
|
|
|
|
|
|
self.statusBar().removeWidget(self.transformation_progress_bar)
|
|
|
|
|
|
self.transformation_progress_bar.hide()
|
|
|
|
|
|
|
|
|
|
|
|
def _update_transformation_progress(self):
|
|
|
|
|
|
"""Aktualisiert den Transformation-Progressbar um einen Schritt."""
|
|
|
|
|
|
if self.transformation_progress_bar:
|
|
|
|
|
|
current_value = self.transformation_progress_bar.value()
|
|
|
|
|
|
self.transformation_progress_bar.setValue(current_value + 1)
|
2025-12-27 20:01:46 +01:00
|
|
|
|
|
|
|
|
|
|
def _show_drop_summary_dialog(self, stats: dict):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Zeigt einen Zusammenfassungsdialog über die verarbeiteten XML-Dateien.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
stats: Statistik-Dictionary mit Verarbeitungsergebnissen
|
|
|
|
|
|
"""
|
|
|
|
|
|
# Erstelle Zusammenfassungstext
|
|
|
|
|
|
summary_lines = []
|
2025-12-28 16:46:39 +01:00
|
|
|
|
summary_lines.append("Verarbeitung abgeschlossen:\n")
|
2025-12-27 20:01:46 +01:00
|
|
|
|
summary_lines.append(f"📊 Gesamt: {stats['total']} Datei(en)")
|
|
|
|
|
|
summary_lines.append(f"✓ Verarbeitet: {stats['processed']} Datei(en)")
|
|
|
|
|
|
|
|
|
|
|
|
if stats["new_added"] > 0:
|
|
|
|
|
|
summary_lines.append(f"➕ Neu hinzugefügt: {stats['new_added']} Datei(en)")
|
|
|
|
|
|
|
|
|
|
|
|
if stats["existing_added"] > 0:
|
|
|
|
|
|
summary_lines.append(f"🔗 Vorhandene zugeordnet: {stats['existing_added']} Datei(en)")
|
|
|
|
|
|
|
|
|
|
|
|
if stats["already_assigned"] > 0:
|
|
|
|
|
|
summary_lines.append(f"ℹ️ Bereits zugeordnet: {stats['already_assigned']} Datei(en)")
|
|
|
|
|
|
|
|
|
|
|
|
if stats["cancelled"] > 0:
|
|
|
|
|
|
summary_lines.append(f"🚫 Abgebrochen: {stats['cancelled']} Datei(en)")
|
|
|
|
|
|
|
|
|
|
|
|
if stats["renamed_files"]:
|
2025-12-28 16:46:39 +01:00
|
|
|
|
summary_lines.append("\n📝 Umbenannte Dateien:")
|
2025-12-27 20:01:46 +01:00
|
|
|
|
for renamed in stats["renamed_files"]:
|
|
|
|
|
|
summary_lines.append(f" • {renamed}")
|
|
|
|
|
|
|
|
|
|
|
|
if stats["errors"] > 0:
|
|
|
|
|
|
summary_lines.append(f"\n❌ Fehler: {stats['errors']}")
|
|
|
|
|
|
for error_msg in stats["error_messages"][:5]: # Zeige max. 5 Fehler
|
|
|
|
|
|
summary_lines.append(f" • {error_msg}")
|
|
|
|
|
|
if len(stats["error_messages"]) > 5:
|
|
|
|
|
|
summary_lines.append(f" ... und {len(stats['error_messages']) - 5} weitere Fehler")
|
|
|
|
|
|
|
|
|
|
|
|
summary_text = "\n".join(summary_lines)
|
|
|
|
|
|
|
|
|
|
|
|
# Wähle Icon basierend auf Erfolg
|
|
|
|
|
|
if stats["errors"] > 0:
|
|
|
|
|
|
QMessageBox.warning(self, "Verarbeitung mit Fehlern abgeschlossen", summary_text)
|
|
|
|
|
|
elif stats["cancelled"] > 0:
|
|
|
|
|
|
QMessageBox.information(self, "Verarbeitung abgebrochen", summary_text)
|
|
|
|
|
|
else:
|
|
|
|
|
|
QMessageBox.information(self, "Verarbeitung erfolgreich", summary_text)
|
|
|
|
|
|
|
2025-08-31 17:04:22 +02:00
|
|
|
|
def _handle_xml_file_drop(self, xml_file_path: Path):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Verarbeitet eine einzelne XML-Datei, die per Drag&Drop hinzugefügt wurde.
|
2025-12-27 20:01:46 +01:00
|
|
|
|
DEPRECATED: Diese Methode wird durch _handle_multiple_xml_files_drop ersetzt.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:04:22 +02:00
|
|
|
|
Args:
|
|
|
|
|
|
xml_file_path: Pfad zur XML-Datei
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.debug(f"Verarbeite XML-Datei: {xml_file_path}")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:04:22 +02:00
|
|
|
|
# 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
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:34:29 +01:00
|
|
|
|
# Prüfe ob Projekt-Nodes verfügbar sind
|
|
|
|
|
|
if not self.pdf_project or not self.pdf_project.nodes:
|
|
|
|
|
|
QMessageBox.warning(self, "Warnung", "Keine Projekt-Nodes verfügbar für die Zuordnung.")
|
|
|
|
|
|
return
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:04:22 +02:00
|
|
|
|
# Öffne den Dialog zur Zuordnung zu XSL-Knoten
|
|
|
|
|
|
dialog = XmlToXslAssignDialog(
|
2025-12-13 21:06:40 +01:00
|
|
|
|
parent=self, xml_file_path=xml_file_path, project_nodes=self.pdf_project.nodes
|
2025-08-31 17:04:22 +02:00
|
|
|
|
)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:04:22 +02:00
|
|
|
|
if dialog.exec() == XmlToXslAssignDialog.DialogCode.Accepted:
|
|
|
|
|
|
# Hole die ausgewählten XSL-Knoten
|
|
|
|
|
|
selected_xsl_nodes = dialog.get_selected_xsl_nodes()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:04:22 +02:00
|
|
|
|
if selected_xsl_nodes:
|
|
|
|
|
|
# Verarbeite die Zuordnung
|
|
|
|
|
|
self._assign_xml_to_xsl_nodes(xml_file_path, selected_xsl_nodes)
|
|
|
|
|
|
else:
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.warning("Keine XSL-Knoten ausgewählt")
|
2025-08-31 17:04:22 +02:00
|
|
|
|
else:
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.debug("Dialog abgebrochen")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:04:22 +02:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
error_msg = f"Fehler beim Verarbeiten der XML-Datei '{xml_file_path}': {str(e)}"
|
2025-12-20 19:39:23 +01:00
|
|
|
|
logger.error(error_msg)
|
2025-08-31 17:04:22 +02:00
|
|
|
|
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.
|
2025-12-07 20:15:38 +01:00
|
|
|
|
Implementiert Hash-basierte Duplikatserkennung und intelligente Dateinamen-Verwaltung.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:04:22 +02:00
|
|
|
|
Args:
|
|
|
|
|
|
xml_file_path: Pfad zur XML-Datei
|
|
|
|
|
|
selected_xsl_nodes: Liste der ausgewählten XSL-Knoten
|
2025-12-27 20:01:46 +01:00
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
dict: Statistiken über die Verarbeitung
|
2025-08-31 17:04:22 +02:00
|
|
|
|
"""
|
|
|
|
|
|
try:
|
2025-12-07 20:15:38 +01:00
|
|
|
|
logger.info(f"Ordne XML-Datei '{xml_file_path.name}' zu {len(selected_xsl_nodes)} XSL-Knoten zu")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
# 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")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
# 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
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
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}")
|
2025-12-27 20:01:46 +01:00
|
|
|
|
return self._assign_existing_xml_to_nodes(existing_xml, selected_xsl_nodes)
|
2025-08-31 17:04:22 +02:00
|
|
|
|
else:
|
2025-12-07 20:15:38 +01:00
|
|
|
|
# 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")
|
2025-12-27 20:01:46 +01:00
|
|
|
|
return self._process_new_xml_file(xml_file_path, selected_xsl_nodes, file_hash)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-08-31 17:04:22 +02:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
error_msg = f"Fehler beim Zuordnen der XML-Datei: {str(e)}"
|
2025-12-07 20:15:38 +01:00
|
|
|
|
logger.error(error_msg)
|
2025-12-27 20:01:46 +01:00
|
|
|
|
return {"status": "error", "error_msg": error_msg}
|
2025-08-31 17:04:22 +02:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
def _start_xml_hash_calculation(self):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Startet die asynchrone Hash-Berechnung für alle XML-Dateien im Projekt.
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
2025-12-13 21:06:40 +01:00
|
|
|
|
if not hasattr(self, "pdf_project") or not self.pdf_project:
|
2025-09-20 17:22:09 +02:00
|
|
|
|
logger.debug("Keine Projekt-Einstellungen verfügbar für Hash-Berechnung")
|
|
|
|
|
|
return
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
# Sammle alle XML-Dateien aus dem Projekt
|
|
|
|
|
|
xml_files = self._collect_all_xml_files()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
if not xml_files:
|
|
|
|
|
|
logger.debug("Keine XML-Dateien für Hash-Berechnung gefunden")
|
|
|
|
|
|
return
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
logger.info(f"Starte Hash-Berechnung für {len(xml_files)} XML-Dateien")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:34:29 +01:00
|
|
|
|
# Prüfe ob Projekt verfügbar ist
|
|
|
|
|
|
if not self.project or not self.project.project_dir:
|
|
|
|
|
|
logger.warning("Kein Projekt-Verzeichnis für Hash-Berechnung verfügbar")
|
|
|
|
|
|
return
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
# 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()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
# Erstelle und starte neuen Hash-Berechnungs-Thread
|
|
|
|
|
|
self.hash_calculator_thread = XmlHashCalculatorThread(
|
2025-12-13 21:06:40 +01:00
|
|
|
|
project_dir=Path(self.project.project_dir), xml_files=xml_files
|
2025-09-20 17:22:09 +02:00
|
|
|
|
)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
# 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)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
# Starte Thread
|
|
|
|
|
|
self.hash_calculator_thread.start()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Fehler beim Starten der Hash-Berechnung: {e}")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
def _collect_all_xml_files(self) -> List[XmlFile]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Sammelt alle XmlFile-Objekte aus der Projektstruktur.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
Returns:
|
|
|
|
|
|
List[XmlFile]: Liste aller gefundenen XML-Dateien
|
|
|
|
|
|
"""
|
|
|
|
|
|
xml_files = []
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
try:
|
|
|
|
|
|
if self.pdf_project and self.pdf_project.nodes:
|
|
|
|
|
|
self._collect_xml_files_recursive(self.pdf_project.nodes, xml_files)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
logger.debug(f"Gesammelt: {len(xml_files)} XML-Dateien")
|
|
|
|
|
|
return xml_files
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Fehler beim Sammeln der XML-Dateien: {e}")
|
|
|
|
|
|
return []
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
def _collect_xml_files_recursive(self, nodes, xml_files: List[XmlFile]):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Sammelt rekursiv alle XML-Dateien aus den Nodes.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
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)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
def _on_hash_calculated(self, xml_file: XmlFile, hash_value: str):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Wird aufgerufen, wenn ein Hash-Wert berechnet wurde.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
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}")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Fehler beim Setzen des Hash-Werts: {e}")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
def _on_hash_calculation_finished(self, processed_count: int, total_count: int):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Wird aufgerufen, wenn die Hash-Berechnung abgeschlossen ist.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
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")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
# Speichere die aktualisierten Projekt-Einstellungen
|
|
|
|
|
|
if processed_count > 0:
|
|
|
|
|
|
self._save_project_settings()
|
|
|
|
|
|
logger.info("Projekt-Einstellungen mit neuen Hash-Werten gespeichert")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Fehler beim Abschließen der Hash-Berechnung: {e}")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
def _on_hash_calculation_error(self, xml_file_path: str, error_message: str):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Wird aufgerufen, wenn ein Fehler bei der Hash-Berechnung auftritt.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
Args:
|
|
|
|
|
|
xml_file_path: Pfad zur XML-Datei
|
|
|
|
|
|
error_message: Fehlermeldung
|
|
|
|
|
|
"""
|
|
|
|
|
|
logger.warning(f"Hash-Berechnungsfehler für {xml_file_path}: {error_message}")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
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.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
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
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:34:29 +01:00
|
|
|
|
# Prüfe ob Projekt verfügbar ist
|
|
|
|
|
|
if not self.project or not self.project.project_dir:
|
|
|
|
|
|
logger.warning("Kein Projekt-Verzeichnis für Hash-Berechnung verfügbar")
|
|
|
|
|
|
return
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
xml_file_path = Path(self.project.project_dir) / xml_file.xml
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
if not xml_file_path.exists():
|
|
|
|
|
|
logger.warning(f"XML-Datei nicht gefunden: {xml_file_path}")
|
|
|
|
|
|
return
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
# Datei binär lesen und Hash berechnen
|
2025-12-13 21:06:40 +01:00
|
|
|
|
with open(xml_file_path, "rb") as f:
|
2025-09-20 17:22:09 +02:00
|
|
|
|
file_content = f.read()
|
|
|
|
|
|
hash_obj = hashlib.blake2b(file_content)
|
|
|
|
|
|
hash_hex = hash_obj.hexdigest()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
# 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}")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Fehler beim Berechnen des Hash für {xml_file.xml}: {e}")
|
|
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
def _get_all_project_xml_files(self) -> List[XmlFile]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Sammelt alle XmlFile-Objekte aus dem gesamten Projekt für Hash-Vergleiche.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
Returns:
|
|
|
|
|
|
List[XmlFile]: Liste aller XML-Dateien im Projekt
|
|
|
|
|
|
"""
|
|
|
|
|
|
xml_files = []
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
try:
|
|
|
|
|
|
if self.pdf_project and self.pdf_project.nodes:
|
|
|
|
|
|
self._collect_xml_files_for_hash_comparison(self.pdf_project.nodes, xml_files)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
logger.debug(f"Hash-Vergleich: {len(xml_files)} XML-Dateien im Projekt gefunden")
|
|
|
|
|
|
return xml_files
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Fehler beim Sammeln der XML-Dateien für Hash-Vergleich: {e}")
|
|
|
|
|
|
return []
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
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.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
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)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
def _find_xml_file_by_hash(self, target_hash: str) -> XmlFile | None:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Sucht eine XML-Datei mit dem angegebenen Hash im gesamten Projekt.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
Args:
|
|
|
|
|
|
target_hash: Der zu suchende Hash-Wert (mit blake2b: Präfix)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
Returns:
|
|
|
|
|
|
XmlFile|None: Die gefundene XML-Datei oder None
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
if not target_hash:
|
|
|
|
|
|
return None
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
all_xml_files = self._get_all_project_xml_files()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
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
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
logger.debug(f"Kein Hash-Match für {target_hash} gefunden")
|
|
|
|
|
|
return None
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Fehler bei Hash-Suche für {target_hash}: {e}")
|
|
|
|
|
|
return None
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
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
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
def _is_filename_used_in_project(self, relative_xml_path: Path) -> bool:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Prüft ob ein relativer XML-Dateipfad bereits im Projekt verwendet wird.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
Args:
|
|
|
|
|
|
relative_xml_path: Relativer Pfad zur XML-Datei (z.B. xml/datei_1.xml)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
Returns:
|
|
|
|
|
|
bool: True wenn der Dateiname bereits verwendet wird
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
all_xml_files = self._get_all_project_xml_files()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
for xml_file in all_xml_files:
|
|
|
|
|
|
if xml_file.xml == relative_xml_path:
|
|
|
|
|
|
return True
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
return False
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
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
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
def _calculate_hash_for_file(self, file_path: Path) -> str | None:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Berechnet synchron den blake2b-Hash für eine Datei.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
Args:
|
|
|
|
|
|
file_path: Pfad zur Datei
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
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
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
# Datei binär lesen und Hash berechnen
|
2025-12-13 21:06:40 +01:00
|
|
|
|
with open(file_path, "rb") as f:
|
2025-12-07 20:15:38 +01:00
|
|
|
|
file_content = f.read()
|
|
|
|
|
|
hash_obj = hashlib.blake2b(file_content)
|
|
|
|
|
|
hash_hex = hash_obj.hexdigest()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
# 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
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Fehler beim Berechnen des Hash für {file_path}: {e}")
|
|
|
|
|
|
return None
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
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.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
Args:
|
|
|
|
|
|
existing_xml: Die bereits vorhandene XML-Datei
|
|
|
|
|
|
selected_xsl_nodes: Liste der ausgewählten XSL-Knoten
|
2025-12-27 20:01:46 +01:00
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
dict: Statistiken mit 'status', 'added_count', 'existing_file'
|
2025-12-07 20:15:38 +01:00
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
added_count = 0
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
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)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
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")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
if added_count > 0:
|
|
|
|
|
|
# Speichere die aktualisierten Projekt-Einstellungen
|
|
|
|
|
|
self._save_project_settings()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
# Aktualisiere das TreeWidget
|
|
|
|
|
|
self._load_nodes_to_tree()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-27 20:01:46 +01:00
|
|
|
|
return {
|
|
|
|
|
|
"status": "existing_added",
|
|
|
|
|
|
"added_count": added_count,
|
|
|
|
|
|
"existing_file": existing_xml.xml.name,
|
|
|
|
|
|
}
|
2025-12-07 20:15:38 +01:00
|
|
|
|
else:
|
2025-12-27 20:01:46 +01:00
|
|
|
|
return {"status": "already_assigned", "added_count": 0, "existing_file": existing_xml.xml.name}
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
error_msg = f"Fehler beim Zuordnen der vorhandenen XML-Datei: {str(e)}"
|
|
|
|
|
|
logger.error(error_msg)
|
2025-12-27 20:01:46 +01:00
|
|
|
|
return {"status": "error", "error_msg": error_msg}
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
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).
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
Args:
|
|
|
|
|
|
xml_file_path: Pfad zur neuen XML-Datei
|
|
|
|
|
|
selected_xsl_nodes: Liste der ausgewählten XSL-Knoten
|
|
|
|
|
|
file_hash: Berechneter Hash der Datei
|
2025-12-27 20:01:46 +01:00
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
dict: Statistiken mit 'status', 'added_count', 'new_file', 'renamed_from'
|
2025-12-07 20:15:38 +01:00
|
|
|
|
"""
|
|
|
|
|
|
try:
|
2025-12-07 20:34:29 +01:00
|
|
|
|
# Prüfe ob Projekt verfügbar ist
|
|
|
|
|
|
if not self.project or not self.project.project_dir:
|
|
|
|
|
|
logger.error("Kein Projekt-Verzeichnis für neue XML-Datei verfügbar")
|
2025-12-27 20:01:46 +01:00
|
|
|
|
return {"status": "error", "error_msg": "Kein Projekt-Verzeichnis verfügbar."}
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
# 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)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
# Bestimme den Ziel-Pfad in xml-Ordner
|
|
|
|
|
|
target_xml_path = xml_dir / xml_file_path.name
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
# 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)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
# Zeige Dialog zur Auswahl des Dateinamens
|
|
|
|
|
|
selected_path = self._show_filename_selection_dialog(xml_file_path.name, alternative_paths)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
if not selected_path:
|
|
|
|
|
|
# Benutzer hat abgebrochen
|
2025-12-27 20:01:46 +01:00
|
|
|
|
return {"status": "cancelled", "added_count": 0}
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
target_xml_path = selected_path
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
# 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}")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
# Erstelle relatives Path zur XML-Datei (relativ zum Projekt-Verzeichnis)
|
|
|
|
|
|
relative_xml_path = Path("xml") / target_xml_path.name
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
# 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
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
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")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
if added_count > 0:
|
|
|
|
|
|
# Speichere die aktualisierten Projekt-Einstellungen
|
|
|
|
|
|
self._save_project_settings()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
# Aktualisiere das TreeWidget
|
|
|
|
|
|
self._load_nodes_to_tree()
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-27 20:01:46 +01:00
|
|
|
|
return {
|
|
|
|
|
|
"status": "new_added",
|
|
|
|
|
|
"added_count": added_count,
|
|
|
|
|
|
"new_file": target_xml_path.name,
|
|
|
|
|
|
"renamed_from": xml_file_path.name if target_xml_path.name != xml_file_path.name else None,
|
|
|
|
|
|
}
|
2025-12-07 20:15:38 +01:00
|
|
|
|
else:
|
2025-12-27 20:01:46 +01:00
|
|
|
|
return {
|
|
|
|
|
|
"status": "already_assigned",
|
|
|
|
|
|
"added_count": 0,
|
|
|
|
|
|
"new_file": target_xml_path.name,
|
|
|
|
|
|
}
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
error_msg = f"Fehler beim Verarbeiten der neuen XML-Datei: {str(e)}"
|
|
|
|
|
|
logger.error(error_msg)
|
2025-12-27 20:01:46 +01:00
|
|
|
|
return {"status": "error", "error_msg": error_msg}
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
def _show_filename_selection_dialog(self, original_name: str, alternative_paths: List[Path]) -> Path | None:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Zeigt einen Dialog zur Auswahl eines alternativen Dateinamens.
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
Args:
|
|
|
|
|
|
original_name: Ursprünglicher Dateiname
|
|
|
|
|
|
alternative_paths: Liste alternativer Pfade
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
Returns:
|
|
|
|
|
|
Path|None: Ausgewählter Pfad oder None bei Abbruch
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
2025-12-13 21:06:40 +01:00
|
|
|
|
from PySide6.QtWidgets import (
|
|
|
|
|
|
QDialog,
|
|
|
|
|
|
QVBoxLayout,
|
|
|
|
|
|
QLabel,
|
|
|
|
|
|
QRadioButton,
|
|
|
|
|
|
QButtonGroup,
|
|
|
|
|
|
QPushButton,
|
|
|
|
|
|
QHBoxLayout,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
dialog = QDialog(self)
|
|
|
|
|
|
dialog.setWindowTitle("Dateiname auswählen")
|
|
|
|
|
|
dialog.setModal(True)
|
|
|
|
|
|
dialog.resize(400, 300)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
layout = QVBoxLayout(dialog)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
# Erklärungstext
|
2025-12-13 21:06:40 +01:00
|
|
|
|
info_label = QLabel(
|
|
|
|
|
|
f"Eine Datei mit dem Namen '{original_name}' existiert bereits.\n\n"
|
|
|
|
|
|
"Bitte wählen Sie einen alternativen Dateinamen:"
|
|
|
|
|
|
)
|
2025-12-07 20:15:38 +01:00
|
|
|
|
layout.addWidget(info_label)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
# Radio-Buttons für alternative Namen
|
|
|
|
|
|
button_group = QButtonGroup(dialog)
|
|
|
|
|
|
radio_buttons = []
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
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)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
# Buttons
|
|
|
|
|
|
button_layout = QHBoxLayout()
|
|
|
|
|
|
ok_button = QPushButton("OK")
|
|
|
|
|
|
cancel_button = QPushButton("Abbrechen")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
button_layout.addWidget(ok_button)
|
|
|
|
|
|
button_layout.addWidget(cancel_button)
|
|
|
|
|
|
layout.addLayout(button_layout)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
# Event-Handler
|
|
|
|
|
|
ok_button.clicked.connect(dialog.accept)
|
|
|
|
|
|
cancel_button.clicked.connect(dialog.reject)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
# 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]
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
return None
|
2025-12-13 21:06:40 +01:00
|
|
|
|
|
2025-12-07 20:15:38 +01:00
|
|
|
|
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-12-11 21:26:13 +01:00
|
|
|
|
def _transform_xml_file(self, item: QTreeWidgetItem, force: bool = False):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Transformiert eine einzelne XML-Datei.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
item: Das TreeWidgetItem der XML-Datei
|
|
|
|
|
|
force: Wenn True, wird Transformation auch bei aktuellem Output durchgeführt
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# Hole XslFile vom Parent-Item
|
|
|
|
|
|
parent_item = item.parent()
|
|
|
|
|
|
if not parent_item:
|
|
|
|
|
|
logger.error("XML-Datei hat kein Parent-Item (XslFile)")
|
|
|
|
|
|
QMessageBox.warning(self, "Fehler", "XML-Datei hat keine zugeordnete XSL-Datei")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
xsl_file_obj = parent_item.data(0, Qt.ItemDataRole.UserRole)
|
|
|
|
|
|
if not isinstance(xsl_file_obj, XslFile):
|
|
|
|
|
|
logger.error(f"Parent-Item ist kein XslFile: {type(xsl_file_obj)}")
|
|
|
|
|
|
QMessageBox.warning(self, "Fehler", "Konnte XSL-Datei nicht ermitteln")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Hole XmlFile-Objekt
|
|
|
|
|
|
xml_file_obj = item.data(0, Qt.ItemDataRole.UserRole)
|
|
|
|
|
|
if not isinstance(xml_file_obj, XmlFile):
|
|
|
|
|
|
logger.error(f"Item ist kein XmlFile: {type(xml_file_obj)}")
|
|
|
|
|
|
QMessageBox.warning(self, "Fehler", "Konnte XML-Datei nicht ermitteln")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
2025-12-14 15:59:37 +01:00
|
|
|
|
# Erstelle TransformationJob mit TreeWidgetItem-Kontext für Parameter-Sammlung
|
|
|
|
|
|
job = self._create_transformation_job(xsl_file_obj, xml_file_obj, parent_item)
|
2025-12-11 21:26:13 +01:00
|
|
|
|
if not job:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Starte Transformation in separatem Thread
|
|
|
|
|
|
self._start_transformation([job], force=force)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Fehler beim Transformieren der XML-Datei: {e}")
|
|
|
|
|
|
QMessageBox.critical(self, "Fehler", f"Fehler beim Transformieren: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
def _transform_xsl_file(self, item: QTreeWidgetItem, force: bool = False):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Transformiert alle XML-Dateien einer XSL-Datei.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
item: Das TreeWidgetItem der XSL-Datei
|
|
|
|
|
|
force: Wenn True, wird Transformation auch bei aktuellem Output durchgeführt
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# Hole XslFile-Objekt
|
|
|
|
|
|
xsl_file_obj = item.data(0, Qt.ItemDataRole.UserRole)
|
|
|
|
|
|
if not isinstance(xsl_file_obj, XslFile):
|
|
|
|
|
|
logger.error(f"Item ist kein XslFile: {type(xsl_file_obj)}")
|
|
|
|
|
|
QMessageBox.warning(self, "Fehler", "Konnte XSL-Datei nicht ermitteln")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Prüfe ob XML-Dateien vorhanden sind
|
|
|
|
|
|
if not xsl_file_obj.xmls:
|
|
|
|
|
|
QMessageBox.information(self, "Info", "Keine XML-Dateien zugeordnet")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Erstelle TransformationJobs für alle XML-Dateien
|
|
|
|
|
|
jobs = []
|
|
|
|
|
|
for xml_file_obj in xsl_file_obj.xmls:
|
2025-12-14 15:59:37 +01:00
|
|
|
|
# Übergebe das XslFile-TreeWidgetItem für Parameter-Sammlung
|
|
|
|
|
|
job = self._create_transformation_job(xsl_file_obj, xml_file_obj, item)
|
2025-12-11 21:26:13 +01:00
|
|
|
|
if job:
|
|
|
|
|
|
jobs.append(job)
|
|
|
|
|
|
|
|
|
|
|
|
if not jobs:
|
|
|
|
|
|
QMessageBox.warning(self, "Fehler", "Konnte keine Transformations-Jobs erstellen")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Starte Transformation in separatem Thread
|
|
|
|
|
|
self._start_transformation(jobs, force=force)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Fehler beim Transformieren der XSL-Datei: {e}")
|
|
|
|
|
|
QMessageBox.critical(self, "Fehler", f"Fehler beim Transformieren: {str(e)}")
|
|
|
|
|
|
|
2025-12-14 21:11:40 +01:00
|
|
|
|
def _count_diff_pdfs_under_node(self, node: TreeNode | XslFile, node_item: QTreeWidgetItem) -> int:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Zählt die Anzahl der existierenden Diff-PDFs unter einem Knoten.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
node: TreeNode oder XslFile Objekt
|
|
|
|
|
|
node_item: Das TreeWidgetItem des Knotens
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
int: Anzahl der existierenden Diff-PDF-Dateien
|
|
|
|
|
|
"""
|
|
|
|
|
|
count = 0
|
|
|
|
|
|
|
|
|
|
|
|
if isinstance(node, XslFile):
|
|
|
|
|
|
# Für XslFile: Zähle Diff-PDFs für jede XML-Datei
|
|
|
|
|
|
if not self.project:
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
diff_dir = self.project.project_dir / "diff"
|
|
|
|
|
|
xsl_id_str = "_".join(str(x) for x in node.id) if node.id else ""
|
|
|
|
|
|
|
|
|
|
|
|
for xml_file_obj in node.xmls:
|
|
|
|
|
|
xml_stem = xml_file_obj.xml.stem
|
|
|
|
|
|
pdf_basename = f"{xml_stem}_xsl_{xsl_id_str}.pdf"
|
|
|
|
|
|
diff_pdf_path = diff_dir / pdf_basename
|
|
|
|
|
|
|
|
|
|
|
|
if diff_pdf_path.exists():
|
|
|
|
|
|
count += 1
|
|
|
|
|
|
|
|
|
|
|
|
elif isinstance(node, TreeNode):
|
|
|
|
|
|
# Für TreeNode: Rekursiv alle Kinder durchgehen
|
|
|
|
|
|
for i in range(node_item.childCount()):
|
|
|
|
|
|
child_item = node_item.child(i)
|
|
|
|
|
|
child_node = child_item.data(0, Qt.ItemDataRole.UserRole)
|
|
|
|
|
|
|
|
|
|
|
|
if isinstance(child_node, (XslFile, TreeNode)):
|
|
|
|
|
|
count += self._count_diff_pdfs_under_node(child_node, child_item)
|
|
|
|
|
|
|
|
|
|
|
|
return count
|
|
|
|
|
|
|
|
|
|
|
|
def _update_diff_pdf_counts_recursive(self, tree_item: QTreeWidgetItem):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Aktualisiert rekursiv die Diff-PDF-Anzahl in Spalte 2 für alle TreeNode und XslFile Items.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
tree_item: Das TreeWidgetItem (kann Root oder beliebiger Knoten sein)
|
|
|
|
|
|
"""
|
|
|
|
|
|
node = tree_item.data(0, Qt.ItemDataRole.UserRole)
|
|
|
|
|
|
|
|
|
|
|
|
# Aktualisiere nur für TreeNode und XslFile, nicht für XmlFile
|
|
|
|
|
|
if isinstance(node, (TreeNode, XslFile)):
|
|
|
|
|
|
count = self._count_diff_pdfs_under_node(node, tree_item)
|
|
|
|
|
|
tree_item.setText(2, str(count) if count > 0 else "")
|
|
|
|
|
|
|
|
|
|
|
|
# Rekursiv für alle Kinder
|
|
|
|
|
|
for i in range(tree_item.childCount()):
|
|
|
|
|
|
child_item = tree_item.child(i)
|
|
|
|
|
|
self._update_diff_pdf_counts_recursive(child_item)
|
|
|
|
|
|
|
|
|
|
|
|
def _update_all_diff_pdf_counts(self):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Aktualisiert die Diff-PDF-Anzahl für alle Knoten im TreeWidget.
|
|
|
|
|
|
"""
|
|
|
|
|
|
root = self.ui.treeWidget.invisibleRootItem()
|
|
|
|
|
|
for i in range(root.childCount()):
|
|
|
|
|
|
self._update_diff_pdf_counts_recursive(root.child(i))
|
|
|
|
|
|
|
2025-12-14 20:45:53 +01:00
|
|
|
|
def _has_xml_files_recursive(self, node: TreeNode) -> bool:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Prüft rekursiv, ob unter einem TreeNode mindestens eine XML-Datei vorhanden ist.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
node: Der TreeNode
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
bool: True wenn mindestens eine XML-Datei gefunden wurde
|
|
|
|
|
|
"""
|
2025-12-28 12:58:39 +01:00
|
|
|
|
if not hasattr(node, "children") or not node.children:
|
2025-12-14 20:45:53 +01:00
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
for child in node.children:
|
|
|
|
|
|
if isinstance(child, XslFile):
|
|
|
|
|
|
if child.xmls:
|
|
|
|
|
|
return True
|
|
|
|
|
|
elif isinstance(child, TreeNode):
|
|
|
|
|
|
if self._has_xml_files_recursive(child):
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def _collect_all_xsl_xml_pairs_recursive(
|
|
|
|
|
|
self, tree_node: TreeNode, tree_item: QTreeWidgetItem
|
|
|
|
|
|
) -> list[tuple[XslFile, XmlFile, QTreeWidgetItem]]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Sammelt rekursiv alle (XslFile, XmlFile, XslFileItem) Tupel unter einem TreeNode.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
tree_node: Der TreeNode
|
|
|
|
|
|
tree_item: Das TreeWidgetItem des TreeNode
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
list: Liste von (XslFile, XmlFile, XslFileItem) Tupeln
|
|
|
|
|
|
"""
|
|
|
|
|
|
pairs = []
|
|
|
|
|
|
|
2025-12-28 12:58:39 +01:00
|
|
|
|
if not hasattr(tree_node, "children") or not tree_node.children:
|
2025-12-14 20:45:53 +01:00
|
|
|
|
return pairs
|
|
|
|
|
|
|
|
|
|
|
|
# Durchlaufe alle Kinder des TreeNode
|
|
|
|
|
|
for i in range(tree_item.childCount()):
|
|
|
|
|
|
child_item = tree_item.child(i)
|
|
|
|
|
|
child_node = child_item.data(0, Qt.ItemDataRole.UserRole)
|
|
|
|
|
|
|
|
|
|
|
|
if isinstance(child_node, XslFile):
|
|
|
|
|
|
# XslFile gefunden - sammle alle XML-Dateien
|
|
|
|
|
|
for xml_file_obj in child_node.xmls:
|
|
|
|
|
|
pairs.append((child_node, xml_file_obj, child_item))
|
|
|
|
|
|
|
|
|
|
|
|
elif isinstance(child_node, TreeNode):
|
|
|
|
|
|
# Rekursiv in Unterknoten suchen
|
|
|
|
|
|
pairs.extend(self._collect_all_xsl_xml_pairs_recursive(child_node, child_item))
|
|
|
|
|
|
|
|
|
|
|
|
return pairs
|
|
|
|
|
|
|
|
|
|
|
|
def _transform_tree_node(self, item: QTreeWidgetItem, force: bool = False):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Transformiert alle XML-Dateien unter einem TreeNode (rekursiv).
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
item: Das TreeWidgetItem des TreeNode
|
|
|
|
|
|
force: Wenn True, wird Transformation auch bei aktuellem Output durchgeführt
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# Hole TreeNode-Objekt
|
|
|
|
|
|
tree_node_obj = item.data(0, Qt.ItemDataRole.UserRole)
|
|
|
|
|
|
if not isinstance(tree_node_obj, TreeNode):
|
|
|
|
|
|
logger.error(f"Item ist kein TreeNode: {type(tree_node_obj)}")
|
|
|
|
|
|
QMessageBox.warning(self, "Fehler", "Konnte TreeNode nicht ermitteln")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Prüfe ob XML-Dateien vorhanden sind
|
|
|
|
|
|
if not self._has_xml_files_recursive(tree_node_obj):
|
|
|
|
|
|
QMessageBox.information(self, "Info", "Keine XML-Dateien unter diesem Knoten gefunden")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Sammle alle XSL/XML-Paare rekursiv
|
|
|
|
|
|
xsl_xml_pairs = self._collect_all_xsl_xml_pairs_recursive(tree_node_obj, item)
|
|
|
|
|
|
|
|
|
|
|
|
if not xsl_xml_pairs:
|
|
|
|
|
|
QMessageBox.information(self, "Info", "Keine XML-Dateien gefunden")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Erstelle TransformationJobs für alle XML-Dateien
|
|
|
|
|
|
jobs = []
|
|
|
|
|
|
for xsl_file_obj, xml_file_obj, xsl_file_item in xsl_xml_pairs:
|
|
|
|
|
|
# Übergebe das XslFile-TreeWidgetItem für Parameter-Sammlung
|
|
|
|
|
|
job = self._create_transformation_job(xsl_file_obj, xml_file_obj, xsl_file_item)
|
|
|
|
|
|
if job:
|
|
|
|
|
|
jobs.append(job)
|
|
|
|
|
|
|
|
|
|
|
|
if not jobs:
|
|
|
|
|
|
QMessageBox.warning(self, "Fehler", "Konnte keine Transformations-Jobs erstellen")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"Starte Transformation für {len(jobs)} XML-Dateien unter TreeNode '{tree_node_obj.bez}'")
|
|
|
|
|
|
|
|
|
|
|
|
# Starte Transformation in separatem Thread
|
|
|
|
|
|
self._start_transformation(jobs, force=force)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Fehler beim Transformieren des TreeNode: {e}")
|
|
|
|
|
|
QMessageBox.critical(self, "Fehler", f"Fehler beim Transformieren: {str(e)}")
|
|
|
|
|
|
|
2025-12-14 15:59:37 +01:00
|
|
|
|
def _create_transformation_job(
|
|
|
|
|
|
self, xsl_file_obj: XslFile, xml_file_obj: XmlFile, xsl_file_item: QTreeWidgetItem | None = None
|
|
|
|
|
|
) -> TransformationJob | None:
|
2025-12-11 21:26:13 +01:00
|
|
|
|
"""
|
|
|
|
|
|
Erstellt einen TransformationJob für eine XML/XSL-Kombination.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
xsl_file_obj: Das XslFile-Objekt
|
|
|
|
|
|
xml_file_obj: Das XmlFile-Objekt
|
2025-12-14 15:59:37 +01:00
|
|
|
|
xsl_file_item: Optional das TreeWidgetItem des XslFile für hierarchische Parameter-Sammlung
|
2025-12-11 21:26:13 +01:00
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
TransformationJob oder None bei Fehler
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
if not self.project:
|
|
|
|
|
|
QMessageBox.warning(self, "Fehler", "Kein Projekt geöffnet")
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
# Hole Tool-Konfigurationen aus app_settings
|
|
|
|
|
|
java_vm = next((jvm for jvm in app_settings.java_vms if jvm.id == self.project.java_vm_id), None)
|
|
|
|
|
|
saxon_jar = next((sj for sj in app_settings.saxon_jars if sj.id == self.project.saxon_jar_id), None)
|
|
|
|
|
|
apache_fop = next((af for af in app_settings.apache_fops if af.id == self.project.apache_fop_id), None)
|
|
|
|
|
|
diff_pdf = next((dp for dp in app_settings.diff_pdfs if dp.id == self.project.diff_pdf_id), None)
|
|
|
|
|
|
xsl_dir = next((xd for xd in app_settings.xsl_dirs if xd.id == self.project.xsl_dir_id), None)
|
|
|
|
|
|
|
|
|
|
|
|
# Prüfe ob alle Konfigurationen vorhanden sind
|
|
|
|
|
|
if not all([java_vm, saxon_jar, apache_fop, diff_pdf, xsl_dir]):
|
|
|
|
|
|
missing = []
|
2025-12-13 21:06:40 +01:00
|
|
|
|
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")
|
2025-12-11 21:26:13 +01:00
|
|
|
|
|
|
|
|
|
|
QMessageBox.warning(
|
2025-12-13 21:06:40 +01:00
|
|
|
|
self, "Fehlende Konfiguration", f"Folgende Konfigurationen fehlen: {', '.join(missing)}"
|
2025-12-11 21:26:13 +01:00
|
|
|
|
)
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
2025-12-14 15:09:52 +01:00
|
|
|
|
# Zusätzliche Sicherheitsprüfung für path_to_binary_file Attribute
|
2025-12-28 12:58:39 +01:00
|
|
|
|
if java_vm is None or not hasattr(java_vm, "path_to_binary_file") or java_vm.path_to_binary_file is None:
|
2025-12-14 15:09:52 +01:00
|
|
|
|
QMessageBox.warning(self, "Konfigurationsfehler", "Java VM Pfad ist nicht konfiguriert")
|
|
|
|
|
|
return None
|
2025-12-28 12:58:39 +01:00
|
|
|
|
|
|
|
|
|
|
if saxon_jar is None or not hasattr(saxon_jar, "path_to_jar_file") or saxon_jar.path_to_jar_file is None:
|
2025-12-14 15:09:52 +01:00
|
|
|
|
QMessageBox.warning(self, "Konfigurationsfehler", "Saxon JAR Pfad ist nicht konfiguriert")
|
|
|
|
|
|
return None
|
2025-12-28 12:58:39 +01:00
|
|
|
|
|
|
|
|
|
|
if apache_fop is None or not hasattr(apache_fop, "path_to_dir") or apache_fop.path_to_dir is None:
|
2025-12-14 15:09:52 +01:00
|
|
|
|
QMessageBox.warning(self, "Konfigurationsfehler", "Apache FOP Pfad ist nicht konfiguriert")
|
|
|
|
|
|
return None
|
2025-12-28 12:58:39 +01:00
|
|
|
|
|
|
|
|
|
|
if diff_pdf is None or not hasattr(diff_pdf, "path_to_binary_file") or diff_pdf.path_to_binary_file is None:
|
2025-12-14 15:09:52 +01:00
|
|
|
|
QMessageBox.warning(self, "Konfigurationsfehler", "diff-pdf Pfad ist nicht konfiguriert")
|
|
|
|
|
|
return None
|
2025-12-28 12:58:39 +01:00
|
|
|
|
|
|
|
|
|
|
if xsl_dir is None or not hasattr(xsl_dir, "path_to_root_dir") or xsl_dir.path_to_root_dir is None:
|
2025-12-14 15:09:52 +01:00
|
|
|
|
QMessageBox.warning(self, "Konfigurationsfehler", "XSL-Verzeichnis Pfad ist nicht konfiguriert")
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
2025-12-11 21:26:13 +01:00
|
|
|
|
# Erstelle absoluten Pfad zur XSL-Datei
|
|
|
|
|
|
xsl_file_abs = xsl_dir.path_to_root_dir / xsl_file_obj.xsl_file
|
|
|
|
|
|
|
2025-12-14 15:59:37 +01:00
|
|
|
|
# Sammle XSLT-Parameter hierarchisch (TreeNode-Eltern → XslFile)
|
2025-12-11 21:26:13 +01:00
|
|
|
|
xslt_params = {}
|
2025-12-14 15:59:37 +01:00
|
|
|
|
|
|
|
|
|
|
# 1. Sammle Parameter von übergeordneten TreeNodes (falls TreeWidgetItem verfügbar)
|
|
|
|
|
|
if xsl_file_item is not None:
|
|
|
|
|
|
parent_params = self._collect_parent_params(xsl_file_item)
|
|
|
|
|
|
xslt_params.update(parent_params)
|
|
|
|
|
|
logger.debug(f"Hierarchische Parameter gesammelt: {parent_params}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
logger.warning(
|
|
|
|
|
|
"Kein TreeWidgetItem-Kontext verfügbar - "
|
|
|
|
|
|
"übergeordnete TreeNode-Parameter werden nicht berücksichtigt"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 2. Überschreibe mit XslFile-eigenen Parametern (höchste Priorität)
|
2025-12-11 21:26:13 +01:00
|
|
|
|
xslt_params.update(xsl_file_obj.xslt_params)
|
|
|
|
|
|
|
2025-12-28 12:58:39 +01:00
|
|
|
|
logger.info(f"Finale XSLT-Parameter für {xml_file_obj.xml} mit {xsl_file_obj.bez}: {xslt_params}")
|
2025-12-14 15:59:37 +01:00
|
|
|
|
|
2025-12-11 21:26:13 +01:00
|
|
|
|
# Erstelle TransformationJob
|
|
|
|
|
|
job = TransformationJob(
|
|
|
|
|
|
project_dir=self.project.project_dir,
|
|
|
|
|
|
xml_file=xml_file_obj.xml,
|
|
|
|
|
|
xsl_file=xsl_file_abs,
|
|
|
|
|
|
xslt_params=xslt_params,
|
|
|
|
|
|
java_vm_path=java_vm.path_to_binary_file,
|
|
|
|
|
|
saxon_jar_path=saxon_jar.path_to_jar_file,
|
|
|
|
|
|
apache_fop_dir=apache_fop.path_to_dir,
|
|
|
|
|
|
diff_pdf_path=diff_pdf.path_to_binary_file,
|
2025-12-12 21:24:54 +01:00
|
|
|
|
diff_pdf_params=diff_pdf.default_params,
|
2025-12-13 21:06:40 +01:00
|
|
|
|
xsl_id=xsl_file_obj.id,
|
2025-12-26 12:45:44 +01:00
|
|
|
|
fop_config_dir=self.project.fop_config_dir,
|
2025-12-11 21:26:13 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return job
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Fehler beim Erstellen des TransformationJobs: {e}")
|
|
|
|
|
|
QMessageBox.critical(self, "Fehler", f"Fehler beim Erstellen des Jobs: {str(e)}")
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def _start_transformation(self, jobs: list[TransformationJob], force: bool = False):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Startet die Transformation in einem separaten Thread.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
jobs: Liste der TransformationJobs
|
|
|
|
|
|
force: Wenn True, werden alle Jobs ausgeführt (ignoriert Up-to-Date)
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# Prüfe ob bereits ein Thread läuft
|
|
|
|
|
|
if self.transformation_thread and self.transformation_thread.isRunning():
|
|
|
|
|
|
QMessageBox.warning(self, "Warnung", "Es läuft bereits eine Transformation")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Erstelle und konfiguriere Thread
|
2025-12-28 16:46:39 +01:00
|
|
|
|
self.transformation_thread = TransformationThread(jobs, force=force, max_workers=app_settings.max_workers)
|
2025-12-11 21:26:13 +01:00
|
|
|
|
|
|
|
|
|
|
# Verbinde Signale
|
|
|
|
|
|
self.transformation_thread.job_started.connect(self._on_transformation_job_started)
|
|
|
|
|
|
self.transformation_thread.job_finished.connect(self._on_transformation_job_finished)
|
|
|
|
|
|
self.transformation_thread.job_error.connect(self._on_transformation_job_error)
|
|
|
|
|
|
self.transformation_thread.all_jobs_finished.connect(self._on_all_transformations_finished)
|
|
|
|
|
|
|
2025-12-27 20:31:54 +01:00
|
|
|
|
# Zeige Progressbar
|
|
|
|
|
|
self._show_transformation_progress_bar(len(jobs))
|
|
|
|
|
|
|
2025-12-11 21:26:13 +01:00
|
|
|
|
# Starte Thread
|
|
|
|
|
|
self.transformation_thread.start()
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"Transformation von {len(jobs)} Job(s) gestartet (force={force})")
|
|
|
|
|
|
self.statusBar().showMessage(f"Transformation von {len(jobs)} Job(s) gestartet...")
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Fehler beim Starten der Transformation: {e}")
|
|
|
|
|
|
QMessageBox.critical(self, "Fehler", f"Fehler beim Starten: {str(e)}")
|
|
|
|
|
|
|
2025-12-27 17:44:46 +01:00
|
|
|
|
def _expand_tree_item_parents(self, item: QTreeWidgetItem):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Öffnet alle Eltern-Knoten eines Tree-Items rekursiv.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
item: Das Tree-Item, dessen Eltern geöffnet werden sollen
|
|
|
|
|
|
"""
|
|
|
|
|
|
if item is None:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Rekursiv alle Eltern öffnen
|
|
|
|
|
|
parent = item.parent()
|
|
|
|
|
|
while parent is not None:
|
|
|
|
|
|
parent.setExpanded(True)
|
|
|
|
|
|
parent = parent.parent()
|
|
|
|
|
|
|
2025-12-13 21:06:40 +01:00
|
|
|
|
def _on_transformation_job_started(self, xml_file_name: str, xsl_id_str: str):
|
2025-12-11 21:26:13 +01:00
|
|
|
|
"""
|
|
|
|
|
|
Signal-Handler: Ein Job wurde gestartet.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
xml_file_name: Name der XML-Datei
|
2025-12-13 21:06:40 +01:00
|
|
|
|
xsl_id_str: XSL-ID als String (z.B. "2002_1_128")
|
2025-12-11 21:26:13 +01:00
|
|
|
|
"""
|
2025-12-13 21:06:40 +01:00
|
|
|
|
logger.info(f"Transformation gestartet: {xml_file_name} (XSL-ID: {xsl_id_str})")
|
2025-12-11 21:26:13 +01:00
|
|
|
|
self.statusBar().showMessage(f"Transformiere: {xml_file_name}")
|
|
|
|
|
|
|
2025-12-13 21:06:40 +01:00
|
|
|
|
# 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
|
2025-12-14 15:16:57 +01:00
|
|
|
|
list(self.xml_item_map.keys())[:3]
|
2025-12-13 21:06:40 +01:00
|
|
|
|
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:
|
2025-12-27 17:44:46 +01:00
|
|
|
|
# Öffne alle Eltern-Knoten, damit der Benutzer den Fortschritt sehen kann
|
|
|
|
|
|
self._expand_tree_item_parents(tree_item)
|
|
|
|
|
|
|
|
|
|
|
|
# Scrolle zum Item, damit es sichtbar ist
|
|
|
|
|
|
self.ui.treeWidget.scrollToItem(tree_item)
|
|
|
|
|
|
|
2025-12-13 21:06:40 +01:00
|
|
|
|
# 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)
|
|
|
|
|
|
|
2025-12-27 17:44:46 +01:00
|
|
|
|
logger.debug(f"Progress Bar für {xml_file_name} gesetzt und Eltern-Knoten geöffnet")
|
2025-12-13 21:06:40 +01:00
|
|
|
|
else:
|
|
|
|
|
|
logger.warning(f"Kein TreeWidget-Item für {xml_file_name} gefunden")
|
|
|
|
|
|
|
2025-12-11 21:26:13 +01:00
|
|
|
|
def _on_transformation_job_finished(self, result: dict):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Signal-Handler: Ein Job wurde abgeschlossen.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
result: Ergebnis-Dictionary
|
|
|
|
|
|
"""
|
2025-12-27 20:31:54 +01:00
|
|
|
|
# Aktualisiere Transformation-Progressbar
|
|
|
|
|
|
self._update_transformation_progress()
|
|
|
|
|
|
|
2025-12-11 21:26:13 +01:00
|
|
|
|
xml_file = result.get("xml_file", "?")
|
|
|
|
|
|
success = result.get("success", False)
|
|
|
|
|
|
duration = result.get("duration", 0)
|
|
|
|
|
|
|
|
|
|
|
|
if success:
|
|
|
|
|
|
logger.info(f"Transformation erfolgreich: {xml_file} ({duration:.2f}s)")
|
|
|
|
|
|
pdfs_identical = result.get("pdfs_identical", False)
|
|
|
|
|
|
if pdfs_identical:
|
|
|
|
|
|
self.statusBar().showMessage(f"✓ {xml_file} - PDFs identisch ({duration:.2f}s)", 3000)
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.statusBar().showMessage(f"⚠ {xml_file} - Unterschiede gefunden ({duration:.2f}s)", 3000)
|
|
|
|
|
|
else:
|
|
|
|
|
|
logger.error(f"Transformation fehlgeschlagen: {xml_file}")
|
|
|
|
|
|
# Zeige Fehlerdetails an
|
|
|
|
|
|
steps = result.get("steps", {})
|
|
|
|
|
|
error_msgs = []
|
|
|
|
|
|
for step_name, step_info in steps.items():
|
|
|
|
|
|
if not step_info.get("success", True):
|
|
|
|
|
|
error_msgs.append(f"{step_name}: {step_info.get('message', 'Unbekannter Fehler')}")
|
|
|
|
|
|
|
|
|
|
|
|
error_text = "\n".join(error_msgs) if error_msgs else "Unbekannter Fehler"
|
|
|
|
|
|
QMessageBox.critical(
|
2025-12-13 21:06:40 +01:00
|
|
|
|
self, "Transformation fehlgeschlagen", f"XML-Datei: {xml_file}\n\nFehler:\n{error_text}"
|
2025-12-11 21:26:13 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
2025-12-13 21:06:40 +01:00
|
|
|
|
# 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)
|
2025-12-14 13:45:20 +01:00
|
|
|
|
icon_widget = self._create_centered_diff_icon(xml_file_path, xsl_id_str)
|
2025-12-13 21:06:40 +01:00
|
|
|
|
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):
|
2025-12-11 21:26:13 +01:00
|
|
|
|
"""
|
|
|
|
|
|
Signal-Handler: Ein Job ist mit einem Fehler abgebrochen.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
xml_file_name: Name der XML-Datei
|
2025-12-13 21:06:40 +01:00
|
|
|
|
xsl_id_str: XSL-ID als String
|
2025-12-11 21:26:13 +01:00
|
|
|
|
error_message: Fehlermeldung
|
|
|
|
|
|
"""
|
2025-12-27 20:31:54 +01:00
|
|
|
|
# Aktualisiere Transformation-Progressbar
|
|
|
|
|
|
self._update_transformation_progress()
|
|
|
|
|
|
|
2025-12-13 21:06:40 +01:00
|
|
|
|
logger.error(f"Transformation-Fehler bei {xml_file_name} (XSL-ID: {xsl_id_str}): {error_message}")
|
2025-12-11 21:26:13 +01:00
|
|
|
|
QMessageBox.critical(self, "Fehler", f"Fehler bei {xml_file_name}:\n{error_message}")
|
|
|
|
|
|
|
2025-12-13 21:06:40 +01:00
|
|
|
|
# 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)")
|
|
|
|
|
|
|
2025-12-28 12:58:39 +01:00
|
|
|
|
def _on_all_transformations_finished(self, successful_count: int, total_count: int, total_duration: float):
|
2025-12-11 21:26:13 +01:00
|
|
|
|
"""
|
|
|
|
|
|
Signal-Handler: Alle Jobs wurden abgeschlossen.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
successful_count: Anzahl erfolgreicher Jobs
|
|
|
|
|
|
total_count: Gesamtanzahl der Jobs
|
2025-12-28 12:58:39 +01:00
|
|
|
|
total_duration: Gesamtdauer aller Transformationen in Sekunden
|
2025-12-11 21:26:13 +01:00
|
|
|
|
"""
|
2025-12-28 12:58:39 +01:00
|
|
|
|
logger.info(
|
|
|
|
|
|
f"Alle Transformationen abgeschlossen: {successful_count}/{total_count} erfolgreich ({total_duration:.2f}s)"
|
|
|
|
|
|
)
|
2025-12-11 21:26:13 +01:00
|
|
|
|
|
2025-12-27 20:31:54 +01:00
|
|
|
|
# Verstecke Transformation-Progressbar
|
|
|
|
|
|
self._hide_transformation_progress_bar()
|
|
|
|
|
|
|
2025-12-14 21:34:37 +01:00
|
|
|
|
# Aktualisiere Diff-PDF-Anzahl und Icons in allen Knoten
|
2025-12-14 21:11:40 +01:00
|
|
|
|
self._update_all_diff_pdf_counts()
|
2025-12-14 21:34:37 +01:00
|
|
|
|
self._update_diff_icons_for_existing_pdfs()
|
2025-12-14 21:11:40 +01:00
|
|
|
|
|
2025-12-28 12:58:39 +01:00
|
|
|
|
# Formatiere Dauer für Anzeige
|
|
|
|
|
|
duration_str = f"{total_duration:.2f}s"
|
|
|
|
|
|
|
2025-12-11 21:26:13 +01:00
|
|
|
|
if successful_count == total_count:
|
2025-12-28 12:58:39 +01:00
|
|
|
|
self.statusBar().showMessage(f"✓ Alle {total_count} Transformationen erfolgreich ({duration_str})", 5000)
|
2025-12-11 21:26:13 +01:00
|
|
|
|
QMessageBox.information(
|
2025-12-28 12:58:39 +01:00
|
|
|
|
self,
|
|
|
|
|
|
"Abgeschlossen",
|
|
|
|
|
|
f"Alle {total_count} Transformationen wurden erfolgreich abgeschlossen.\n\nGesamtdauer: {duration_str}",
|
2025-12-11 21:26:13 +01:00
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
failed_count = total_count - successful_count
|
|
|
|
|
|
self.statusBar().showMessage(
|
2025-12-28 12:58:39 +01:00
|
|
|
|
f"⚠ {successful_count}/{total_count} erfolgreich, {failed_count} fehlgeschlagen ({duration_str})", 5000
|
2025-12-11 21:26:13 +01:00
|
|
|
|
)
|
|
|
|
|
|
QMessageBox.warning(
|
|
|
|
|
|
self,
|
|
|
|
|
|
"Abgeschlossen mit Fehlern",
|
2025-12-28 12:58:39 +01:00
|
|
|
|
f"{successful_count} von {total_count} Transformationen erfolgreich\n{failed_count} fehlgeschlagen\n\nGesamtdauer: {duration_str}",
|
2025-12-11 21:26:13 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
2025-12-16 21:38:59 +01:00
|
|
|
|
def _collect_all_diff_pdfs_under_node(
|
|
|
|
|
|
self, node_obj, item: QTreeWidgetItem
|
|
|
|
|
|
) -> list[tuple[Path, str, Path, Path, Path]]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Sammelt alle Diff-PDFs unter einem TreeNode oder XslFile.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
node_obj: TreeNode oder XslFile Objekt
|
|
|
|
|
|
item: Das TreeWidgetItem des Knotens
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
list[tuple]: Liste von (xml_file_path, xsl_id_str, diff_pdf_path, ref_pdf_path, new_pdf_path)
|
|
|
|
|
|
"""
|
|
|
|
|
|
diff_pdfs = []
|
|
|
|
|
|
|
|
|
|
|
|
if not self.project:
|
|
|
|
|
|
return diff_pdfs
|
|
|
|
|
|
|
|
|
|
|
|
diff_dir = self.project.project_dir / "diff"
|
|
|
|
|
|
ref_dir = self.project.project_dir / "ref"
|
|
|
|
|
|
new_dir = self.project.project_dir / "new"
|
|
|
|
|
|
|
|
|
|
|
|
if not diff_dir.exists():
|
|
|
|
|
|
return diff_pdfs
|
|
|
|
|
|
|
|
|
|
|
|
if isinstance(node_obj, TreeNode):
|
|
|
|
|
|
# Sammle alle XSL/XML-Paare rekursiv
|
|
|
|
|
|
xsl_xml_pairs = self._collect_all_xsl_xml_pairs_recursive(node_obj, item)
|
|
|
|
|
|
|
|
|
|
|
|
for xsl_file_obj, xml_file_obj, xsl_file_item in xsl_xml_pairs:
|
|
|
|
|
|
# Baue XML-Pfad auf
|
|
|
|
|
|
xml_file_path = self.project.project_dir / xml_file_obj.xml
|
|
|
|
|
|
|
|
|
|
|
|
# Baue PDF-Namen
|
|
|
|
|
|
xml_stem = xml_file_path.stem
|
|
|
|
|
|
xsl_id_str = "_".join(str(x) for x in xsl_file_obj.id) if xsl_file_obj.id else ""
|
|
|
|
|
|
pdf_basename = f"{xml_stem}_xsl_{xsl_id_str}.pdf"
|
|
|
|
|
|
|
|
|
|
|
|
diff_pdf_path = diff_dir / pdf_basename
|
|
|
|
|
|
ref_pdf_path = ref_dir / pdf_basename
|
|
|
|
|
|
new_pdf_path = new_dir / pdf_basename
|
|
|
|
|
|
|
|
|
|
|
|
# Prüfe ob Diff-PDF existiert
|
|
|
|
|
|
if diff_pdf_path.exists():
|
|
|
|
|
|
diff_pdfs.append((xml_file_path, xsl_id_str, diff_pdf_path, ref_pdf_path, new_pdf_path))
|
|
|
|
|
|
|
|
|
|
|
|
elif isinstance(node_obj, XslFile):
|
|
|
|
|
|
# Sammle alle XML-Dateien dieser XslFile
|
|
|
|
|
|
xsl_id_str = "_".join(str(x) for x in node_obj.id) if node_obj.id else ""
|
|
|
|
|
|
|
|
|
|
|
|
for xml_file_obj in node_obj.xmls:
|
|
|
|
|
|
# Baue XML-Pfad auf
|
|
|
|
|
|
xml_file_path = self.project.project_dir / xml_file_obj.xml
|
|
|
|
|
|
|
|
|
|
|
|
# Baue PDF-Namen
|
|
|
|
|
|
xml_stem = xml_file_path.stem
|
|
|
|
|
|
pdf_basename = f"{xml_stem}_xsl_{xsl_id_str}.pdf"
|
|
|
|
|
|
|
|
|
|
|
|
diff_pdf_path = diff_dir / pdf_basename
|
|
|
|
|
|
ref_pdf_path = ref_dir / pdf_basename
|
|
|
|
|
|
new_pdf_path = new_dir / pdf_basename
|
|
|
|
|
|
|
|
|
|
|
|
# Prüfe ob Diff-PDF existiert
|
|
|
|
|
|
if diff_pdf_path.exists():
|
|
|
|
|
|
diff_pdfs.append((xml_file_path, xsl_id_str, diff_pdf_path, ref_pdf_path, new_pdf_path))
|
|
|
|
|
|
|
|
|
|
|
|
return diff_pdfs
|
|
|
|
|
|
|
2025-12-15 21:10:15 +01:00
|
|
|
|
def _find_next_diff_pdf(self) -> tuple[Path, str] | None:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Findet die nächste Diff-PDF im Projekt.
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
tuple[Path, str] | None: (xml_file_path, xsl_id_str) der nächsten Diff-PDF oder None
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not self.project:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
diff_dir = self.project.project_dir / "diff"
|
|
|
|
|
|
if not diff_dir.exists():
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
# Durchlaufe xml_item_map und prüfe welche Items Diff-PDFs haben
|
|
|
|
|
|
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 = parts[0]
|
|
|
|
|
|
xsl_id_str = parts[1]
|
|
|
|
|
|
|
|
|
|
|
|
# Konvertiere zu Path
|
|
|
|
|
|
xml_file_path = Path(xml_path_str)
|
|
|
|
|
|
|
|
|
|
|
|
# Prüfe ob Diff-PDF existiert
|
|
|
|
|
|
xml_stem = xml_file_path.stem
|
|
|
|
|
|
pdf_basename = f"{xml_stem}_xsl_{xsl_id_str}.pdf"
|
|
|
|
|
|
diff_pdf_path = diff_dir / pdf_basename
|
|
|
|
|
|
|
|
|
|
|
|
if diff_pdf_path.exists():
|
|
|
|
|
|
return (xml_file_path, xsl_id_str)
|
|
|
|
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
2025-12-16 21:38:59 +01:00
|
|
|
|
def _accept_single_diff_pdf(
|
|
|
|
|
|
self, xml_file_path: Path, xsl_id_str: str, diff_pdf_path: Path, ref_pdf_path: Path, new_pdf_path: Path
|
|
|
|
|
|
) -> bool:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Akzeptiert eine einzelne Diff-PDF ohne Viewer-Update.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
xml_file_path: Pfad zur XML-Datei
|
|
|
|
|
|
xsl_id_str: XSL-ID als String
|
|
|
|
|
|
diff_pdf_path: Pfad zur Diff-PDF
|
|
|
|
|
|
ref_pdf_path: Pfad zur Ref-PDF
|
|
|
|
|
|
new_pdf_path: Pfad zur New-PDF
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
bool: True wenn erfolgreich, False bei Fehler
|
|
|
|
|
|
"""
|
2025-12-27 17:28:04 +01:00
|
|
|
|
pdf_basename = diff_pdf_path.name # Initialisiere am Anfang für Exception-Handler
|
2025-12-28 12:58:39 +01:00
|
|
|
|
|
2025-12-16 21:38:59 +01:00
|
|
|
|
try:
|
|
|
|
|
|
# Prüfe ob new-PDF existiert
|
|
|
|
|
|
if not new_pdf_path.exists():
|
|
|
|
|
|
logger.warning(f"New-PDF nicht gefunden: {pdf_basename}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
# Lösche alte ref-PDF falls vorhanden
|
|
|
|
|
|
if ref_pdf_path.exists():
|
|
|
|
|
|
ref_pdf_path.unlink()
|
|
|
|
|
|
logger.debug(f"Alte Ref-PDF gelöscht: {ref_pdf_path}")
|
|
|
|
|
|
|
|
|
|
|
|
# Verschiebe new-PDF nach ref
|
|
|
|
|
|
shutil.move(str(new_pdf_path), str(ref_pdf_path))
|
|
|
|
|
|
logger.debug(f"New-PDF verschoben nach Ref: {new_pdf_path} -> {ref_pdf_path}")
|
|
|
|
|
|
|
|
|
|
|
|
# Lösche diff-PDF
|
|
|
|
|
|
if diff_pdf_path.exists():
|
|
|
|
|
|
diff_pdf_path.unlink()
|
|
|
|
|
|
logger.debug(f"Diff-PDF gelöscht: {diff_pdf_path}")
|
|
|
|
|
|
|
|
|
|
|
|
# Diff-Icon beim XML-Knoten entfernen
|
|
|
|
|
|
# WICHTIG: xml_item_map verwendet relative Pfade, nicht absolute!
|
2025-12-27 17:28:04 +01:00
|
|
|
|
if self.project and self.project.project_dir:
|
|
|
|
|
|
xml_relative_path = xml_file_path.relative_to(self.project.project_dir)
|
|
|
|
|
|
else:
|
|
|
|
|
|
# Fallback: Verwende absoluten Pfad als String
|
|
|
|
|
|
xml_relative_path = xml_file_path
|
2025-12-16 21:38:59 +01:00
|
|
|
|
map_key = f"{xml_relative_path}|{xsl_id_str}"
|
|
|
|
|
|
if map_key in self.xml_item_map:
|
|
|
|
|
|
tree_item = self.xml_item_map[map_key]
|
|
|
|
|
|
# Entferne Icon-Widget aus Spalte 2
|
|
|
|
|
|
tree_item.setData(2, Qt.ItemDataRole.UserRole, None)
|
|
|
|
|
|
self.ui.treeWidget.setItemWidget(tree_item, 2, None)
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"Änderungen akzeptiert für: {pdf_basename}")
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Fehler beim Akzeptieren von {pdf_basename}: {e}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def _accept_all_changes_under_node(self, item: QTreeWidgetItem):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Akzeptiert alle Diff-PDFs unter einem TreeNode oder XslFile.
|
|
|
|
|
|
Leert den Viewer, falls eine der akzeptierten PDFs gerade angezeigt wird.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
item: Das TreeWidgetItem des TreeNode oder XslFile
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# Hole Node-Objekt
|
|
|
|
|
|
node_obj = item.data(0, Qt.ItemDataRole.UserRole)
|
|
|
|
|
|
if not node_obj:
|
|
|
|
|
|
logger.warning("Kein Node-Objekt gefunden")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Sammle alle Diff-PDFs unter diesem Knoten
|
|
|
|
|
|
diff_pdfs = self._collect_all_diff_pdfs_under_node(node_obj, item)
|
|
|
|
|
|
|
|
|
|
|
|
if not diff_pdfs:
|
|
|
|
|
|
QMessageBox.information(self, "Info", "Keine Diff-PDFs unter diesem Knoten gefunden")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Frage Benutzer um Bestätigung
|
|
|
|
|
|
node_name = node_obj.bez if hasattr(node_obj, "bez") else "Unbekannt"
|
|
|
|
|
|
|
|
|
|
|
|
reply = QMessageBox.question(
|
|
|
|
|
|
self,
|
|
|
|
|
|
"Alle Änderungen übernehmen",
|
|
|
|
|
|
f"Möchten Sie wirklich alle {len(diff_pdfs)} Änderungen unter '{node_name}' übernehmen?\n\n"
|
|
|
|
|
|
f"Dies verschiebt alle new-PDFs nach ref und löscht die diff-PDFs.",
|
|
|
|
|
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
|
|
|
|
QMessageBox.StandardButton.No,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if reply != QMessageBox.StandardButton.Yes:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# Merke, ob eine der zu akzeptierenden PDFs gerade im Viewer angezeigt wird
|
|
|
|
|
|
viewer_needs_clearing = False
|
|
|
|
|
|
if self.current_diff_xml_path and self.current_diff_xsl_id:
|
|
|
|
|
|
for xml_file_path, xsl_id_str, diff_pdf_path, ref_pdf_path, new_pdf_path in diff_pdfs:
|
|
|
|
|
|
if xml_file_path == self.current_diff_xml_path and xsl_id_str == self.current_diff_xsl_id:
|
|
|
|
|
|
viewer_needs_clearing = True
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
# Akzeptiere alle Diff-PDFs
|
|
|
|
|
|
successful_count = 0
|
|
|
|
|
|
for xml_file_path, xsl_id_str, diff_pdf_path, ref_pdf_path, new_pdf_path in diff_pdfs:
|
|
|
|
|
|
if self._accept_single_diff_pdf(xml_file_path, xsl_id_str, diff_pdf_path, ref_pdf_path, new_pdf_path):
|
|
|
|
|
|
successful_count += 1
|
|
|
|
|
|
|
|
|
|
|
|
# Aktualisiere Diff-PDF-Anzahl auf übergeordneten Ebenen
|
|
|
|
|
|
self._update_all_diff_pdf_counts()
|
|
|
|
|
|
|
|
|
|
|
|
# Leere Viewer falls nötig
|
|
|
|
|
|
if viewer_needs_clearing:
|
|
|
|
|
|
self._clear_pdf_viewer()
|
|
|
|
|
|
|
|
|
|
|
|
# Zeige Erfolgsmeldung
|
|
|
|
|
|
if successful_count == len(diff_pdfs):
|
|
|
|
|
|
logger.info(f"Alle {successful_count} Änderungen erfolgreich übernommen")
|
|
|
|
|
|
QMessageBox.information(
|
|
|
|
|
|
self, "Erfolg", f"Alle {successful_count} Änderungen wurden erfolgreich übernommen"
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
failed_count = len(diff_pdfs) - successful_count
|
2025-12-28 12:58:39 +01:00
|
|
|
|
logger.warning(
|
|
|
|
|
|
f"{successful_count}/{len(diff_pdfs)} Änderungen übernommen, {failed_count} fehlgeschlagen"
|
|
|
|
|
|
)
|
2025-12-16 21:38:59 +01:00
|
|
|
|
QMessageBox.warning(
|
|
|
|
|
|
self,
|
|
|
|
|
|
"Teilweise erfolgreich",
|
|
|
|
|
|
f"{successful_count} von {len(diff_pdfs)} Änderungen übernommen\n{failed_count} fehlgeschlagen",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Fehler beim Übernehmen aller Änderungen: {e}")
|
|
|
|
|
|
QMessageBox.critical(self, "Fehler", f"Fehler beim Übernehmen der Änderungen:\n{str(e)}")
|
|
|
|
|
|
|
2025-12-26 12:57:38 +01:00
|
|
|
|
def _close_all_pdf_documents(self):
|
|
|
|
|
|
"""Schließt alle geöffneten PDF-Dokumente explizit (wichtig für Windows)."""
|
|
|
|
|
|
import gc
|
|
|
|
|
|
|
|
|
|
|
|
if self.pdf_documents:
|
|
|
|
|
|
for pdf_basename, docs in self.pdf_documents.items():
|
|
|
|
|
|
for doc_type, doc in docs.items():
|
|
|
|
|
|
if doc:
|
|
|
|
|
|
doc.close()
|
|
|
|
|
|
logger.debug(f"PDF-Dokument geschlossen: {pdf_basename} ({doc_type})")
|
|
|
|
|
|
|
|
|
|
|
|
# Lösche alle Referenzen
|
|
|
|
|
|
self.pdf_documents.clear()
|
|
|
|
|
|
|
|
|
|
|
|
# Lösche gerenderte Pixmaps
|
|
|
|
|
|
self.current_rendered_pixmaps = None
|
|
|
|
|
|
|
|
|
|
|
|
# Erzwinge Garbage Collection um Dateihandles freizugeben (wichtig für Windows)
|
|
|
|
|
|
gc.collect()
|
|
|
|
|
|
|
|
|
|
|
|
logger.info("Alle PDF-Dokumente geschlossen und Referenzen freigegeben")
|
|
|
|
|
|
|
2025-12-15 21:10:15 +01:00
|
|
|
|
def _clear_pdf_viewer(self):
|
|
|
|
|
|
"""Leert den PDF-Viewer und alle Thumbnails."""
|
2025-12-26 12:57:38 +01:00
|
|
|
|
# Schließe alle PDF-Dokumente explizit (wichtig für Windows)
|
|
|
|
|
|
self._close_all_pdf_documents()
|
|
|
|
|
|
|
2025-12-15 21:10:15 +01:00
|
|
|
|
# Entferne Widgets aus Layouts
|
|
|
|
|
|
self._clear_layout(self.ui.verticalLayout_2)
|
|
|
|
|
|
self._clear_layout(self.ui.verticalLayout_3)
|
|
|
|
|
|
|
|
|
|
|
|
# Zurücksetzen der Datenstrukturen
|
|
|
|
|
|
self.thumbnail_to_page = {}
|
|
|
|
|
|
self.pdf_documents = {}
|
|
|
|
|
|
self.current_rendered_pixmaps = None
|
|
|
|
|
|
self.fullsize_label = None
|
|
|
|
|
|
self.current_pdf = None
|
|
|
|
|
|
self.current_diff_xml_path = None
|
|
|
|
|
|
self.current_diff_xsl_id = None
|
|
|
|
|
|
|
2026-01-02 20:11:56 +01:00
|
|
|
|
# PDF-Pfade zurücksetzen und Buttons deaktivieren
|
|
|
|
|
|
self.current_ref_pdf_path = None
|
|
|
|
|
|
self.current_new_pdf_path = None
|
|
|
|
|
|
self.ui.view_ref_pdf.setEnabled(False)
|
|
|
|
|
|
self.ui.view_new_pdf.setEnabled(False)
|
|
|
|
|
|
|
2026-01-02 20:22:29 +01:00
|
|
|
|
# Slider deaktivieren
|
|
|
|
|
|
self.ui.alpha.setEnabled(False)
|
|
|
|
|
|
self.ui.zoom.setEnabled(False)
|
|
|
|
|
|
|
2025-12-15 21:10:15 +01:00
|
|
|
|
logger.info("PDF-Viewer geleert")
|
|
|
|
|
|
|
|
|
|
|
|
def _on_accept_changes_clicked(self):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Handler für Accept-Changes-Button.
|
|
|
|
|
|
Verschiebt new PDF nach ref, löscht diff PDF, aktualisiert Icons und lädt nächste Diff-PDF.
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
if not self.project or not self.current_diff_xml_path or not self.current_diff_xsl_id:
|
|
|
|
|
|
logger.warning("Keine aktuelle Diff-PDF zum Akzeptieren")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# PDF-Dateinamen ermitteln
|
|
|
|
|
|
xml_stem = self.current_diff_xml_path.stem
|
|
|
|
|
|
pdf_basename = f"{xml_stem}_xsl_{self.current_diff_xsl_id}.pdf"
|
|
|
|
|
|
|
|
|
|
|
|
# Pfade
|
|
|
|
|
|
diff_dir = self.project.project_dir / "diff"
|
|
|
|
|
|
ref_dir = self.project.project_dir / "ref"
|
|
|
|
|
|
new_dir = self.project.project_dir / "new"
|
|
|
|
|
|
|
|
|
|
|
|
diff_pdf_path = diff_dir / pdf_basename
|
|
|
|
|
|
ref_pdf_path = ref_dir / pdf_basename
|
|
|
|
|
|
new_pdf_path = new_dir / pdf_basename
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"Akzeptiere Änderungen für: {pdf_basename}")
|
|
|
|
|
|
|
|
|
|
|
|
# Prüfe ob new-PDF existiert
|
|
|
|
|
|
if not new_pdf_path.exists():
|
|
|
|
|
|
QMessageBox.warning(self, "Fehler", f"New-PDF nicht gefunden:\n{pdf_basename}")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
2025-12-26 12:57:38 +01:00
|
|
|
|
# Schließe alle PDF-Dokumente und leere UI VOR dem Löschen/Verschieben (wichtig für Windows)
|
|
|
|
|
|
self._close_all_pdf_documents()
|
|
|
|
|
|
|
|
|
|
|
|
# Entferne auch alle Widgets, die Pixmaps enthalten könnten
|
|
|
|
|
|
self._clear_layout(self.ui.verticalLayout_2)
|
|
|
|
|
|
self._clear_layout(self.ui.verticalLayout_3)
|
|
|
|
|
|
self.thumbnail_to_page = {}
|
|
|
|
|
|
self.fullsize_label = None
|
|
|
|
|
|
|
|
|
|
|
|
# Verarbeite alle pending Qt Events um sicherzustellen, dass Widgets/Ressourcen freigegeben werden
|
|
|
|
|
|
from PySide6.QtWidgets import QApplication
|
2025-12-28 12:58:39 +01:00
|
|
|
|
|
2025-12-26 12:57:38 +01:00
|
|
|
|
QApplication.processEvents()
|
|
|
|
|
|
|
|
|
|
|
|
logger.info("PDF-Dokumente geschlossen und UI geleert vor Dateioperationen")
|
|
|
|
|
|
|
2025-12-15 21:10:15 +01:00
|
|
|
|
# Lösche alte ref-PDF falls vorhanden
|
|
|
|
|
|
if ref_pdf_path.exists():
|
|
|
|
|
|
ref_pdf_path.unlink()
|
|
|
|
|
|
logger.info(f"Alte Ref-PDF gelöscht: {ref_pdf_path}")
|
|
|
|
|
|
|
|
|
|
|
|
# Verschiebe new-PDF nach ref
|
|
|
|
|
|
shutil.move(str(new_pdf_path), str(ref_pdf_path))
|
|
|
|
|
|
logger.info(f"New-PDF verschoben nach Ref: {new_pdf_path} -> {ref_pdf_path}")
|
|
|
|
|
|
|
|
|
|
|
|
# Lösche diff-PDF
|
|
|
|
|
|
if diff_pdf_path.exists():
|
|
|
|
|
|
diff_pdf_path.unlink()
|
|
|
|
|
|
logger.info(f"Diff-PDF gelöscht: {diff_pdf_path}")
|
|
|
|
|
|
|
|
|
|
|
|
# Diff-Icon beim aktuellen XML-Knoten entfernen
|
|
|
|
|
|
map_key = f"{self.current_diff_xml_path}|{self.current_diff_xsl_id}"
|
|
|
|
|
|
if map_key in self.xml_item_map:
|
|
|
|
|
|
tree_item = self.xml_item_map[map_key]
|
|
|
|
|
|
# Entferne Icon-Widget aus Spalte 2
|
|
|
|
|
|
tree_item.setData(2, Qt.ItemDataRole.UserRole, None)
|
|
|
|
|
|
self.ui.treeWidget.setItemWidget(tree_item, 2, None)
|
|
|
|
|
|
logger.info(f"Diff-Icon entfernt für: {map_key}")
|
|
|
|
|
|
|
|
|
|
|
|
# Diff-PDF-Anzahl auf übergeordneten Ebenen aktualisieren
|
|
|
|
|
|
self._update_all_diff_pdf_counts()
|
|
|
|
|
|
|
|
|
|
|
|
# Finde nächste Diff-PDF
|
|
|
|
|
|
next_diff = self._find_next_diff_pdf()
|
|
|
|
|
|
|
|
|
|
|
|
if next_diff:
|
|
|
|
|
|
# Lade nächste Diff-PDF
|
|
|
|
|
|
next_xml_path, next_xsl_id = next_diff
|
|
|
|
|
|
logger.info(f"Lade nächste Diff-PDF: {next_xml_path} / {next_xsl_id}")
|
|
|
|
|
|
self._load_pdf_for_comparison(next_xml_path, next_xsl_id)
|
|
|
|
|
|
|
|
|
|
|
|
# Wähle den entsprechenden XML-Knoten im Baum aus
|
|
|
|
|
|
map_key = f"{next_xml_path}|{next_xsl_id}"
|
|
|
|
|
|
if map_key in self.xml_item_map:
|
|
|
|
|
|
tree_item = self.xml_item_map[map_key]
|
|
|
|
|
|
self.ui.treeWidget.setCurrentItem(tree_item)
|
|
|
|
|
|
self.ui.treeWidget.scrollToItem(tree_item)
|
|
|
|
|
|
logger.info(f"TreeWidget-Item ausgewählt: {map_key}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
# Keine weiteren Diff-PDFs, Button deaktivieren und Viewer leeren
|
|
|
|
|
|
logger.info("Keine weiteren Diff-PDFs vorhanden")
|
|
|
|
|
|
self.ui.accept_changes.setEnabled(False)
|
|
|
|
|
|
self._clear_pdf_viewer()
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Fehler beim Akzeptieren der Änderungen: {e}")
|
|
|
|
|
|
QMessageBox.critical(self, "Fehler", f"Fehler beim Akzeptieren der Änderungen:\n{str(e)}")
|
|
|
|
|
|
|
2026-01-02 20:11:56 +01:00
|
|
|
|
def _on_view_ref_pdf_clicked(self):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Handler für view_ref_pdf Button.
|
|
|
|
|
|
Öffnet die Referenz-PDF im systemseitig installierten PDF-Viewer.
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not self.current_ref_pdf_path or not self.current_ref_pdf_path.exists():
|
|
|
|
|
|
QMessageBox.warning(self, "Fehler", "Referenz-PDF nicht gefunden")
|
|
|
|
|
|
logger.warning("Referenz-PDF nicht verfügbar")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"Öffne Referenz-PDF im System-Viewer: {self.current_ref_pdf_path}")
|
|
|
|
|
|
url = QUrl.fromLocalFile(str(self.current_ref_pdf_path))
|
|
|
|
|
|
if not QDesktopServices.openUrl(url):
|
2026-01-04 17:24:19 +01:00
|
|
|
|
QMessageBox.critical(self, "Fehler", f"Konnte Referenz-PDF nicht öffnen:\n{self.current_ref_pdf_path}")
|
2026-01-02 20:11:56 +01:00
|
|
|
|
logger.error(f"Fehler beim Öffnen der Referenz-PDF: {self.current_ref_pdf_path}")
|
|
|
|
|
|
|
|
|
|
|
|
def _on_view_new_pdf_clicked(self):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Handler für view_new_pdf Button.
|
|
|
|
|
|
Öffnet die neue PDF im systemseitig installierten PDF-Viewer.
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not self.current_new_pdf_path or not self.current_new_pdf_path.exists():
|
|
|
|
|
|
QMessageBox.warning(self, "Fehler", "Neue PDF nicht gefunden")
|
|
|
|
|
|
logger.warning("Neue PDF nicht verfügbar")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"Öffne neue PDF im System-Viewer: {self.current_new_pdf_path}")
|
|
|
|
|
|
url = QUrl.fromLocalFile(str(self.current_new_pdf_path))
|
|
|
|
|
|
if not QDesktopServices.openUrl(url):
|
|
|
|
|
|
QMessageBox.critical(self, "Fehler", f"Konnte neue PDF nicht öffnen:\n{self.current_new_pdf_path}")
|
|
|
|
|
|
logger.error(f"Fehler beim Öffnen der neuen PDF: {self.current_new_pdf_path}")
|
|
|
|
|
|
|
2026-01-02 21:06:37 +01:00
|
|
|
|
def eventFilter(self, obj, event):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Event-Filter für Zoom per STRG+Mausrad im PDF-Viewer.
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
obj: Das Objekt, das das Event erhalten hat
|
|
|
|
|
|
event: Das Event
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
True wenn Event behandelt wurde, sonst False
|
|
|
|
|
|
"""
|
|
|
|
|
|
# Prüfe ob Event von scrollArea_2 kommt und ein Wheel-Event ist
|
|
|
|
|
|
if obj == self.ui.scrollArea_2 and event.type() == QEvent.Type.Wheel:
|
|
|
|
|
|
# Prüfe ob STRG-Taste gedrückt ist
|
|
|
|
|
|
if event.modifiers() & Qt.KeyboardModifier.ControlModifier:
|
|
|
|
|
|
# Zoom-Schritte: 10% pro Mausrad-Klick
|
|
|
|
|
|
zoom_step = 10
|
|
|
|
|
|
|
|
|
|
|
|
# Mausrad-Delta (positiv = nach oben, negativ = nach unten)
|
|
|
|
|
|
delta = event.angleDelta().y()
|
|
|
|
|
|
|
|
|
|
|
|
# Aktuellen Zoom-Wert holen
|
|
|
|
|
|
current_zoom = self.ui.zoom.value()
|
|
|
|
|
|
|
|
|
|
|
|
# Neuen Zoom-Wert berechnen
|
|
|
|
|
|
if delta > 0:
|
|
|
|
|
|
# Zoom rein (rauf scrollen)
|
|
|
|
|
|
new_zoom = min(current_zoom + zoom_step, self.ui.zoom.maximum())
|
|
|
|
|
|
else:
|
|
|
|
|
|
# Zoom raus (runter scrollen)
|
|
|
|
|
|
new_zoom = max(current_zoom - zoom_step, self.ui.zoom.minimum())
|
|
|
|
|
|
|
|
|
|
|
|
# Zoom-Slider aktualisieren (triggert automatisch apply_zoom)
|
|
|
|
|
|
self.ui.zoom.setValue(new_zoom)
|
|
|
|
|
|
|
|
|
|
|
|
# Event als behandelt markieren
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
# Für alle anderen Events: Standard-Verarbeitung
|
|
|
|
|
|
return super().eventFilter(obj, event)
|
|
|
|
|
|
|
2025-05-29 16:30:01 +02:00
|
|
|
|
def closeEvent(self, event):
|
|
|
|
|
|
"""Wird beim Schließen der Anwendung aufgerufen."""
|
2025-12-15 20:07:53 +01:00
|
|
|
|
# UI-Zustände speichern
|
|
|
|
|
|
self._save_ui_state()
|
|
|
|
|
|
|
2025-09-20 17:22:09 +02:00
|
|
|
|
# Stoppe Hash-Berechnungs-Thread falls noch aktiv
|
2025-12-13 21:06:40 +01:00
|
|
|
|
if (
|
|
|
|
|
|
hasattr(self, "hash_calculator_thread")
|
|
|
|
|
|
and self.hash_calculator_thread
|
|
|
|
|
|
and self.hash_calculator_thread.isRunning()
|
|
|
|
|
|
):
|
2025-09-20 17:22:09 +02:00
|
|
|
|
self.hash_calculator_thread.quit()
|
|
|
|
|
|
self.hash_calculator_thread.wait()
|
2025-12-11 21:26:13 +01:00
|
|
|
|
|
|
|
|
|
|
# Stoppe Transformations-Thread falls noch aktiv
|
2025-12-13 21:06:40 +01:00
|
|
|
|
if (
|
|
|
|
|
|
hasattr(self, "transformation_thread")
|
|
|
|
|
|
and self.transformation_thread
|
|
|
|
|
|
and self.transformation_thread.isRunning()
|
|
|
|
|
|
):
|
2025-12-11 21:26:13 +01:00
|
|
|
|
self.transformation_thread.quit()
|
|
|
|
|
|
self.transformation_thread.wait()
|
|
|
|
|
|
|
2025-12-28 16:46:39 +01:00
|
|
|
|
# Beende Saxon-Worker-Pool
|
|
|
|
|
|
self._shutdown_saxon_worker_pool()
|
|
|
|
|
|
|
2026-01-04 17:24:19 +01:00
|
|
|
|
# Beende FOP-Worker-Pool
|
|
|
|
|
|
self._shutdown_fop_worker_pool()
|
|
|
|
|
|
|
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)
|
2025-12-15 20:07:53 +01:00
|
|
|
|
|
|
|
|
|
|
def _save_ui_state(self):
|
|
|
|
|
|
"""Speichert die aktuellen UI-Zustände (Fenstergeometrie, Splitter, TreeWidget-Spalten)."""
|
|
|
|
|
|
global app_settings
|
|
|
|
|
|
|
|
|
|
|
|
# Fenstergeometrie speichern
|
|
|
|
|
|
geometry = self.geometry()
|
|
|
|
|
|
app_settings.window_geometry = (geometry.x(), geometry.y(), geometry.width(), geometry.height())
|
|
|
|
|
|
|
|
|
|
|
|
# Splitter-Positionen speichern
|
|
|
|
|
|
app_settings.splitter_sizes = self.ui.splitter.sizes()
|
|
|
|
|
|
|
|
|
|
|
|
# TreeWidget-Spaltenbreiten speichern
|
|
|
|
|
|
column_count = self.ui.treeWidget.columnCount()
|
|
|
|
|
|
app_settings.tree_column_widths = [self.ui.treeWidget.columnWidth(i) for i in range(column_count)]
|
|
|
|
|
|
|
|
|
|
|
|
# Konfiguration speichern
|
|
|
|
|
|
app_settings.save()
|