4f2d136d17
Beim Auswählen eines XML-Knotens im Baum wird jetzt die Ref-PDF direkt im internen Viewer geladen, sofern keine Diff-PDF existiert. Der Kontextmenü-Eintrag "Ref-PDF öffnen" und der zugehörige Handler wurden entfernt. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
971 lines
39 KiB
Python
971 lines
39 KiB
Python
import logging
|
|
import shutil
|
|
|
|
from PySide6.QtCore import Qt, QEvent
|
|
from PySide6.QtGui import QAction
|
|
from PySide6.QtWidgets import (
|
|
QMainWindow,
|
|
QApplication,
|
|
QStyleFactory,
|
|
QTreeWidgetItem,
|
|
QMessageBox,
|
|
QMenu,
|
|
)
|
|
|
|
from ui.MainWinddow_ui import Ui_MainWindow
|
|
from ui.AppSettings import AppSettingsDlg
|
|
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
|
|
from pathlib import Path
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class MainWindow(
|
|
QMainWindow,
|
|
TreeManagerMixin,
|
|
PdfViewerMixin,
|
|
WorkerPoolMixin,
|
|
DatabaseMixin,
|
|
DragDropMixin,
|
|
HashCalculationMixin,
|
|
TransformationMixin,
|
|
):
|
|
def __init__(self, parent=None):
|
|
"""
|
|
Konstruktor für die MainWindow-Klasse.
|
|
Verwendet PySide6.QtPdf für optimale Performance.
|
|
|
|
Args:
|
|
parent: Übergeordnetes Widget, falls vorhanden
|
|
"""
|
|
super().__init__(parent)
|
|
|
|
# UI einrichten
|
|
self.ui = Ui_MainWindow()
|
|
self.ui.setupUi(self)
|
|
|
|
# Dict zum Speichern der Beziehung zwischen Thumbnails und Seitennummern
|
|
self.thumbnail_to_page = {}
|
|
|
|
# PDF-Dokumente für späteres On-Demand-Rendering speichern
|
|
self.pdf_documents = {} # {pdf_filename: {'diff': QPdfDocument, 'ref': QPdfDocument, 'new': QPdfDocument}}
|
|
|
|
# Aktueller Zoom-Faktor
|
|
self.current_zoom = 100 # 100%
|
|
|
|
# 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
|
|
|
|
# Cache für die aktuell gerenderten Pixmaps (Performance-Optimierung)
|
|
self.current_rendered_pixmaps = None
|
|
|
|
# Label für die Vollansicht (nur ein einziges Label)
|
|
self.fullsize_label = None
|
|
|
|
# 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
|
|
|
|
# Das aktuelle Projekt (Project) aus app_settings
|
|
self.project = None
|
|
|
|
# Das aktuelle ProjectData
|
|
self.pdf_project = None
|
|
|
|
# Hash-Berechnungs-Thread
|
|
self.hash_calculator_thread = None
|
|
|
|
# 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 = {}
|
|
|
|
# Theme-Menü initialisieren
|
|
self._setup_theme_menu()
|
|
|
|
# Vorhandene Projekte-Menü initialisieren
|
|
self._setup_projects_menu()
|
|
|
|
#
|
|
if theme := app_settings.theme:
|
|
self.change_theme(theme)
|
|
else:
|
|
self.change_theme("Fusion")
|
|
|
|
# Signale und Slots verbinden
|
|
self._connect_signals()
|
|
|
|
# Kontextmenü für TreeWidget einrichten
|
|
self._setup_tree_context_menu()
|
|
|
|
# Drag&Drop für TreeWidget aktivieren
|
|
self._setup_drag_drop()
|
|
|
|
# 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)
|
|
|
|
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()
|
|
|
|
logger.debug(f"Verfügbare Themes: {available_themes}")
|
|
logger.debug(f"Aktuelles Theme: {current_theme}")
|
|
|
|
# Füge Theme-Aktionen zum Menü hinzu
|
|
for theme_name in available_themes:
|
|
action = QAction(theme_name, self)
|
|
action.setCheckable(True)
|
|
|
|
# Markiere das aktuelle Theme
|
|
if theme_name.lower() == current_theme.lower():
|
|
action.setChecked(True)
|
|
|
|
# Verbinde die Aktion mit der Theme-Wechsel-Funktion
|
|
action.triggered.connect(lambda checked, theme=theme_name: self.change_theme(theme))
|
|
|
|
# Füge die Aktion zum Theme-Menü hinzu
|
|
self.ui.menuThema.addAction(action)
|
|
|
|
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()
|
|
|
|
# Prüfe ob Projekte vorhanden sind
|
|
if not app_settings.pdf_projects:
|
|
# Keine Projekte vorhanden - Menü deaktivieren
|
|
self.ui.actionVorhandene_Projekte.setEnabled(False)
|
|
self.ui.actionVorhandene_Projekte.setText("Vorhandene Projekte (keine vorhanden)")
|
|
logger.info("Projekte-Menü deaktiviert - keine Projekte vorhanden")
|
|
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
|
|
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 gültige Projekt als Menü-Eintrag hinzu
|
|
for project in valid_projects:
|
|
project_action = QAction(project.name, self)
|
|
project_action.setCheckable(True)
|
|
project_action.setToolTip(f"Projekt-Ordner: {project.project_dir}")
|
|
|
|
# Markiere das aktuell geladene Projekt
|
|
if hasattr(self, "project") and self.project and self.project.id == project.id:
|
|
project_action.setChecked(True)
|
|
|
|
# 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)
|
|
|
|
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")
|
|
|
|
def open_existing_project(self, project: Project):
|
|
"""
|
|
Öffnet ein vorhandenes Projekt.
|
|
|
|
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}")
|
|
|
|
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
|
|
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")
|
|
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")
|
|
|
|
# Aktualisiere das Projekte-Menü um das Häkchen beim geladenen Projekt anzuzeigen
|
|
self._setup_projects_menu()
|
|
|
|
# Aktualisiere das Projektpfad-Label
|
|
self.ui.projectPath.setText(f"Projekt: {project.project_dir}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Laden des Projekts '{project.name}': {e}")
|
|
# Fallback: Erstelle Standard-Einstellungen
|
|
try:
|
|
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
|
|
|
|
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 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)
|
|
|
|
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()
|
|
|
|
# PDF-Dokumente schließen ist bei QtPdf automatisch durch Garbage Collection
|
|
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()
|