2025-05-22 19:10:39 +02:00
|
|
|
import glob
|
2025-05-23 11:09:47 +02:00
|
|
|
import os
|
2025-05-31 21:27:58 +02:00
|
|
|
import time
|
2025-08-10 14:03:15 +02:00
|
|
|
import polars as pl
|
2025-05-23 11:09:47 +02:00
|
|
|
|
2025-05-31 21:27:58 +02:00
|
|
|
from PySide6.QtCore import Qt, QSize
|
2025-08-03 20:31:32 +02:00
|
|
|
from PySide6.QtGui import QCursor, QPixmap, QPainter, QAction, QIcon
|
2025-08-10 14:03:15 +02:00
|
|
|
from PySide6.QtWidgets import QLabel, QMainWindow, QApplication, QStyleFactory, QMenu, QTreeWidgetItem, QMessageBox
|
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-03 16:31:38 +02:00
|
|
|
from conf import app_settings, PdfProject, PdfProjectSettings, TreeNode, XslFile, XmlFile
|
2025-06-16 20:30:56 +02:00
|
|
|
from pathlib import Path
|
2025-05-20 11:24:07 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class MainWindow(QMainWindow):
|
|
|
|
|
def __init__(self, parent=None):
|
|
|
|
|
"""
|
|
|
|
|
Konstruktor für die MainWindow-Klasse.
|
2025-05-31 21:27:58 +02:00
|
|
|
Verwendet PySide6.QtPdf für optimale Performance.
|
|
|
|
|
|
2025-05-20 11:24:07 +02:00
|
|
|
Args:
|
|
|
|
|
parent: Übergeordnetes Widget, falls vorhanden
|
|
|
|
|
"""
|
|
|
|
|
super().__init__(parent)
|
2025-05-23 11:09:47 +02:00
|
|
|
|
2025-05-20 11:24:07 +02:00
|
|
|
# UI einrichten
|
|
|
|
|
self.ui = Ui_MainWindow()
|
|
|
|
|
self.ui.setupUi(self)
|
2025-05-23 11:09:47 +02:00
|
|
|
|
2025-05-29 16:30:01 +02:00
|
|
|
# Dict zum Speichern der Beziehung zwischen Thumbnails und Seitennummern
|
|
|
|
|
self.thumbnail_to_page = {}
|
2025-05-23 11:09:47 +02:00
|
|
|
|
2025-05-29 16:30:01 +02:00
|
|
|
# PDF-Dokumente für späteres On-Demand-Rendering speichern
|
2025-05-31 21:27:58 +02:00
|
|
|
self.pdf_documents = {} # {pdf_filename: {'diff': QPdfDocument, 'ref': QPdfDocument, 'new': QPdfDocument}}
|
2025-05-29 16:30:01 +02:00
|
|
|
|
2025-05-22 21:05:22 +02:00
|
|
|
# Aktueller Zoom-Faktor
|
|
|
|
|
self.current_zoom = 100 # 100%
|
2025-05-23 11:09:47 +02:00
|
|
|
|
2025-05-29 16:30:01 +02:00
|
|
|
# Aktuell angezeigte Seite
|
|
|
|
|
self.current_page = 0
|
|
|
|
|
self.current_pdf = None
|
|
|
|
|
|
2025-05-29 19:03:19 +02:00
|
|
|
# Cache für die aktuell gerenderten Pixmaps (Performance-Optimierung)
|
|
|
|
|
self.current_rendered_pixmaps = None
|
|
|
|
|
|
2025-05-29 16:30:01 +02:00
|
|
|
# Label für die Vollansicht (nur ein einziges Label)
|
|
|
|
|
self.fullsize_label = None
|
|
|
|
|
|
2025-05-23 21:26:50 +02:00
|
|
|
# Variablen für Drag-to-Scroll (Anti-Jitter für 4K/DPI-Skalierung)
|
|
|
|
|
self.is_dragging = False
|
|
|
|
|
self.last_drag_position = None
|
|
|
|
|
self.drag_threshold = 3 # Mindestbewegung in Pixeln vor dem Scrollen
|
|
|
|
|
self.scroll_sensitivity = 0.7 # Reduzierte Empfindlichkeit für sanfteres Scrollen
|
|
|
|
|
|
2025-05-27 20:48:21 +02:00
|
|
|
# Theme-Menü initialisieren
|
|
|
|
|
self._setup_theme_menu()
|
2025-06-17 19:11:08 +02:00
|
|
|
|
2025-06-22 11:58:57 +02:00
|
|
|
# Vorhandene Projekte-Menü initialisieren
|
|
|
|
|
self._setup_projects_menu()
|
|
|
|
|
|
2025-06-17 19:11:08 +02:00
|
|
|
#
|
|
|
|
|
if (theme := app_settings.theme):
|
|
|
|
|
self.change_theme(theme)
|
|
|
|
|
else:
|
|
|
|
|
self.change_theme('Fusion')
|
2025-05-27 20:48:21 +02:00
|
|
|
|
2025-05-22 19:10:39 +02:00
|
|
|
# Bilder laden
|
2025-05-21 20:26:03 +02:00
|
|
|
self._load_images()
|
2025-05-23 11:09:47 +02:00
|
|
|
|
2025-05-20 11:24:07 +02:00
|
|
|
# Signale und Slots verbinden
|
|
|
|
|
self._connect_signals()
|
2025-08-03 16:31:38 +02:00
|
|
|
|
|
|
|
|
# Kontextmenü für TreeWidget einrichten
|
|
|
|
|
self._setup_tree_context_menu()
|
2025-05-23 11:09:47 +02:00
|
|
|
|
2025-05-27 20:48:21 +02:00
|
|
|
def _setup_theme_menu(self):
|
|
|
|
|
"""Initialisiert das Theme-Menü mit verfügbaren Themes."""
|
|
|
|
|
# Hole alle verfügbaren Themes
|
|
|
|
|
available_themes = QStyleFactory.keys()
|
|
|
|
|
current_theme = QApplication.style().objectName()
|
2025-05-28 19:30:37 +02:00
|
|
|
|
2025-05-27 20:48:21 +02:00
|
|
|
print(f"Verfügbare Themes: {available_themes}")
|
|
|
|
|
print(f"Aktuelles Theme: {current_theme}")
|
2025-05-28 19:30:37 +02:00
|
|
|
|
2025-05-27 20:48:21 +02:00
|
|
|
# Füge Theme-Aktionen zum Menü hinzu
|
|
|
|
|
for theme_name in available_themes:
|
|
|
|
|
action = QAction(theme_name, self)
|
|
|
|
|
action.setCheckable(True)
|
2025-05-28 19:30:37 +02:00
|
|
|
|
2025-05-27 20:48:21 +02:00
|
|
|
# Markiere das aktuelle Theme
|
|
|
|
|
if theme_name.lower() == current_theme.lower():
|
|
|
|
|
action.setChecked(True)
|
2025-05-28 19:30:37 +02:00
|
|
|
|
2025-05-27 20:48:21 +02:00
|
|
|
# Verbinde die Aktion mit der Theme-Wechsel-Funktion
|
|
|
|
|
action.triggered.connect(lambda checked, theme=theme_name: self.change_theme(theme))
|
2025-05-28 19:30:37 +02:00
|
|
|
|
2025-05-27 20:48:21 +02:00
|
|
|
# Füge die Aktion zum Theme-Menü hinzu
|
|
|
|
|
self.ui.menuThema.addAction(action)
|
|
|
|
|
|
2025-06-22 11:58:57 +02:00
|
|
|
def _setup_projects_menu(self):
|
|
|
|
|
"""Initialisiert das Vorhandene Projekte-Menü mit gespeicherten Projekten."""
|
|
|
|
|
# Prüfe ob Projekte vorhanden sind
|
|
|
|
|
if not app_settings.pdf_projects:
|
|
|
|
|
# Keine Projekte vorhanden - Menü deaktiviert lassen
|
|
|
|
|
self.ui.actionVorhandene_Projekte.setEnabled(False)
|
|
|
|
|
self.ui.actionVorhandene_Projekte.setText("Vorhandene Projekte (keine vorhanden)")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Projekte vorhanden - Menü aktivieren und Untermenü erstellen
|
|
|
|
|
self.ui.actionVorhandene_Projekte.setEnabled(True)
|
|
|
|
|
self.ui.actionVorhandene_Projekte.setText("Vorhandene Projekte")
|
|
|
|
|
|
|
|
|
|
# Erstelle ein Untermenü für die Projekte
|
|
|
|
|
projects_menu = QMenu(self)
|
|
|
|
|
|
|
|
|
|
# Füge jedes Projekt als Menü-Eintrag hinzu
|
|
|
|
|
for project in app_settings.pdf_projects:
|
|
|
|
|
project_action = QAction(project.name, self)
|
|
|
|
|
project_action.setToolTip(f"Projekt-Ordner: {project.project_dir}")
|
|
|
|
|
|
|
|
|
|
# Verbinde die Aktion mit der Projekt-Öffnen-Funktion
|
|
|
|
|
project_action.triggered.connect(
|
|
|
|
|
lambda checked, proj=project: self.open_existing_project(proj)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
projects_menu.addAction(project_action)
|
|
|
|
|
|
|
|
|
|
# Setze das Untermenü für die Aktion
|
|
|
|
|
self.ui.actionVorhandene_Projekte.setMenu(projects_menu)
|
|
|
|
|
|
|
|
|
|
print(f"Projekte-Menü initialisiert mit {len(app_settings.pdf_projects)} Projekten")
|
|
|
|
|
|
|
|
|
|
def open_existing_project(self, project: PdfProject):
|
|
|
|
|
"""
|
|
|
|
|
Öffnet ein vorhandenes Projekt.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
project: Das zu öffnende PdfProject-Objekt
|
|
|
|
|
"""
|
|
|
|
|
print(f"Öffne Projekt: {project.name}")
|
|
|
|
|
print(f"Projekt-Ordner: {project.project_dir}")
|
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
self.project = project
|
|
|
|
|
|
2025-06-22 14:47:17 +02:00
|
|
|
try:
|
|
|
|
|
# Prüfe ob project.yaml existiert und nicht leer ist
|
|
|
|
|
project_yaml_path = Path(project.project_dir) / 'project.yaml'
|
|
|
|
|
|
|
|
|
|
if project_yaml_path.exists() and project_yaml_path.stat().st_size > 0:
|
|
|
|
|
# Versuche die Projekt-Einstellungen zu laden
|
|
|
|
|
self.pdf_project = PdfProjectSettings.readSettings(project_dir=project.project_dir)
|
|
|
|
|
print(f"Projekt-Einstellungen aus {project_yaml_path} geladen!")
|
|
|
|
|
else:
|
|
|
|
|
# Erstelle Standard-Projekt-Einstellungen wenn Datei leer oder nicht vorhanden
|
|
|
|
|
print("project.yaml ist leer oder nicht vorhanden, erstelle Standard-Einstellungen")
|
|
|
|
|
self.pdf_project = PdfProjectSettings()
|
|
|
|
|
|
|
|
|
|
# Speichere die Standard-Einstellungen in die project.yaml
|
|
|
|
|
self.pdf_project.writeSettings(project_dir=project.project_dir)
|
|
|
|
|
print(f"Standard-Projekt-Einstellungen in {project_yaml_path} gespeichert")
|
|
|
|
|
|
2025-07-27 18:33:14 +02:00
|
|
|
# Lade die Nodes in das TreeWidget
|
|
|
|
|
self._load_nodes_to_tree()
|
|
|
|
|
|
2025-06-22 14:47:17 +02:00
|
|
|
except Exception as e:
|
|
|
|
|
print(f"Fehler beim Laden des Projekts '{project.name}': {e}")
|
|
|
|
|
# Fallback: Erstelle Standard-Einstellungen
|
|
|
|
|
try:
|
|
|
|
|
self.pdf_project = PdfProjectSettings()
|
|
|
|
|
print("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:
|
|
|
|
|
print(f"Fehler beim Erstellen der Fallback-Einstellungen: {fallback_error}")
|
2025-06-22 11:58:57 +02:00
|
|
|
|
2025-05-27 20:48:21 +02:00
|
|
|
def change_theme(self, theme_name):
|
|
|
|
|
"""
|
|
|
|
|
Wechselt das Theme der Anwendung.
|
2025-05-28 19:30:37 +02:00
|
|
|
|
2025-05-27 20:48:21 +02:00
|
|
|
Args:
|
|
|
|
|
theme_name: Name des zu verwendenden Themes
|
|
|
|
|
"""
|
|
|
|
|
print(f"Wechsle zu Theme: {theme_name}")
|
2025-05-28 19:30:37 +02:00
|
|
|
|
2025-05-27 20:48:21 +02:00
|
|
|
try:
|
|
|
|
|
# Erstelle den neuen Style
|
|
|
|
|
style = QStyleFactory.create(theme_name)
|
|
|
|
|
if style:
|
|
|
|
|
# Wende den neuen Style auf die Anwendung an
|
|
|
|
|
QApplication.setStyle(style)
|
2025-05-28 19:30:37 +02:00
|
|
|
|
2025-05-27 20:48:21 +02:00
|
|
|
# Aktualisiere die Checkmarks im Menü
|
|
|
|
|
for action in self.ui.menuThema.actions():
|
|
|
|
|
action.setChecked(action.text() == theme_name)
|
2025-05-28 19:30:37 +02:00
|
|
|
|
2025-05-27 20:48:21 +02:00
|
|
|
print(f"Theme erfolgreich gewechselt zu: {theme_name}")
|
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:
|
|
|
|
|
print(f"Fehler: Theme '{theme_name}' konnte nicht erstellt werden")
|
2025-05-28 19:30:37 +02:00
|
|
|
|
2025-05-27 20:48:21 +02:00
|
|
|
except Exception as e:
|
|
|
|
|
print(f"Fehler beim Wechseln des Themes: {e}")
|
|
|
|
|
|
2025-05-21 20:26:03 +02:00
|
|
|
def _load_images(self):
|
2025-05-29 16:30:01 +02:00
|
|
|
"""Lädt PDF-Thumbnails und bereitet On-Demand-Rendering vor."""
|
2025-05-22 19:10:39 +02:00
|
|
|
# Entferne bestehende Widgets aus den Layouts
|
|
|
|
|
self._clear_layout(self.ui.verticalLayout_2)
|
|
|
|
|
self._clear_layout(self.ui.verticalLayout_3)
|
2025-05-23 11:09:47 +02:00
|
|
|
|
2025-05-22 21:05:22 +02:00
|
|
|
# Dicts zurücksetzen
|
2025-05-29 16:30:01 +02:00
|
|
|
self.thumbnail_to_page = {}
|
|
|
|
|
self.pdf_documents = {}
|
2025-05-29 19:03:19 +02:00
|
|
|
self.current_rendered_pixmaps = None
|
2025-05-23 11:09:47 +02:00
|
|
|
|
2025-05-27 18:31:45 +02:00
|
|
|
# Basis-Pfad zu den PDF-Ordnern
|
2025-05-21 20:26:03 +02:00
|
|
|
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
2025-06-09 17:17:53 +02:00
|
|
|
pdf_base_dir = os.path.join(base_dir, "ui", "res", "pdf")
|
2025-05-28 19:30:37 +02:00
|
|
|
|
2025-05-27 18:31:45 +02:00
|
|
|
diff_dir = os.path.join(pdf_base_dir, "diff")
|
|
|
|
|
ref_dir = os.path.join(pdf_base_dir, "ref")
|
|
|
|
|
new_dir = os.path.join(pdf_base_dir, "new")
|
|
|
|
|
|
|
|
|
|
# Prüfe ob diff-Ordner existiert und PDF-Dateien enthält
|
|
|
|
|
if not os.path.exists(diff_dir):
|
|
|
|
|
print(f"Diff-Ordner nicht gefunden: {diff_dir}")
|
|
|
|
|
return
|
2025-05-23 20:38:19 +02:00
|
|
|
|
2025-05-27 18:31:45 +02:00
|
|
|
# Finde alle PDF-Dateien im diff-Ordner
|
|
|
|
|
diff_pdfs = glob.glob(os.path.join(diff_dir, "*.pdf"))
|
|
|
|
|
if not diff_pdfs:
|
|
|
|
|
print("Keine PDF-Dateien im diff-Ordner gefunden")
|
2025-05-23 20:38:19 +02:00
|
|
|
return
|
|
|
|
|
|
2025-05-27 18:31:45 +02:00
|
|
|
print(f"Gefundene PDF-Dateien im diff-Ordner: {diff_pdfs}")
|
|
|
|
|
|
|
|
|
|
# Für jede PDF-Datei im diff-Ordner
|
|
|
|
|
for diff_pdf_path in diff_pdfs:
|
|
|
|
|
pdf_filename = os.path.basename(diff_pdf_path)
|
|
|
|
|
ref_pdf_path = os.path.join(ref_dir, pdf_filename)
|
|
|
|
|
new_pdf_path = os.path.join(new_dir, pdf_filename)
|
|
|
|
|
|
|
|
|
|
# Prüfe ob gleichnamige PDFs in ref und new existieren
|
|
|
|
|
if not os.path.exists(ref_pdf_path):
|
|
|
|
|
print(f"Referenz-PDF nicht gefunden: {ref_pdf_path}")
|
|
|
|
|
continue
|
|
|
|
|
if not os.path.exists(new_pdf_path):
|
|
|
|
|
print(f"New-PDF nicht gefunden: {new_pdf_path}")
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
try:
|
2025-05-31 21:27:58 +02:00
|
|
|
# Alle drei PDF-Dateien öffnen mit QtPdf
|
|
|
|
|
diff_doc = QPdfDocument()
|
|
|
|
|
ref_doc = QPdfDocument()
|
|
|
|
|
new_doc = QPdfDocument()
|
|
|
|
|
|
|
|
|
|
# PDF-Dateien laden
|
|
|
|
|
diff_doc.load(diff_pdf_path)
|
|
|
|
|
ref_doc.load(ref_pdf_path)
|
|
|
|
|
new_doc.load(new_pdf_path)
|
|
|
|
|
|
|
|
|
|
# Warten bis PDFs geladen sind
|
|
|
|
|
if (diff_doc.status() != QPdfDocument.Status.Ready or
|
|
|
|
|
ref_doc.status() != QPdfDocument.Status.Ready or
|
|
|
|
|
new_doc.status() != QPdfDocument.Status.Ready):
|
|
|
|
|
print(f"Fehler beim Laden der PDFs für {pdf_filename}")
|
|
|
|
|
continue
|
2025-05-27 18:31:45 +02:00
|
|
|
|
2025-05-29 16:30:01 +02:00
|
|
|
# PDF-Dokumente für später speichern
|
|
|
|
|
self.pdf_documents[pdf_filename] = {
|
|
|
|
|
'diff': diff_doc,
|
|
|
|
|
'ref': ref_doc,
|
|
|
|
|
'new': new_doc
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-27 18:31:45 +02:00
|
|
|
print(f"PDFs geladen: {pdf_filename}")
|
2025-05-31 21:27:58 +02:00
|
|
|
print(f" diff: {diff_doc.pageCount()} Seiten")
|
|
|
|
|
print(f" ref: {ref_doc.pageCount()} Seiten")
|
|
|
|
|
print(f" new: {new_doc.pageCount()} Seiten")
|
|
|
|
|
|
|
|
|
|
# Nehme die Seitenzahl der diff-PDF als Basis
|
|
|
|
|
max_pages = diff_doc.pageCount()
|
2025-05-27 18:31:45 +02:00
|
|
|
|
2025-05-31 21:27:58 +02:00
|
|
|
# Performance-Test: Messe Thumbnail-Erstellung
|
|
|
|
|
start_time = time.time()
|
2025-05-27 18:31:45 +02:00
|
|
|
|
2025-05-29 16:30:01 +02:00
|
|
|
# Erstelle nur Thumbnails (keine Vollbilder)
|
2025-05-27 18:31:45 +02:00
|
|
|
for page_num in range(max_pages):
|
2025-05-29 16:30:01 +02:00
|
|
|
# Nur diff-Seite für Thumbnail rendern
|
2025-05-31 21:27:58 +02:00
|
|
|
page_size = diff_doc.pagePointSize(page_num)
|
|
|
|
|
|
|
|
|
|
# Skalierung für Thumbnail (entspricht ca. Matrix(1.0, 1.0) in PyMuPDF)
|
|
|
|
|
scale_factor = 200.0 / page_size.width() # 200 Pixel Breite für Thumbnail
|
|
|
|
|
|
|
|
|
|
# Seite rendern
|
|
|
|
|
page_image = diff_doc.render(page_num, QSize(int(page_size.width() * scale_factor),
|
|
|
|
|
int(page_size.height() * scale_factor)))
|
|
|
|
|
|
|
|
|
|
diff_pixmap = QPixmap.fromImage(page_image)
|
2025-05-27 18:31:45 +02:00
|
|
|
|
2025-05-29 16:30:01 +02:00
|
|
|
# Thumbnail erstellen und zur linken Spalte hinzufügen
|
2025-05-27 18:31:45 +02:00
|
|
|
thumbnail = QLabel()
|
|
|
|
|
thumbnail.setObjectName(f"thumbnail_{pdf_filename}_page_{page_num + 1}")
|
|
|
|
|
thumbnail.setPixmap(diff_pixmap.scaledToWidth(200, Qt.TransformationMode.SmoothTransformation))
|
|
|
|
|
thumbnail.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
|
|
|
|
|
thumbnail.setMouseTracking(True)
|
|
|
|
|
self.ui.verticalLayout_2.addWidget(thumbnail)
|
2025-05-28 19:30:37 +02:00
|
|
|
|
2025-05-29 16:30:01 +02:00
|
|
|
# Seitennummer für Thumbnail anzeigen
|
|
|
|
|
thumbnail_info = QLabel(f"Seite {page_num + 1}")
|
|
|
|
|
thumbnail_info.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
|
|
|
self.ui.verticalLayout_2.addWidget(thumbnail_info)
|
2025-05-27 18:31:45 +02:00
|
|
|
|
2025-05-29 16:30:01 +02:00
|
|
|
# Beziehung zwischen Thumbnail und Seitennummer speichern
|
|
|
|
|
self.thumbnail_to_page[thumbnail] = {
|
|
|
|
|
'pdf_filename': pdf_filename,
|
|
|
|
|
'page_num': page_num
|
|
|
|
|
}
|
2025-05-27 18:31:45 +02:00
|
|
|
|
|
|
|
|
# Click-Event für das Thumbnail einrichten
|
|
|
|
|
thumbnail.mousePressEvent = lambda event, t=thumbnail: self.on_thumbnail_clicked(event, t)
|
2025-05-28 19:30:37 +02:00
|
|
|
|
2025-05-29 16:30:01 +02:00
|
|
|
print(f"Thumbnail für Seite {page_num + 1} erstellt")
|
2025-05-28 19:30:37 +02:00
|
|
|
|
2025-05-31 21:27:58 +02:00
|
|
|
thumbnail_time = time.time() - start_time
|
|
|
|
|
print(f"Performance: {max_pages} Thumbnails in {thumbnail_time:.3f}s")
|
|
|
|
|
|
2025-05-29 16:30:01 +02:00
|
|
|
# Setze die erste PDF als aktuelle PDF
|
|
|
|
|
if self.current_pdf is None:
|
|
|
|
|
self.current_pdf = pdf_filename
|
2025-05-27 18:31:45 +02:00
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"Fehler beim Laden der PDFs: {e}")
|
|
|
|
|
|
2025-06-16 20:30:56 +02:00
|
|
|
# Erstelle das eine Vollbild-Label für die rechte Spalte (immer erstellen)
|
|
|
|
|
if self.fullsize_label is None:
|
|
|
|
|
self.fullsize_label = QLabel()
|
|
|
|
|
self.fullsize_label.setObjectName("fullsize_current_page")
|
|
|
|
|
self.fullsize_label.setAlignment(Qt.AlignmentFlag.AlignHCenter)
|
|
|
|
|
self.fullsize_label.setCursor(QCursor(Qt.CursorShape.OpenHandCursor))
|
|
|
|
|
self.ui.verticalLayout_3.addWidget(self.fullsize_label)
|
2025-05-29 16:30:01 +02:00
|
|
|
|
2025-06-16 20:30:56 +02:00
|
|
|
# Drag-to-Scroll Events für das große Bild einrichten
|
|
|
|
|
self.fullsize_label.mousePressEvent = lambda event: self.on_fullsize_mouse_press(event, self.fullsize_label)
|
|
|
|
|
self.fullsize_label.mouseMoveEvent = lambda event: self.on_fullsize_mouse_move(event, self.fullsize_label)
|
|
|
|
|
self.fullsize_label.mouseReleaseEvent = lambda event: self.on_fullsize_mouse_release(event, self.fullsize_label)
|
2025-05-29 16:30:01 +02:00
|
|
|
|
|
|
|
|
# Zeige die erste Seite initial an
|
|
|
|
|
if self.current_pdf:
|
|
|
|
|
self.render_and_display_page(self.current_pdf, 0)
|
|
|
|
|
|
|
|
|
|
def render_and_display_page(self, pdf_filename, page_num):
|
|
|
|
|
"""
|
|
|
|
|
Rendert und zeigt eine spezifische Seite in der Vollansicht an.
|
2025-05-29 19:03:19 +02:00
|
|
|
Cached die gerenderten Pixmaps für bessere Performance.
|
2025-05-29 16:30:01 +02:00
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
pdf_filename: Name der PDF-Datei
|
|
|
|
|
page_num: Seitennummer (0-basiert)
|
|
|
|
|
"""
|
|
|
|
|
print(f"Rendere Seite {page_num + 1} von {pdf_filename}")
|
|
|
|
|
|
|
|
|
|
if pdf_filename not in self.pdf_documents:
|
|
|
|
|
print(f"PDF-Dokument {pdf_filename} nicht gefunden")
|
|
|
|
|
return
|
|
|
|
|
|
2025-05-31 21:27:58 +02:00
|
|
|
start_time = time.time()
|
|
|
|
|
|
2025-05-29 16:30:01 +02:00
|
|
|
try:
|
|
|
|
|
docs = self.pdf_documents[pdf_filename]
|
|
|
|
|
|
2025-05-29 21:21:18 +02:00
|
|
|
# Diff-Seite laden (bestimmt die Abmessungen)
|
2025-05-31 21:27:58 +02:00
|
|
|
diff_doc = docs['diff']
|
|
|
|
|
page_size = diff_doc.pagePointSize(page_num)
|
|
|
|
|
|
|
|
|
|
# Hohe Auflösung für Vollbild (entspricht ca. Matrix(2.0, 2.0) in PyMuPDF)
|
|
|
|
|
scale_factor = 2.0
|
|
|
|
|
render_size = QSize(int(page_size.width() * scale_factor),
|
|
|
|
|
int(page_size.height() * scale_factor))
|
2025-05-29 16:30:01 +02:00
|
|
|
|
2025-05-29 21:21:18 +02:00
|
|
|
# Diff-Seite rendern (immer vorhanden)
|
2025-05-31 21:27:58 +02:00
|
|
|
diff_image = diff_doc.render(page_num, render_size)
|
|
|
|
|
diff_pixmap = QPixmap.fromImage(diff_image)
|
2025-05-29 21:21:18 +02:00
|
|
|
|
2025-05-31 21:27:58 +02:00
|
|
|
# Ermittle die Abmessungen für weiße Seiten
|
2025-05-29 21:21:18 +02:00
|
|
|
diff_width = diff_pixmap.width()
|
|
|
|
|
diff_height = diff_pixmap.height()
|
|
|
|
|
|
|
|
|
|
# Ref-Seite prüfen und rendern oder weiße Seite erstellen
|
2025-05-31 21:27:58 +02:00
|
|
|
ref_doc = docs['ref']
|
|
|
|
|
if page_num < ref_doc.pageCount():
|
|
|
|
|
ref_image = ref_doc.render(page_num, render_size)
|
|
|
|
|
ref_pixmap = QPixmap.fromImage(ref_image)
|
2025-05-29 21:21:18 +02:00
|
|
|
print(f"Ref-Seite {page_num + 1} gerendert")
|
|
|
|
|
else:
|
|
|
|
|
# Erstelle weiße Seite mit gleichen Abmessungen wie Diff-Seite
|
|
|
|
|
ref_pixmap = QPixmap(diff_width, diff_height)
|
|
|
|
|
ref_pixmap.fill(Qt.GlobalColor.white)
|
2025-05-31 21:27:58 +02:00
|
|
|
print(f"Weiße Ref-Seite {page_num + 1} erstellt")
|
2025-05-29 21:21:18 +02:00
|
|
|
|
|
|
|
|
# New-Seite prüfen und rendern oder weiße Seite erstellen
|
2025-05-31 21:27:58 +02:00
|
|
|
new_doc = docs['new']
|
|
|
|
|
if page_num < new_doc.pageCount():
|
|
|
|
|
new_image = new_doc.render(page_num, render_size)
|
|
|
|
|
new_pixmap = QPixmap.fromImage(new_image)
|
2025-05-29 21:21:18 +02:00
|
|
|
print(f"New-Seite {page_num + 1} gerendert")
|
|
|
|
|
else:
|
|
|
|
|
# Erstelle weiße Seite mit gleichen Abmessungen wie Diff-Seite
|
|
|
|
|
new_pixmap = QPixmap(diff_width, diff_height)
|
|
|
|
|
new_pixmap.fill(Qt.GlobalColor.white)
|
2025-05-31 21:27:58 +02:00
|
|
|
print(f"Weiße New-Seite {page_num + 1} erstellt")
|
2025-05-29 16:30:01 +02:00
|
|
|
|
2025-05-29 19:03:19 +02:00
|
|
|
# Cache die gerenderten Pixmaps für schnelle Alpha/Zoom-Operationen
|
|
|
|
|
self.current_rendered_pixmaps = {
|
|
|
|
|
'ref': ref_pixmap,
|
|
|
|
|
'diff': diff_pixmap,
|
|
|
|
|
'new': new_pixmap
|
|
|
|
|
}
|
2025-05-29 16:30:01 +02:00
|
|
|
|
|
|
|
|
# Aktualisiere aktuelle Seite
|
|
|
|
|
self.current_page = page_num
|
|
|
|
|
self.current_pdf = pdf_filename
|
|
|
|
|
|
2025-05-29 19:03:19 +02:00
|
|
|
# Zeige das Bild mit aktuellem Alpha- und Zoom-Wert an
|
|
|
|
|
self.update_current_display()
|
|
|
|
|
|
2025-05-31 21:27:58 +02:00
|
|
|
render_time = time.time() - start_time
|
|
|
|
|
print(f"Performance: Seite {page_num + 1} gerendert in {render_time:.3f}s")
|
2025-05-29 16:30:01 +02:00
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"Fehler beim Rendern der Seite {page_num + 1}: {e}")
|
2025-05-23 11:09:47 +02:00
|
|
|
|
2025-05-29 19:03:19 +02:00
|
|
|
def update_current_display(self):
|
|
|
|
|
"""
|
|
|
|
|
Aktualisiert die Anzeige der aktuellen Seite basierend auf gecachten Pixmaps.
|
|
|
|
|
Verwendet für Alpha- und Zoom-Änderungen ohne erneutes PDF-Rendering.
|
|
|
|
|
"""
|
|
|
|
|
if not self.current_rendered_pixmaps:
|
|
|
|
|
print("Keine gerenderten Pixmaps verfügbar")
|
|
|
|
|
return
|
|
|
|
|
|
2025-06-16 20:30:56 +02:00
|
|
|
if self.fullsize_label is None:
|
|
|
|
|
print("Fullsize-Label ist nicht verfügbar")
|
|
|
|
|
return
|
|
|
|
|
|
2025-05-29 19:03:19 +02:00
|
|
|
# Hole die gecachten Pixmaps
|
|
|
|
|
ref_pixmap = self.current_rendered_pixmaps['ref']
|
|
|
|
|
diff_pixmap = self.current_rendered_pixmaps['diff']
|
|
|
|
|
new_pixmap = self.current_rendered_pixmaps['new']
|
|
|
|
|
|
|
|
|
|
# Erstelle das überlagerte Bild mit aktuellem Alpha-Wert
|
|
|
|
|
alpha_value = self.ui.alpha.value()
|
|
|
|
|
layered_pixmap = self.create_layered_pixmap(ref_pixmap, diff_pixmap, new_pixmap, alpha_value)
|
|
|
|
|
|
|
|
|
|
# Wende aktuellen Zoom an
|
|
|
|
|
zoom_factor = self.current_zoom / 100.0
|
|
|
|
|
if zoom_factor != 1.0:
|
|
|
|
|
new_width = int(layered_pixmap.width() * zoom_factor)
|
|
|
|
|
layered_pixmap = layered_pixmap.scaledToWidth(new_width, Qt.TransformationMode.SmoothTransformation)
|
|
|
|
|
|
|
|
|
|
# Setze das überlagerte Bild
|
|
|
|
|
self.fullsize_label.setPixmap(layered_pixmap)
|
|
|
|
|
self.fullsize_label.setAlignment(Qt.AlignmentFlag.AlignHCenter)
|
|
|
|
|
|
2025-05-27 18:31:45 +02:00
|
|
|
def create_layered_pixmap(self, ref_pixmap, diff_pixmap, new_pixmap, alpha_value):
|
|
|
|
|
"""
|
|
|
|
|
Erstellt ein übergelagertes Pixmap basierend auf dem Alpha-Wert.
|
2025-05-28 19:30:37 +02:00
|
|
|
|
2025-05-27 18:31:45 +02:00
|
|
|
Args:
|
|
|
|
|
ref_pixmap: Unterste Ebene (ref)
|
|
|
|
|
diff_pixmap: Mittlere Ebene (diff)
|
|
|
|
|
new_pixmap: Oberste Ebene (new)
|
|
|
|
|
alpha_value: Alpha-Wert (-100 bis 100)
|
2025-05-28 19:30:37 +02:00
|
|
|
|
2025-05-27 18:31:45 +02:00
|
|
|
Returns:
|
|
|
|
|
QPixmap: Das überlagerte Bild
|
|
|
|
|
"""
|
|
|
|
|
# Verwende die Größe des größten Bildes
|
|
|
|
|
max_width = max(ref_pixmap.width(), diff_pixmap.width(), new_pixmap.width())
|
|
|
|
|
max_height = max(ref_pixmap.height(), diff_pixmap.height(), new_pixmap.height())
|
2025-05-28 19:30:37 +02:00
|
|
|
|
2025-05-27 18:31:45 +02:00
|
|
|
# Erstelle ein leeres Pixmap für das Ergebnis
|
|
|
|
|
result = QPixmap(max_width, max_height)
|
|
|
|
|
result.fill(Qt.GlobalColor.white)
|
2025-05-28 19:30:37 +02:00
|
|
|
|
2025-05-27 18:31:45 +02:00
|
|
|
painter = QPainter(result)
|
|
|
|
|
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
2025-05-28 19:30:37 +02:00
|
|
|
|
2025-05-27 18:31:45 +02:00
|
|
|
if alpha_value <= 0:
|
|
|
|
|
# Alpha von -100 bis 0: Übergang von ref zu diff
|
2025-06-15 19:52:24 +02:00
|
|
|
ref_opacity = 1.0 # - (alpha_value + 100) / 100.0
|
2025-05-27 18:31:45 +02:00
|
|
|
diff_opacity = (alpha_value + 100) / 100.0
|
|
|
|
|
new_opacity = 0.0
|
|
|
|
|
else:
|
|
|
|
|
# Alpha von 0 bis 100: Übergang von diff zu new
|
|
|
|
|
ref_opacity = 0.0
|
2025-06-15 19:52:24 +02:00
|
|
|
diff_opacity = 1.0 # - alpha_value / 100.0
|
2025-05-27 18:31:45 +02:00
|
|
|
new_opacity = alpha_value / 100.0
|
2025-05-28 19:30:37 +02:00
|
|
|
|
2025-05-27 18:31:45 +02:00
|
|
|
# Zeichne die Ebenen mit entsprechender Transparenz
|
|
|
|
|
if ref_opacity > 0:
|
|
|
|
|
painter.setOpacity(ref_opacity)
|
|
|
|
|
painter.drawPixmap(0, 0, ref_pixmap)
|
2025-05-28 19:30:37 +02:00
|
|
|
|
2025-05-27 18:31:45 +02:00
|
|
|
if diff_opacity > 0:
|
|
|
|
|
painter.setOpacity(diff_opacity)
|
|
|
|
|
painter.drawPixmap(0, 0, diff_pixmap)
|
2025-05-28 19:30:37 +02:00
|
|
|
|
2025-05-27 18:31:45 +02:00
|
|
|
if new_opacity > 0:
|
|
|
|
|
painter.setOpacity(new_opacity)
|
|
|
|
|
painter.drawPixmap(0, 0, new_pixmap)
|
2025-05-28 19:30:37 +02:00
|
|
|
|
2025-05-27 18:31:45 +02:00
|
|
|
painter.end()
|
|
|
|
|
return result
|
2025-05-23 11:09:47 +02:00
|
|
|
|
2025-05-31 21:27:58 +02:00
|
|
|
def _clear_layout(self, layout):
|
|
|
|
|
"""Entfernt alle Widgets aus einem Layout."""
|
|
|
|
|
if layout is not None:
|
|
|
|
|
while layout.count():
|
|
|
|
|
item = layout.takeAt(0)
|
|
|
|
|
widget = item.widget()
|
|
|
|
|
if widget is not None:
|
|
|
|
|
widget.deleteLater()
|
|
|
|
|
|
|
|
|
|
def _connect_signals(self):
|
|
|
|
|
"""Verbindet Signale mit den entsprechenden Slots."""
|
|
|
|
|
# Button-Klicks verbinden
|
|
|
|
|
self.ui.pushButton.clicked.connect(self.on_button_clicked)
|
|
|
|
|
|
|
|
|
|
# Zoom-Slider verbinden
|
|
|
|
|
self.ui.zoom.valueChanged.connect(self.apply_zoom)
|
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-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-08-10 14:03:15 +02:00
|
|
|
|
|
|
|
|
# Button "lade aus FN2" verbinden
|
|
|
|
|
self.ui.pB_lade_aus_fn2.clicked.connect(self.on_load_from_fn2_clicked)
|
2025-05-31 21:27:58 +02:00
|
|
|
|
2025-08-03 16:31:38 +02:00
|
|
|
def _setup_tree_context_menu(self):
|
|
|
|
|
"""Richtet das Kontextmenü für das TreeWidget ein."""
|
|
|
|
|
# Aktiviere Kontextmenü für das TreeWidget
|
|
|
|
|
self.ui.treeWidget.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
|
|
|
|
self.ui.treeWidget.customContextMenuRequested.connect(self._show_tree_context_menu)
|
|
|
|
|
print("Kontextmenü für TreeWidget eingerichtet")
|
|
|
|
|
|
|
|
|
|
def _show_tree_context_menu(self, position):
|
|
|
|
|
"""
|
|
|
|
|
Zeigt das Kontextmenü für das TreeWidget an.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
position: Position des Rechtsklicks
|
|
|
|
|
"""
|
|
|
|
|
# Hole das Item an der Position
|
|
|
|
|
item = self.ui.treeWidget.itemAt(position)
|
|
|
|
|
|
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-08-03 16:31:38 +02:00
|
|
|
|
|
|
|
|
if context_menu:
|
|
|
|
|
# Zeige das Kontextmenü an der globalen Position
|
|
|
|
|
global_pos = self.ui.treeWidget.mapToGlobal(position)
|
|
|
|
|
context_menu.exec(global_pos)
|
|
|
|
|
|
|
|
|
|
def _get_node_type_from_item(self, item):
|
|
|
|
|
"""
|
|
|
|
|
Bestimmt den Node-Typ basierend auf dem TreeWidgetItem.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
item: Das TreeWidgetItem
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
str: Der Node-Typ ('TreeNode', 'XslFile', 'XmlFile' oder 'Unknown')
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
# Prüfe ob das Item ein Parent hat (dann ist es ein Child-Item)
|
|
|
|
|
parent_item = item.parent()
|
|
|
|
|
|
|
|
|
|
if parent_item:
|
|
|
|
|
# Child-Item - prüfe ob es ein XML-File ist
|
|
|
|
|
text = item.text(0)
|
|
|
|
|
if text.startswith("XML:"):
|
|
|
|
|
return "XmlFile"
|
|
|
|
|
else:
|
|
|
|
|
# Könnte ein TreeNode-Child oder XslFile-Child sein
|
|
|
|
|
# Prüfe den Parent-Typ
|
|
|
|
|
parent_type = self._get_node_type_from_item(parent_item)
|
|
|
|
|
if parent_type == "XslFile":
|
|
|
|
|
return "XmlFile"
|
|
|
|
|
else:
|
|
|
|
|
# Rekursiv bestimmen basierend auf gespeicherten Daten
|
|
|
|
|
return self._determine_node_type_from_data(item)
|
|
|
|
|
else:
|
|
|
|
|
# Root-Item - bestimme Typ basierend auf gespeicherten Daten
|
|
|
|
|
return self._determine_node_type_from_data(item)
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"Fehler beim Bestimmen des Node-Typs: {e}")
|
|
|
|
|
return "Unknown"
|
|
|
|
|
|
|
|
|
|
def _determine_node_type_from_data(self, item):
|
|
|
|
|
"""
|
|
|
|
|
Bestimmt den Node-Typ basierend auf den gespeicherten Daten im Item.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
item: Das TreeWidgetItem
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
str: Der Node-Typ ('TreeNode', 'XslFile' oder 'Unknown')
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
# Hole die gespeicherte Node-ID
|
|
|
|
|
node_id = item.data(0, Qt.ItemDataRole.UserRole)
|
|
|
|
|
if not node_id:
|
|
|
|
|
return "Unknown"
|
|
|
|
|
|
|
|
|
|
# Suche in den Projekt-Nodes nach der ID
|
|
|
|
|
if hasattr(self, 'pdf_project') and self.pdf_project and self.pdf_project.nodes:
|
|
|
|
|
node = self._find_node_by_id(self.pdf_project.nodes, node_id)
|
|
|
|
|
if node:
|
|
|
|
|
if isinstance(node, TreeNode):
|
|
|
|
|
return "TreeNode"
|
|
|
|
|
elif isinstance(node, XslFile):
|
|
|
|
|
return "XslFile"
|
|
|
|
|
|
|
|
|
|
return "Unknown"
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"Fehler beim Bestimmen des Node-Typs aus Daten: {e}")
|
|
|
|
|
return "Unknown"
|
|
|
|
|
|
|
|
|
|
def _find_node_by_id(self, nodes, target_id):
|
|
|
|
|
"""
|
|
|
|
|
Sucht rekursiv nach einem Node mit der angegebenen ID.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
nodes: Liste der Nodes zum Durchsuchen
|
|
|
|
|
target_id: Die zu suchende ID
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
TreeNode|XslFile|None: Der gefundene Node oder None
|
|
|
|
|
"""
|
|
|
|
|
for node in nodes:
|
|
|
|
|
if node.id == target_id:
|
|
|
|
|
return node
|
|
|
|
|
|
|
|
|
|
# Rekursiv in Kindern suchen (nur bei TreeNode)
|
|
|
|
|
if isinstance(node, TreeNode) and node.children:
|
|
|
|
|
found = self._find_node_by_id(node.children, target_id)
|
|
|
|
|
if found:
|
|
|
|
|
return found
|
|
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def _create_context_menu_for_type(self, node_type, item):
|
|
|
|
|
"""
|
|
|
|
|
Erstellt das Kontextmenü für den angegebenen Node-Typ.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
node_type: Der Typ des Nodes ('TreeNode', 'XslFile', 'XmlFile')
|
|
|
|
|
item: Das TreeWidgetItem
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
QMenu: Das erstellte Kontextmenü oder None
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
menu = QMenu(self)
|
|
|
|
|
|
|
|
|
|
if node_type == "TreeNode":
|
|
|
|
|
# Kontextmenü für TreeNode
|
|
|
|
|
action_add_child = QAction("Unterknoten hinzufügen", self)
|
2025-08-03 20:31:32 +02:00
|
|
|
action_add_child.setIcon(QIcon(QIcon.fromTheme(u"folder-new")))
|
2025-08-03 16:31:38 +02:00
|
|
|
action_add_child.triggered.connect(lambda: self._add_tree_node_child(item))
|
|
|
|
|
menu.addAction(action_add_child)
|
|
|
|
|
|
|
|
|
|
action_add_xsl = QAction("XSL-Datei hinzufügen", self)
|
2025-08-03 20:31:32 +02:00
|
|
|
action_add_xsl.setIcon(QIcon(QIcon.fromTheme("document-new")))
|
2025-08-03 16:31:38 +02:00
|
|
|
action_add_xsl.triggered.connect(lambda: self._add_xsl_file_to_node(item))
|
|
|
|
|
menu.addAction(action_add_xsl)
|
|
|
|
|
|
|
|
|
|
menu.addSeparator()
|
|
|
|
|
|
|
|
|
|
action_edit = QAction("Bearbeiten", self)
|
2025-08-03 20:31:32 +02:00
|
|
|
action_edit.setIcon(QIcon(QIcon.fromTheme(QIcon.ThemeIcon.DocumentProperties)))
|
2025-08-03 16:31:38 +02:00
|
|
|
action_edit.triggered.connect(lambda: self._edit_tree_node(item))
|
|
|
|
|
menu.addAction(action_edit)
|
|
|
|
|
|
|
|
|
|
action_delete = QAction("Löschen", self)
|
2025-08-03 20:31:32 +02:00
|
|
|
action_delete.setIcon(QIcon(QIcon.fromTheme(QIcon.ThemeIcon.EditDelete)))
|
2025-08-03 16:31:38 +02:00
|
|
|
action_delete.triggered.connect(lambda: self._delete_tree_node(item))
|
|
|
|
|
menu.addAction(action_delete)
|
|
|
|
|
|
|
|
|
|
elif node_type == "XslFile":
|
|
|
|
|
# Kontextmenü für XslFile
|
|
|
|
|
action_add_xml = QAction("XML-Datei hinzufügen", self)
|
2025-08-03 20:31:32 +02:00
|
|
|
action_add_xml.setIcon(QIcon(QIcon.fromTheme("document-new")))
|
2025-08-03 16:31:38 +02:00
|
|
|
action_add_xml.triggered.connect(lambda: self._add_xml_file_to_xsl(item))
|
|
|
|
|
menu.addAction(action_add_xml)
|
|
|
|
|
|
|
|
|
|
menu.addSeparator()
|
|
|
|
|
|
|
|
|
|
action_edit = QAction("Bearbeiten", self)
|
2025-08-03 20:31:32 +02:00
|
|
|
action_edit.setIcon(QIcon(QIcon.fromTheme(QIcon.ThemeIcon.DocumentProperties)))
|
2025-08-03 16:31:38 +02:00
|
|
|
action_edit.triggered.connect(lambda: self._edit_xsl_file(item))
|
|
|
|
|
menu.addAction(action_edit)
|
|
|
|
|
|
|
|
|
|
action_delete = QAction("Löschen", self)
|
2025-08-03 20:31:32 +02:00
|
|
|
action_delete.setIcon(QIcon(QIcon.fromTheme(QIcon.ThemeIcon.EditDelete)))
|
2025-08-03 16:31:38 +02:00
|
|
|
action_delete.triggered.connect(lambda: self._delete_xsl_file(item))
|
|
|
|
|
menu.addAction(action_delete)
|
|
|
|
|
|
|
|
|
|
elif node_type == "XmlFile":
|
|
|
|
|
# Kontextmenü für XmlFile
|
|
|
|
|
action_edit = QAction("Bearbeiten", self)
|
2025-08-03 20:31:32 +02:00
|
|
|
action_edit.setIcon(QIcon(QIcon.fromTheme(QIcon.ThemeIcon.DocumentProperties)))
|
2025-08-03 16:31:38 +02:00
|
|
|
action_edit.triggered.connect(lambda: self._edit_xml_file(item))
|
|
|
|
|
menu.addAction(action_edit)
|
|
|
|
|
|
|
|
|
|
action_delete = QAction("Löschen", self)
|
2025-08-03 20:31:32 +02:00
|
|
|
action_delete.setIcon(QIcon(QIcon.fromTheme(QIcon.ThemeIcon.EditDelete)))
|
2025-08-03 16:31:38 +02:00
|
|
|
action_delete.triggered.connect(lambda: self._delete_xml_file(item))
|
|
|
|
|
menu.addAction(action_delete)
|
|
|
|
|
|
|
|
|
|
else:
|
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-08-03 20:31:32 +02:00
|
|
|
action_add_tree_node.setIcon(QIcon(QIcon.fromTheme(u"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-08-03 16:31:38 +02:00
|
|
|
|
|
|
|
|
return menu
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"Fehler beim Erstellen des Kontextmenüs: {e}")
|
|
|
|
|
return None
|
|
|
|
|
|
2025-05-31 21:27:58 +02:00
|
|
|
def on_alpha_changed(self, alpha_value):
|
|
|
|
|
"""
|
|
|
|
|
Wird ausgeführt, wenn der Alpha-Slider geändert wird.
|
|
|
|
|
Optimiert: Verwendet gecachte Pixmaps statt erneutes PDF-Rendering.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
alpha_value: Der neue Alpha-Wert (-100 bis 100)
|
|
|
|
|
"""
|
|
|
|
|
print(f"Alpha geändert auf {alpha_value}")
|
|
|
|
|
|
|
|
|
|
start_time = time.time()
|
|
|
|
|
# Verwende gecachte Pixmaps für schnelle Alpha-Änderungen
|
|
|
|
|
self.update_current_display()
|
|
|
|
|
alpha_time = time.time() - start_time
|
|
|
|
|
print(f"Alpha-Update in {alpha_time:.6f}s")
|
|
|
|
|
|
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
|
|
|
|
|
print("Einstellungen wurden gespeichert")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"Fehler beim Öffnen des Einstellungen-Dialogs: {e}")
|
|
|
|
|
|
2025-06-16 20:30:56 +02:00
|
|
|
def open_new_project_dialog(self):
|
|
|
|
|
"""Öffnet Pdf-Projekt-Dialog."""
|
|
|
|
|
try:
|
|
|
|
|
# Erstelle und zeige den PdfProject-Dialog
|
|
|
|
|
dialog = PdfProjectDlg(self)
|
|
|
|
|
if dialog.exec() == PdfProjectDlg.DialogCode.Accepted:
|
|
|
|
|
# Hole die Projektdaten aus dem Dialog
|
|
|
|
|
project_data = dialog.get_project_data()
|
|
|
|
|
|
|
|
|
|
# Erstelle neue ID für das Projekt
|
|
|
|
|
new_id = max([p.id for p in app_settings.pdf_projects], default=0) + 1
|
|
|
|
|
|
|
|
|
|
# Erstelle PdfProject-Objekt
|
|
|
|
|
new_project = PdfProject(
|
|
|
|
|
id=new_id,
|
|
|
|
|
name=project_data['name'],
|
|
|
|
|
project_dir=Path(project_data['project_dir']),
|
|
|
|
|
java_vm_id=project_data['java_vm_id'] if project_data['java_vm_id'] != -1 else 1,
|
|
|
|
|
diff_pdf_id=project_data['diff_pdf_id'] if project_data['diff_pdf_id'] != -1 else 1,
|
|
|
|
|
saxon_jar_id=project_data['saxon_jar_id'] if project_data['saxon_jar_id'] != -1 else 1,
|
|
|
|
|
apache_fop_id=project_data['apache_fop_id'] if project_data['apache_fop_id'] != -1 else 1,
|
|
|
|
|
xsl_dir_id=project_data['xsl_dir_id'] if project_data['xsl_dir_id'] != -1 else 1,
|
2025-07-14 21:00:06 +02:00
|
|
|
postgre_sql_db_id=project_data['postgre_sql_db_id'] if project_data['postgre_sql_db_id'] != -1 else 1,
|
2025-06-16 20:30:56 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Erstelle Projekt-Ordnerstruktur
|
|
|
|
|
self._create_project_structure(new_project)
|
|
|
|
|
|
|
|
|
|
# Füge das neue Projekt zu app_settings hinzu
|
|
|
|
|
app_settings.pdf_projects.append(new_project)
|
|
|
|
|
|
|
|
|
|
# Speichere app_settings
|
|
|
|
|
app_settings.save()
|
|
|
|
|
|
|
|
|
|
print(f"Neues PDF-Projekt '{project_data['name']}' wurde erstellt und gespeichert")
|
|
|
|
|
print(f"Projekt-ID: {new_id}")
|
|
|
|
|
print(f"Projekt-Ordner: {project_data['project_dir']}")
|
|
|
|
|
|
2025-06-22 11:58:57 +02:00
|
|
|
# Aktualisiere das Projekte-Menü
|
|
|
|
|
self._setup_projects_menu()
|
|
|
|
|
|
2025-06-16 20:30:56 +02:00
|
|
|
except Exception as e:
|
|
|
|
|
print(f"Fehler beim Erstellen des neuen Projekts: {e}")
|
|
|
|
|
|
|
|
|
|
def _create_project_structure(self, project: PdfProject):
|
|
|
|
|
"""
|
|
|
|
|
Erstellt die Ordnerstruktur und project.yaml-Datei für ein neues Projekt.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
project: Das PdfProject-Objekt
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
project_dir = Path(project.project_dir)
|
|
|
|
|
|
|
|
|
|
# Erstelle Unterordner
|
|
|
|
|
subdirs = ['xml', 'new', 'diff', 'ref', 'tmp']
|
|
|
|
|
for subdir in subdirs:
|
|
|
|
|
subdir_path = project_dir / subdir
|
|
|
|
|
subdir_path.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
print(f"Ordner erstellt: {subdir_path}")
|
|
|
|
|
|
|
|
|
|
project_yaml_path = project_dir / 'project.yaml'
|
|
|
|
|
|
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
|
|
|
|
|
default_settings = PdfProjectSettings()
|
|
|
|
|
|
|
|
|
|
# Speichere die Standard-Einstellungen in die project.yaml
|
|
|
|
|
default_settings.writeSettings(project_dir=project_dir)
|
|
|
|
|
print(f"project.yaml mit Standard-Einstellungen erstellt: {project_yaml_path}")
|
|
|
|
|
else:
|
|
|
|
|
print(f"project.yaml existiert bereits: {project_yaml_path}")
|
2025-06-16 20:30:56 +02:00
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"Fehler beim Erstellen der Projekt-Struktur: {e}")
|
|
|
|
|
raise
|
|
|
|
|
|
2025-05-22 19:10:39 +02:00
|
|
|
def on_button_clicked(self):
|
|
|
|
|
"""Wird ausgeführt, wenn der Button geklickt wird."""
|
|
|
|
|
print("Button wurde geklickt!")
|
2025-05-23 11:09:47 +02:00
|
|
|
|
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:
|
|
|
|
|
pdf_filename = page_info['pdf_filename']
|
|
|
|
|
page_num = page_info['page_num']
|
|
|
|
|
|
|
|
|
|
print(f"Thumbnail für Seite {page_num + 1} von {pdf_filename} wurde angeklickt")
|
|
|
|
|
|
|
|
|
|
# Rendere und zeige die gewählte Seite an
|
|
|
|
|
self.render_and_display_page(pdf_filename, page_num)
|
2025-05-23 11:09:47 +02:00
|
|
|
|
2025-05-22 21:05:22 +02:00
|
|
|
def apply_zoom(self, zoom_value):
|
|
|
|
|
"""
|
2025-05-29 16:30:01 +02:00
|
|
|
Wendet den Zoom-Faktor auf das aktuelle Bild an.
|
2025-05-29 19:03:19 +02:00
|
|
|
Optimiert: Verwendet gecachte Pixmaps statt erneutes PDF-Rendering.
|
2025-05-23 11:09:47 +02:00
|
|
|
|
2025-05-22 21:05:22 +02:00
|
|
|
Args:
|
|
|
|
|
zoom_value: Der neue Zoom-Wert (in Prozent)
|
|
|
|
|
"""
|
|
|
|
|
self.current_zoom = zoom_value
|
|
|
|
|
print(f"Zoom geändert auf {zoom_value}%")
|
2025-05-23 11:09:47 +02:00
|
|
|
|
2025-05-29 19:03:19 +02:00
|
|
|
# Verwende gecachte Pixmaps für schnelle Zoom-Änderungen
|
|
|
|
|
self.update_current_display()
|
2025-05-23 21:26:50 +02:00
|
|
|
|
|
|
|
|
def on_fullsize_mouse_press(self, event, fullsize_label):
|
2025-05-31 21:27:58 +02:00
|
|
|
"""Wird ausgeführt, wenn die Maustaste auf einem großen Bild gedrückt wird."""
|
2025-05-23 21:26:50 +02:00
|
|
|
if event.button() == Qt.MouseButton.LeftButton:
|
|
|
|
|
self.is_dragging = True
|
|
|
|
|
self.last_drag_position = event.globalPosition().toPoint()
|
|
|
|
|
fullsize_label.setCursor(QCursor(Qt.CursorShape.ClosedHandCursor))
|
|
|
|
|
|
|
|
|
|
def on_fullsize_mouse_move(self, event, fullsize_label):
|
2025-05-31 21:27:58 +02:00
|
|
|
"""Wird ausgeführt, wenn die Maus über einem großen Bild bewegt wird."""
|
2025-05-23 21:26:50 +02:00
|
|
|
if self.is_dragging and self.last_drag_position is not None:
|
|
|
|
|
current_pos = event.globalPosition().toPoint()
|
|
|
|
|
delta = current_pos - self.last_drag_position
|
2025-05-28 19:30:37 +02:00
|
|
|
|
2025-05-23 21:26:50 +02:00
|
|
|
if abs(delta.x()) >= self.drag_threshold or abs(delta.y()) >= self.drag_threshold:
|
|
|
|
|
v_scrollbar = self.ui.scrollArea_2.verticalScrollBar()
|
|
|
|
|
h_scrollbar = self.ui.scrollArea_2.horizontalScrollBar()
|
2025-05-28 19:30:37 +02:00
|
|
|
|
2025-05-23 21:26:50 +02:00
|
|
|
scroll_delta_y = int(-delta.y() * self.scroll_sensitivity)
|
|
|
|
|
scroll_delta_x = int(-delta.x() * self.scroll_sensitivity)
|
2025-05-28 19:30:37 +02:00
|
|
|
|
2025-05-23 21:26:50 +02:00
|
|
|
new_v_value = v_scrollbar.value() + scroll_delta_y
|
|
|
|
|
new_h_value = h_scrollbar.value() + scroll_delta_x
|
2025-05-28 19:30:37 +02:00
|
|
|
|
2025-05-23 21:26:50 +02:00
|
|
|
v_scrollbar.setValue(new_v_value)
|
|
|
|
|
h_scrollbar.setValue(new_h_value)
|
2025-05-28 19:30:37 +02:00
|
|
|
|
2025-05-23 21:26:50 +02:00
|
|
|
self.last_drag_position = current_pos
|
|
|
|
|
|
|
|
|
|
def on_fullsize_mouse_release(self, event, fullsize_label):
|
2025-05-31 21:27:58 +02:00
|
|
|
"""Wird ausgeführt, wenn die Maustaste auf einem großen Bild losgelassen wird."""
|
2025-05-23 21:26:50 +02:00
|
|
|
if event.button() == Qt.MouseButton.LeftButton:
|
|
|
|
|
self.is_dragging = False
|
|
|
|
|
self.last_drag_position = None
|
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.
|
|
|
|
|
"""
|
|
|
|
|
print("Lade Nodes in TreeWidget...")
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
# TreeWidget leeren
|
|
|
|
|
self.ui.treeWidget.clear()
|
|
|
|
|
|
|
|
|
|
# Prüfe ob pdf_project existiert und Nodes hat
|
|
|
|
|
if not hasattr(self, 'pdf_project') or not self.pdf_project:
|
|
|
|
|
print("Keine Projekt-Einstellungen verfügbar")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
if not self.pdf_project.nodes:
|
|
|
|
|
print("Keine Nodes in den Projekt-Einstellungen gefunden")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Lade alle Root-Nodes
|
|
|
|
|
for node in self.pdf_project.nodes:
|
|
|
|
|
tree_item = self._create_tree_item_from_node(node)
|
|
|
|
|
self.ui.treeWidget.addTopLevelItem(tree_item)
|
|
|
|
|
|
|
|
|
|
print(f"{len(self.pdf_project.nodes)} Root-Nodes in TreeWidget geladen")
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"Fehler beim Laden der Nodes in TreeWidget: {e}")
|
|
|
|
|
|
|
|
|
|
def _create_tree_item_from_node(self, node):
|
|
|
|
|
"""
|
|
|
|
|
Erstellt ein QTreeWidgetItem aus einem TreeNode oder XslFile.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
node: TreeNode oder XslFile Objekt
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
QTreeWidgetItem: Das erstellte Tree-Item
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
# Erstelle Tree-Item
|
|
|
|
|
item = QTreeWidgetItem()
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
# Setze zusätzliche Informationen in Spalte 1
|
|
|
|
|
if isinstance(node, TreeNode):
|
|
|
|
|
# TreeNode: Zeige Anzahl der Kinder
|
|
|
|
|
child_count = len(node.children) if node.children else 0
|
|
|
|
|
item.setText(1, f"({child_count} Kinder)")
|
|
|
|
|
|
|
|
|
|
# Speichere Node-ID als Data
|
|
|
|
|
item.setData(0, Qt.ItemDataRole.UserRole, node.id)
|
|
|
|
|
|
|
|
|
|
# Lade Kinder rekursiv
|
|
|
|
|
if node.children:
|
|
|
|
|
for child in node.children:
|
|
|
|
|
child_item = self._create_tree_item_from_node(child)
|
|
|
|
|
item.addChild(child_item)
|
|
|
|
|
|
|
|
|
|
elif isinstance(node, XslFile):
|
|
|
|
|
# XslFile: Zeige XSL-Datei-Pfad
|
|
|
|
|
item.setText(1, str(node.xsl_file))
|
|
|
|
|
|
|
|
|
|
# Speichere XslFile-ID als Data
|
|
|
|
|
item.setData(0, Qt.ItemDataRole.UserRole, node.id)
|
|
|
|
|
|
|
|
|
|
# Lade XML-Dateien als Kinder
|
|
|
|
|
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))
|
|
|
|
|
item.addChild(xml_item)
|
|
|
|
|
|
|
|
|
|
return item
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"Fehler beim Erstellen des Tree-Items: {e}")
|
|
|
|
|
# Fallback: Erstelle einfaches Item
|
|
|
|
|
fallback_item = QTreeWidgetItem()
|
|
|
|
|
fallback_item.setText(0, "Fehler beim Laden")
|
|
|
|
|
fallback_item.setText(1, str(e))
|
|
|
|
|
return fallback_item
|
|
|
|
|
|
2025-08-03 16:31:38 +02:00
|
|
|
# Kontextmenü-Aktionen für TreeNode
|
|
|
|
|
def _add_tree_node_child(self, parent_item):
|
|
|
|
|
"""Fügt einen Unterknoten zu einem TreeNode hinzu."""
|
|
|
|
|
print(f"Unterknoten zu TreeNode hinzufügen: {parent_item.text(0)}")
|
|
|
|
|
# TODO: Dialog zum Eingeben der Node-Daten öffnen
|
|
|
|
|
|
|
|
|
|
def _add_xsl_file_to_node(self, parent_item):
|
|
|
|
|
"""Fügt eine XSL-Datei zu einem TreeNode hinzu."""
|
|
|
|
|
print(f"XSL-Datei zu TreeNode hinzufügen: {parent_item.text(0)}")
|
|
|
|
|
# TODO: Dialog zum Auswählen der XSL-Datei öffnen
|
|
|
|
|
|
|
|
|
|
def _edit_tree_node(self, item):
|
|
|
|
|
"""Bearbeitet einen TreeNode."""
|
|
|
|
|
print(f"TreeNode bearbeiten: {item.text(0)}")
|
|
|
|
|
# TODO: Dialog zum Bearbeiten der Node-Daten öffnen
|
|
|
|
|
|
|
|
|
|
def _delete_tree_node(self, item):
|
|
|
|
|
"""Löscht einen TreeNode."""
|
|
|
|
|
print(f"TreeNode löschen: {item.text(0)}")
|
|
|
|
|
# TODO: Bestätigungsdialog und Löschung implementieren
|
|
|
|
|
|
|
|
|
|
# Kontextmenü-Aktionen für XslFile
|
|
|
|
|
def _add_xml_file_to_xsl(self, parent_item):
|
|
|
|
|
"""Fügt eine XML-Datei zu einer XSL-Datei hinzu."""
|
|
|
|
|
print(f"XML-Datei zu XslFile hinzufügen: {parent_item.text(0)}")
|
|
|
|
|
# TODO: Dialog zum Auswählen der XML-Datei öffnen
|
|
|
|
|
|
|
|
|
|
def _edit_xsl_file(self, item):
|
|
|
|
|
"""Bearbeitet eine XSL-Datei."""
|
|
|
|
|
print(f"XslFile bearbeiten: {item.text(0)}")
|
|
|
|
|
# TODO: Dialog zum Bearbeiten der XSL-Datei öffnen
|
|
|
|
|
|
|
|
|
|
def _delete_xsl_file(self, item):
|
|
|
|
|
"""Löscht eine XSL-Datei."""
|
|
|
|
|
print(f"XslFile löschen: {item.text(0)}")
|
|
|
|
|
# TODO: Bestätigungsdialog und Löschung implementieren
|
|
|
|
|
|
|
|
|
|
# Kontextmenü-Aktionen für XmlFile
|
|
|
|
|
def _edit_xml_file(self, item):
|
|
|
|
|
"""Bearbeitet eine XML-Datei."""
|
|
|
|
|
print(f"XmlFile bearbeiten: {item.text(0)}")
|
|
|
|
|
# TODO: Dialog zum Bearbeiten der XML-Datei öffnen
|
|
|
|
|
|
|
|
|
|
def _delete_xml_file(self, item):
|
|
|
|
|
"""Löscht eine XML-Datei."""
|
|
|
|
|
print(f"XmlFile löschen: {item.text(0)}")
|
|
|
|
|
# TODO: Bestätigungsdialog und Löschung implementieren
|
|
|
|
|
|
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."""
|
|
|
|
|
print("Neuen TreeNode als Root-Element hinzufügen")
|
|
|
|
|
# TODO: Dialog zum Eingeben der TreeNode-Daten öffnen
|
|
|
|
|
|
2025-08-10 14:03:15 +02:00
|
|
|
def on_load_from_fn2_clicked(self):
|
|
|
|
|
"""
|
|
|
|
|
Wird ausgeführt, wenn der Button "lade aus FN2" geklickt wird.
|
|
|
|
|
Führt SQL-Abfrage aus und aktualisiert die Projekt-Nodes.
|
|
|
|
|
"""
|
|
|
|
|
print("Button 'lade aus FN2' wurde geklickt!")
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
# Prüfe ob ein Projekt geladen ist
|
|
|
|
|
if not hasattr(self, 'project') or not self.project:
|
|
|
|
|
QMessageBox.warning(self, "Warnung", "Kein Projekt geladen. Bitte öffnen Sie zuerst ein Projekt.")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Hole das aktuelle Projekt aus app_settings
|
|
|
|
|
if not self.project:
|
|
|
|
|
QMessageBox.warning(self, "Warnung", "Aktuelles Projekt nicht in den Einstellungen gefunden.")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Hole die PostgreSQL-Datenbank-Konfiguration
|
|
|
|
|
db_config = self._get_database_config(self.project.postgre_sql_db_id)
|
|
|
|
|
if not db_config:
|
|
|
|
|
QMessageBox.warning(self, "Warnung", "PostgreSQL-Datenbank-Konfiguration nicht gefunden.")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Führe SQL-Abfrage aus
|
|
|
|
|
df = self._execute_sql_query(db_config)
|
|
|
|
|
if df is None:
|
|
|
|
|
return # Fehler bereits angezeigt
|
|
|
|
|
|
|
|
|
|
# Verarbeite die Daten wie in readCsv.py
|
|
|
|
|
new_nodes = self._process_sql_data(df)
|
|
|
|
|
|
|
|
|
|
# Merge mit vorhandenen Nodes
|
|
|
|
|
self._merge_nodes_with_existing(new_nodes)
|
|
|
|
|
|
|
|
|
|
# Speichere die aktualisierten Projekt-Einstellungen
|
|
|
|
|
self._save_project_settings()
|
|
|
|
|
|
|
|
|
|
# Lade das Projekt neu
|
|
|
|
|
self._load_nodes_to_tree()
|
|
|
|
|
|
|
|
|
|
QMessageBox.information(self, "Erfolg", "Daten erfolgreich aus FN2 geladen und Projekt aktualisiert!")
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"Fehler beim Laden aus FN2: {e}")
|
|
|
|
|
QMessageBox.critical(self, "Fehler", f"Fehler beim Laden aus FN2:\n{str(e)}")
|
|
|
|
|
|
|
|
|
|
# def _get_current_project(self):
|
|
|
|
|
# """
|
|
|
|
|
# Ermittelt das aktuell geladene Projekt aus app_settings.
|
|
|
|
|
|
|
|
|
|
# Returns:
|
|
|
|
|
# PdfProject|None: Das aktuelle Projekt oder None
|
|
|
|
|
# """
|
|
|
|
|
# # Da wir kein direktes Attribut für das aktuelle Projekt haben,
|
|
|
|
|
# # nehmen wir das erste Projekt als Fallback oder implementieren eine bessere Logik
|
|
|
|
|
# if app_settings.pdf_projects:
|
|
|
|
|
# # TODO: Hier sollte eine bessere Logik implementiert werden,
|
|
|
|
|
# # um das tatsächlich aktuelle Projekt zu ermitteln
|
|
|
|
|
# return app_settings.pdf_projects[0]
|
|
|
|
|
# return None
|
|
|
|
|
|
|
|
|
|
def _get_database_config(self, db_id):
|
|
|
|
|
"""
|
|
|
|
|
Holt die Datenbank-Konfiguration anhand der ID.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
db_id: ID der PostgreSQL-Datenbank
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
PostgreSqlDb|None: Die Datenbank-Konfiguration oder None
|
|
|
|
|
"""
|
|
|
|
|
for db in app_settings.postgresql_dbs:
|
|
|
|
|
if db.id == db_id:
|
|
|
|
|
return db
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def _execute_sql_query(self, db_config):
|
|
|
|
|
"""
|
|
|
|
|
Führt die SQL-Abfrage aus der data.sql Datei aus.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
db_config: PostgreSQL-Datenbank-Konfiguration
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
pl.DataFrame|None: Die Abfrageergebnisse oder None bei Fehler
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
# Lade SQL-Abfrage aus Datei
|
|
|
|
|
sql_file_path = Path("src/res/data.sql")
|
|
|
|
|
if not sql_file_path.exists():
|
|
|
|
|
QMessageBox.critical(self, "Fehler", f"SQL-Datei nicht gefunden: {sql_file_path}")
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
with open(sql_file_path, 'r', encoding='utf-8') as f:
|
|
|
|
|
sql_query = f.read()
|
|
|
|
|
|
|
|
|
|
print(f"SQL-Abfrage geladen: {len(sql_query)} Zeichen")
|
|
|
|
|
|
|
|
|
|
# Verbindung zur PostgreSQL-Datenbank herstellen
|
|
|
|
|
connection_string = ("postgresql://"
|
|
|
|
|
f"{db_config.username}:"
|
|
|
|
|
f"{db_config.password}@"
|
|
|
|
|
f"{db_config.host}:"
|
|
|
|
|
f"{db_config.port}/"
|
|
|
|
|
f"{db_config.database}?"
|
|
|
|
|
f"sslmode={db_config.ssl_mode.value}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
print(f"Verbinde zu PostgreSQL: {db_config.host}:{db_config.port}/{db_config.database}")
|
|
|
|
|
|
|
|
|
|
df = pl.read_database_uri(sql_query, connection_string, engine='connectorx').sort(["reporttyp_bez", "report_bez", "repfile_bez"])
|
|
|
|
|
return df
|
|
|
|
|
except Exception as e:
|
|
|
|
|
error_msg = f"Fehler beim Ausführen der SQL-Abfrage: {str(e)}"
|
|
|
|
|
print(error_msg)
|
|
|
|
|
QMessageBox.critical(self, "Fehler", error_msg)
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def _process_sql_data(self, df):
|
|
|
|
|
"""
|
|
|
|
|
Verarbeitet die SQL-Daten wie in readCsv.py und erstellt Node-Struktur.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
df: Polars DataFrame mit den SQL-Ergebnissen
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
list[TreeNode]: Liste der erstellten Root-Nodes
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
start_time = time.time()
|
|
|
|
|
|
|
|
|
|
# Gruppiere die Daten wie in readCsv.py
|
|
|
|
|
ebene_1 = df.group_by(["reporttyp", "reporttyp_bez"]).len()
|
|
|
|
|
ebene_2 = df.group_by(["reporttyp", "report", "report_bez"]).len()
|
|
|
|
|
ebene_3 = df.group_by(["reporttyp", "report", "repfile", "repfile_bez", "xsl_datei"]).len()
|
|
|
|
|
|
|
|
|
|
group_time = time.time() - start_time
|
|
|
|
|
print(f"Performance: Gruppierung in {group_time:.3f}s")
|
|
|
|
|
|
|
|
|
|
new_nodes = []
|
|
|
|
|
|
|
|
|
|
start_time = time.time()
|
|
|
|
|
|
|
|
|
|
# Erstelle Node-Struktur wie in readCsv.py
|
|
|
|
|
for r1 in ebene_1.rows(named=True):
|
|
|
|
|
tn_1 = TreeNode(id=(r1["reporttyp"],), bez=r1["reporttyp_bez"], children=[])
|
|
|
|
|
r1_children = ebene_2.filter(pl.col("reporttyp") == r1["reporttyp"])
|
|
|
|
|
|
|
|
|
|
for r2 in r1_children.rows(named=True):
|
|
|
|
|
tn_2 = TreeNode(id=(r2["reporttyp"], r2["report"]), bez=r2["report_bez"], children=[])
|
|
|
|
|
r2_children = ebene_3.filter((pl.col("reporttyp") == r1["reporttyp"]) & (pl.col("report") == r2["report"]))
|
|
|
|
|
|
|
|
|
|
for r3 in r2_children.rows(named=True):
|
|
|
|
|
x = XslFile(
|
|
|
|
|
id=(r3["reporttyp"], r3["report"], r3["repfile"]),
|
|
|
|
|
bez=r3["repfile_bez"],
|
|
|
|
|
xsl_file=Path(r3["xsl_datei"]),
|
|
|
|
|
xmls=[]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
tn_2.children.append(x)
|
|
|
|
|
tn_1.children.append(tn_2)
|
|
|
|
|
new_nodes.append(tn_1)
|
|
|
|
|
|
|
|
|
|
nodes_time = time.time() - start_time
|
|
|
|
|
print(f"Performance: Node-Erstellung in {nodes_time:.3f}s")
|
|
|
|
|
print(f"Erstellt: {len(new_nodes)} Root-Nodes")
|
|
|
|
|
|
|
|
|
|
return new_nodes
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"Fehler beim Verarbeiten der SQL-Daten: {e}")
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
def _merge_nodes_with_existing(self, new_nodes):
|
|
|
|
|
"""
|
|
|
|
|
Merged neue Nodes mit vorhandenen Nodes basierend auf IDs.
|
|
|
|
|
Überschreibt nur einzelne Eigenschaften, nicht ganze Nodes.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
new_nodes: Liste der neuen Nodes
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
print("Merge neue Nodes mit vorhandenen...")
|
|
|
|
|
|
|
|
|
|
# Erstelle ein Dictionary der neuen Nodes für schnellen Zugriff
|
|
|
|
|
new_nodes_dict = {}
|
|
|
|
|
self._build_nodes_dict(new_nodes, new_nodes_dict)
|
|
|
|
|
|
|
|
|
|
print(f"Neue Nodes Dictionary erstellt: {len(new_nodes_dict)} Einträge")
|
|
|
|
|
|
|
|
|
|
# Merge mit vorhandenen Nodes
|
|
|
|
|
if self.pdf_project.nodes:
|
|
|
|
|
self._merge_nodes_recursive(self.pdf_project.nodes, new_nodes_dict)
|
|
|
|
|
|
|
|
|
|
# Füge komplett neue Root-Nodes hinzu
|
|
|
|
|
existing_root_ids = {node.id for node in self.pdf_project.nodes}
|
|
|
|
|
for new_node in new_nodes:
|
|
|
|
|
if new_node.id not in existing_root_ids:
|
|
|
|
|
self.pdf_project.nodes.append(new_node)
|
|
|
|
|
print(f"Neue Root-Node hinzugefügt: {new_node.bez}")
|
|
|
|
|
|
|
|
|
|
print("Merge abgeschlossen")
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"Fehler beim Mergen der Nodes: {e}")
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
def _build_nodes_dict(self, nodes, nodes_dict):
|
|
|
|
|
"""
|
|
|
|
|
Erstellt rekursiv ein Dictionary aller Nodes für schnellen ID-basierten Zugriff.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
nodes: Liste der Nodes
|
|
|
|
|
nodes_dict: Dictionary zum Füllen
|
|
|
|
|
"""
|
|
|
|
|
for node in nodes:
|
|
|
|
|
nodes_dict[node.id] = node
|
|
|
|
|
|
|
|
|
|
if isinstance(node, TreeNode) and node.children:
|
|
|
|
|
self._build_nodes_dict(node.children, nodes_dict)
|
|
|
|
|
|
|
|
|
|
def _merge_nodes_recursive(self, existing_nodes, new_nodes_dict):
|
|
|
|
|
"""
|
|
|
|
|
Merged rekursiv vorhandene Nodes mit neuen Nodes.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
existing_nodes: Liste der vorhandenen Nodes
|
|
|
|
|
new_nodes_dict: Dictionary der neuen Nodes
|
|
|
|
|
"""
|
|
|
|
|
for existing_node in existing_nodes:
|
|
|
|
|
if existing_node.id in new_nodes_dict:
|
|
|
|
|
new_node = new_nodes_dict[existing_node.id]
|
|
|
|
|
|
|
|
|
|
# Aktualisiere nur die Bezeichnung, falls sie sich geändert hat
|
|
|
|
|
if existing_node.bez != new_node.bez:
|
|
|
|
|
print(f"Aktualisiere Bezeichnung für Node {existing_node.id}: '{existing_node.bez}' -> '{new_node.bez}'")
|
|
|
|
|
existing_node.bez = new_node.bez
|
|
|
|
|
|
|
|
|
|
# Für XslFile: Aktualisiere xsl_file Pfad
|
|
|
|
|
if isinstance(existing_node, XslFile) and isinstance(new_node, XslFile):
|
|
|
|
|
if existing_node.xsl_file != new_node.xsl_file:
|
|
|
|
|
print(f"Aktualisiere XSL-Datei für Node {existing_node.id}: '{existing_node.xsl_file}' -> '{new_node.xsl_file}'")
|
|
|
|
|
existing_node.xsl_file = new_node.xsl_file
|
|
|
|
|
|
|
|
|
|
# Rekursiv für Kinder (nur bei TreeNode)
|
|
|
|
|
if isinstance(existing_node, TreeNode) and existing_node.children:
|
|
|
|
|
self._merge_nodes_recursive(existing_node.children, new_nodes_dict)
|
|
|
|
|
|
|
|
|
|
# Füge neue Kinder hinzu, die noch nicht existieren
|
|
|
|
|
if existing_node.id in new_nodes_dict:
|
|
|
|
|
new_node = new_nodes_dict[existing_node.id]
|
|
|
|
|
if isinstance(new_node, TreeNode) and new_node.children:
|
|
|
|
|
existing_child_ids = {child.id for child in existing_node.children}
|
|
|
|
|
for new_child in new_node.children:
|
|
|
|
|
if new_child.id not in existing_child_ids:
|
|
|
|
|
existing_node.children.append(new_child)
|
|
|
|
|
print(f"Neues Kind hinzugefügt zu Node {existing_node.id}: {new_child.bez}")
|
|
|
|
|
|
|
|
|
|
def _save_project_settings(self):
|
|
|
|
|
"""
|
|
|
|
|
Speichert die aktualisierten Projekt-Einstellungen.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
current_project: Das aktuelle Projekt
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
start_time = time.time()
|
|
|
|
|
|
|
|
|
|
# Speichere in project.yaml im Projekt-Verzeichnis
|
|
|
|
|
self.pdf_project.writeSettings(project_dir=self.project.project_dir)
|
|
|
|
|
|
|
|
|
|
dump_time = time.time() - start_time
|
|
|
|
|
print(f"Performance: Projekt-Einstellungen gespeichert in {dump_time:.3f}s")
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"Fehler beim Speichern der Projekt-Einstellungen: {e}")
|
|
|
|
|
raise
|
|
|
|
|
|
2025-05-29 16:30:01 +02:00
|
|
|
def closeEvent(self, event):
|
|
|
|
|
"""Wird beim Schließen der Anwendung aufgerufen."""
|
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)
|