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()