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

1028 lines
41 KiB
Python
Raw Normal View History

import logging
import shutil
2025-05-23 11:09:47 +02:00
from PySide6.QtCore import Qt, QUrl, QEvent
from PySide6.QtGui import QAction, QDesktopServices
from PySide6.QtWidgets import (
QMainWindow,
QApplication,
QStyleFactory,
QTreeWidgetItem,
QMessageBox,
QMenu,
)
2025-05-23 11:09:47 +02:00
2025-05-21 20:26:03 +02:00
from ui.MainWinddow_ui import Ui_MainWindow
from ui.AppSettings import AppSettingsDlg
2025-06-16 20:30:56 +02:00
from ui.PdfProject import PdfProjectDlg
from ui.mixins import (
TreeManagerMixin,
PdfViewerMixin,
WorkerPoolMixin,
DatabaseMixin,
DragDropMixin,
HashCalculationMixin,
TransformationMixin,
)
from conf import app_settings, Project, ProjectData, TreeNode, XslFile
2025-06-16 20:30:56 +02:00
from pathlib import Path
2025-05-20 11:24:07 +02:00
logger = logging.getLogger(__name__)
class MainWindow(
QMainWindow,
TreeManagerMixin,
PdfViewerMixin,
WorkerPoolMixin,
DatabaseMixin,
DragDropMixin,
HashCalculationMixin,
TransformationMixin,
):
2025-05-20 11:24:07 +02:00
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-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
# Aktuelle Diff-PDF-Informationen (für Accept Changes)
self.current_diff_xml_path = None
self.current_diff_xsl_id = None
# 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-08-10 17:32:22 +02:00
# Das aktuelle Projekt (Project) aus app_settings
self.project = None
2025-08-10 17:32:22 +02:00
# Das aktuelle ProjectData
self.pdf_project = None
# Hash-Berechnungs-Thread
self.hash_calculator_thread = None
# Transformations-Thread
self.transformation_thread = None
# 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
# Gespeicherte Worker-Pool-Metriken (werden vor Shutdown gecacht)
self.last_saxon_metrics = None
self.last_fop_metrics = None
# 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-06-22 11:58:57 +02:00
# Vorhandene Projekte-Menü initialisieren
self._setup_projects_menu()
#
if theme := app_settings.theme:
self.change_theme(theme)
else:
self.change_theme("Fusion")
2025-05-27 20:48:21 +02:00
2025-05-20 11:24:07 +02:00
# Signale und Slots verbinden
self._connect_signals()
2025-08-03 16:31:38 +02:00
# Kontextmenü für TreeWidget einrichten
self._setup_tree_context_menu()
2025-08-14 20:32:29 +02:00
# TreeWidget Styling für größeren vertikalen Abstand
self._setup_tree_widget_styling()
# Drag&Drop für TreeWidget aktivieren
self._setup_drag_drop()
2025-05-23 11:09:47 +02:00
# Zoom per Mausrad (STRG+Mausrad) für PDF-Viewer aktivieren
self._setup_scroll_area_zoom()
# 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
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."""
# Entferne das alte Untermenü, falls vorhanden
old_menu = self.ui.actionVorhandene_Projekte.menu()
if old_menu:
old_menu.clear()
old_menu.deleteLater()
2025-06-22 11:58:57 +02:00
# Prüfe ob Projekte vorhanden sind
if not app_settings.pdf_projects:
# Keine Projekte vorhanden - Menü deaktivieren
2025-06-22 11:58:57 +02:00
self.ui.actionVorhandene_Projekte.setEnabled(False)
self.ui.actionVorhandene_Projekte.setText("Vorhandene Projekte (keine vorhanden)")
logger.info("Projekte-Menü deaktiviert - keine Projekte vorhanden")
2025-06-22 11:58:57 +02:00
return
# Filtere nur existierende Projekte
valid_projects = []
invalid_projects = []
for project in app_settings.pdf_projects:
if project.project_dir.exists():
valid_projects.append(project)
else:
invalid_projects.append(project)
logger.warning(f"Projekt-Verzeichnis existiert nicht: {project.name}{project.project_dir}")
# Wenn keine gültigen Projekte vorhanden sind
if not valid_projects:
self.ui.actionVorhandene_Projekte.setEnabled(False)
self.ui.actionVorhandene_Projekte.setText("Vorhandene Projekte (keine gültigen vorhanden)")
logger.warning(f"Projekte-Menü deaktiviert - {len(invalid_projects)} ungültige(s) Projekt(e) gefunden")
return
# Gültige Projekte vorhanden - Menü aktivieren und Untermenü erstellen
2025-06-22 11:58:57 +02:00
self.ui.actionVorhandene_Projekte.setEnabled(True)
self.ui.actionVorhandene_Projekte.setText("Vorhandene Projekte")
2025-06-22 11:58:57 +02:00
# Erstelle ein Untermenü für die Projekte
projects_menu = QMenu(self)
# Füge jedes gültige Projekt als Menü-Eintrag hinzu
for project in valid_projects:
2025-06-22 11:58:57 +02:00
project_action = QAction(project.name, self)
project_action.setToolTip(f"Projekt-Ordner: {project.project_dir}")
2025-06-22 11:58:57 +02:00
# Verbinde die Aktion mit der Projekt-Öffnen-Funktion
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-06-22 11:58:57 +02:00
# Setze das Untermenü für die Aktion
self.ui.actionVorhandene_Projekte.setMenu(projects_menu)
logger.info(f"Projekte-Menü initialisiert mit {len(valid_projects)} gültigen Projekt(en)")
if invalid_projects:
logger.warning(f"{len(invalid_projects)} ungültige(s) Projekt(e) ausgeblendet")
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-06-22 11:58:57 +02:00
Args:
project: Das zu öffnende PdfProject-Objekt
"""
logger.info(f"Öffne Projekt: {project.name}")
logger.debug(f"Projekt-Ordner: {project.project_dir}")
# Speichere vorheriges Projekt inkl. Expand-Status (falls vorhanden)
if hasattr(self, "project") and self.project and hasattr(self, "pdf_project") and self.pdf_project:
try:
logger.info(f"Speichere vorheriges Projekt: {self.project.name}")
self._save_project_settings()
logger.info("Vorheriges Projekt erfolgreich gespeichert")
except Exception as e:
logger.error(f"Fehler beim Speichern des vorherigen Projekts: {e}")
2025-08-10 14:03:15 +02:00
self.project = project
try:
# Prüfe ob project.yaml existiert und nicht leer ist
project_yaml_path = Path(project.project_dir) / "project.yaml"
if project_yaml_path.exists() and project_yaml_path.stat().st_size > 0:
# Versuche die Projekt-Einstellungen zu laden
2025-08-10 17:32:22 +02:00
self.pdf_project = ProjectData.readSettings(project_dir=project.project_dir)
logger.info(f"Projekt-Einstellungen aus {project_yaml_path} geladen!")
else:
# Erstelle Standard-Projekt-Einstellungen wenn Datei leer oder nicht vorhanden
logger.warning("project.yaml ist leer oder nicht vorhanden, erstelle Standard-Einstellungen")
2025-08-10 17:32:22 +02:00
self.pdf_project = ProjectData()
# Speichere die Standard-Einstellungen in die project.yaml
self.pdf_project.writeSettings(project_dir=project.project_dir)
logger.info(f"Standard-Projekt-Einstellungen in {project_yaml_path} gespeichert")
# Lade die Nodes in das TreeWidget (inkl. Diff-PDF-Counts und Icons)
self._load_nodes_to_tree()
# Starte Hash-Berechnung für alle XML-Dateien
self._start_xml_hash_calculation()
# Worker-Pools werden jetzt erst beim Start der Transformation initialisiert (lazy loading)
# Aktiviere das Aktions-Menü, da ein Projekt geladen wurde
self.ui.menuAktion.setEnabled(True)
logger.info("Aktions-Menü aktiviert nach Projekt-Laden")
except Exception as e:
logger.error(f"Fehler beim Laden des Projekts '{project.name}': {e}")
# Fallback: Erstelle Standard-Einstellungen
try:
2025-08-10 17:32:22 +02:00
self.pdf_project = ProjectData()
logger.info("Fallback: Standard-Projekt-Einstellungen erstellt")
# Auch bei Fallback die Nodes laden
self._load_nodes_to_tree()
except Exception as fallback_error:
logger.error(f"Fehler beim Erstellen der Fallback-Einstellungen: {fallback_error}")
def change_theme(self, theme_name):
"""
Wechselt das Theme der Anwendung.
Args:
theme_name: Name des zu verwendenden Themes
"""
logger.info(f"Wechsle zu Theme: {theme_name}")
try:
# Erstelle den neuen Style
style = QStyleFactory.create(theme_name)
if style:
# Wende den neuen Style auf die Anwendung an
QApplication.setStyle(style)
# Aktualisiere die Checkmarks im Menü
for action in self.ui.menuThema.actions():
action.setChecked(action.text() == theme_name)
logger.info(f"Theme erfolgreich gewechselt zu: {theme_name}")
app_settings.theme = theme_name
app_settings.save()
else:
logger.error(f"Fehler: Theme '{theme_name}' konnte nicht erstellt werden")
except Exception as e:
logger.error(f"Fehler beim Wechseln des Themes: {e}")
def _connect_signals(self):
"""Verbindet Signale mit den entsprechenden Slots."""
# Zoom-Slider verbinden
self.ui.zoom.valueChanged.connect(self.apply_zoom)
self.ui.zoom.mouseDoubleClickEvent = lambda event: self.ui.zoom.setValue(100)
# Alpha-Slider verbinden
self.ui.alpha.valueChanged.connect(self.on_alpha_changed)
self.ui.alpha.mouseDoubleClickEvent = lambda event: self.ui.alpha.setValue(0)
# Menü-Aktionen verbinden
self.ui.actionNeu.triggered.connect(self.open_new_project_dialog)
self.ui.actionEinstellungen.triggered.connect(self.open_settings_dialog)
self.ui.actionAlle_XML_Dateien_transformieren.triggered.connect(self._transform_all_xml_files)
self.ui.actionAlle_XML_Dateien_neu_transformieren_force.triggered.connect(self._transform_all_xml_files_force)
# Worker-Pool-Metriken Menüeintrag (programmatisch hinzufügen)
from PySide6.QtGui import QAction
self.action_worker_metrics = QAction("Worker-Pool-Metriken", self)
self.action_worker_metrics.triggered.connect(self._show_worker_pool_metrics)
# Füge die Aktion zum Projekt-Menü hinzu (nach Einstellungen, vor Beenden)
actions = self.ui.menuProjekt.actions()
# Finde die Position nach actionEinstellungen
insert_index = len(actions) # Fallback: Am Ende
for i, action in enumerate(actions):
if action == self.ui.actionEinstellungen:
insert_index = i + 1
break
before_action = actions[insert_index] if insert_index < len(actions) else None
if before_action:
self.ui.menuProjekt.insertAction(before_action, self.action_worker_metrics)
else:
self.ui.menuProjekt.addAction(self.action_worker_metrics)
# Menü-Aktion "Aus Datenbank laden" verbinden (macht das Gleiche wie Button)
self.ui.actionAus_Datenbank_laden.triggered.connect(self.on_load_from_fn2_clicked)
# Button "Accept Changes" verbinden
self.ui.accept_changes.clicked.connect(self._on_accept_changes_clicked)
# 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)
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
logger.info("Einstellungen wurden gespeichert")
except Exception as e:
logger.error(f"Fehler beim Öffnen des Einstellungen-Dialogs: {e}")
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 = Project(
id=new_id,
name=project_data["name"],
project_dir=Path(project_data["project_dir"]),
java_vm_id=project_data["java_vm_id"] if project_data["java_vm_id"] != -1 else 1,
diff_pdf_id=project_data["diff_pdf_id"] if project_data["diff_pdf_id"] != -1 else 1,
saxon_jar_id=project_data["saxon_jar_id"] if project_data["saxon_jar_id"] != -1 else 1,
apache_fop_id=project_data["apache_fop_id"] if project_data["apache_fop_id"] != -1 else 1,
xsl_dir_id=project_data["xsl_dir_id"] if project_data["xsl_dir_id"] != -1 else 1,
postgre_sql_db_id=project_data["postgre_sql_db_id"]
if project_data["postgre_sql_db_id"] != -1
else 1,
fop_config_dir=Path(project_data["fop_config_dir"]) if project_data.get("fop_config_dir") else None,
)
# 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()
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']}")
# Aktualisiere das Projekte-Menü
self._setup_projects_menu()
except Exception as e:
logger.error(f"Fehler beim Erstellen des neuen Projekts: {e}")
def _create_project_structure(self, project: Project):
"""
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)
logger.debug(f"Ordner erstellt: {subdir_path}")
project_yaml_path = project_dir / "project.yaml"
# Erstelle Standard-Projekt-Einstellungen und speichere sie
if not project_yaml_path.exists():
# Erstelle Standard-PdfProjectSettings
default_settings = ProjectData()
# Speichere die Standard-Einstellungen in die project.yaml
default_settings.writeSettings(project_dir=project_dir)
logger.info(f"project.yaml mit Standard-Einstellungen erstellt: {project_yaml_path}")
else:
logger.debug(f"project.yaml existiert bereits: {project_yaml_path}")
except Exception as e:
logger.error(f"Fehler beim Erstellen der Projekt-Struktur: {e}")
raise
def _update_diff_icons_for_existing_pdfs(self):
"""
Durchläuft alle XML-Items und setzt Icons für bereits existierende Diff-PDFs.
Wird nach dem Laden eines Projekts aufgerufen.
"""
if not hasattr(self, "project") or not self.project:
logger.debug("Kein Projekt geladen, überspringe Diff-Icon-Update")
return
diff_dir = self.project.project_dir / "diff"
if not diff_dir.exists():
logger.debug(f"Diff-Ordner existiert nicht: {diff_dir}")
return
logger.info("Aktualisiere Diff-Icons für existierende PDFs...")
logger.info(f"XML-Item-Map hat {len(self.xml_item_map)} Einträge")
# Durchlaufe alle XML-Items in der Map
icon_count = 0
for map_key, tree_item in self.xml_item_map.items():
# Map-Key hat Format "xml_path|xsl_id"
parts = map_key.split("|")
if len(parts) != 2:
continue
xml_path_str, xsl_id_str = parts
xml_path = Path(xml_path_str)
xml_stem = xml_path.stem
# Diff-PDF-Dateiname: "{xml_stem}_xsl_{xsl_id_str}.pdf"
expected_pdf = diff_dir / f"{xml_stem}_xsl_{xsl_id_str}.pdf"
if expected_pdf.exists():
# Icon setzen
icon_widget = self._create_centered_diff_icon(xml_path, xsl_id_str)
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")
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}")
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
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
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
"""
pdf_basename = diff_pdf_path.name # Initialisiere am Anfang für Exception-Handler
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!
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
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
logger.warning(
f"{successful_count}/{len(diff_pdfs)} Änderungen übernommen, {failed_count} fehlgeschlagen"
)
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)}")
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
# 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
QApplication.processEvents()
logger.info("PDF-Dokumente geschlossen und UI geleert vor Dateioperationen")
# 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)}")
def _open_ref_pdf_for_xml_file(self, item):
"""
Handler für Kontextmenü-Aktion "Ref-PDF öffnen" bei XML-Dateien.
Öffnet die Referenz-PDF für die ausgewählte XML-Datei im systemseitig installierten PDF-Viewer.
Args:
item: Das TreeWidgetItem der XML-Datei
"""
try:
if not self.project:
QMessageBox.warning(self, "Fehler", "Kein Projekt geöffnet")
return
# Hole XML-Datei-Objekt
xml_file_obj = item.data(0, Qt.ItemDataRole.UserRole)
if not xml_file_obj:
QMessageBox.warning(self, "Fehler", "XML-Datei-Objekt nicht gefunden")
return
# Hole Parent-Item (XslFile)
parent_item = item.parent()
if not parent_item:
QMessageBox.warning(self, "Fehler", "Parent-Item nicht gefunden")
return
# Hole XslFile-Objekt
xsl_file_obj = parent_item.data(0, Qt.ItemDataRole.UserRole)
if not xsl_file_obj:
QMessageBox.warning(self, "Fehler", "XSL-Datei-Objekt nicht gefunden")
return
# Erstelle XSL-ID-String
xsl_id_str = "_".join(map(str, xsl_file_obj.id))
# Ermittle PDF-Dateinamen
xml_file_path = xml_file_obj.xml
xml_stem = xml_file_path.stem
pdf_basename = f"{xml_stem}_xsl_{xsl_id_str}.pdf"
# Pfad zur Ref-PDF
ref_dir = self.project.project_dir / "ref"
ref_pdf_path = ref_dir / pdf_basename
# Prüfe ob Ref-PDF existiert
if not ref_pdf_path.exists():
QMessageBox.warning(self, "Fehler", f"Referenz-PDF nicht gefunden:\n{pdf_basename}")
logger.warning(f"Referenz-PDF nicht gefunden: {ref_pdf_path}")
return
# Öffne Ref-PDF im System-Viewer
logger.info(f"Öffne Referenz-PDF im System-Viewer: {ref_pdf_path}")
url = QUrl.fromLocalFile(str(ref_pdf_path))
if not QDesktopServices.openUrl(url):
QMessageBox.critical(self, "Fehler", f"Konnte Referenz-PDF nicht öffnen:\n{ref_pdf_path}")
logger.error(f"Fehler beim Öffnen der Referenz-PDF: {ref_pdf_path}")
except Exception as e:
logger.error(f"Fehler beim Öffnen der Referenz-PDF: {e}")
QMessageBox.critical(self, "Fehler", f"Fehler beim Öffnen der Referenz-PDF:\n{str(e)}")
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."""
# UI-Zustände speichern
self._save_ui_state()
# Speichere Projekt-Einstellungen inkl. Expand-Status (falls Projekt geladen)
if hasattr(self, "project") and self.project and hasattr(self, "pdf_project") and self.pdf_project:
try:
self._save_project_settings()
logger.info("Projekt-Einstellungen beim Beenden gespeichert")
except Exception as e:
logger.error(f"Fehler beim Speichern der Projekt-Einstellungen: {e}")
# Stoppe Hash-Berechnungs-Thread falls noch aktiv
if (
hasattr(self, "hash_calculator_thread")
and self.hash_calculator_thread
and self.hash_calculator_thread.isRunning()
):
self.hash_calculator_thread.quit()
self.hash_calculator_thread.wait()
# Stoppe Transformations-Thread falls noch aktiv
if (
hasattr(self, "transformation_thread")
and self.transformation_thread
and self.transformation_thread.isRunning()
):
self.transformation_thread.quit()
self.transformation_thread.wait()
# Beende Saxon-Worker-Pool
self._shutdown_saxon_worker_pool()
# 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)
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()