2 Commits

Author SHA1 Message Date
info 104698ab0f Performance-Optimierung bei Duplikat-Dateinamengenerierung
Sammelt alle verwendeten XML-Dateinamen einmalig in ein Set für schnelleren Lookup beim Generieren alternativer Dateinamen. Ersetzt wiederholte Funktionsaufrufe durch effizienten Set-Lookup.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-07 20:04:37 +01:00
info 2e69c5b3d1 XML-Dateien Duplikat-Erkennung 2025-09-20 18:05:11 +02:00
28 changed files with 1653 additions and 5309 deletions
+14
View File
@@ -0,0 +1,14 @@
# agents.md
Entwicklung mit Python und PySide6
## Richtlinien
### Allgemeines zum Projekt
- In diesem Projekt wird der uv Packetmanager verwendet.
## PySide6-GUI
- Beim Erstellen neuer Dialoge sollte immer eine passende UI-Datei erstellt werden.
- Der Entwickler sollte später in der Lage sein, den neuen Dialog über die UI-Datei zu gestalten.
- Aus der UI-Datei wird in Visual Studio Code über eine Erweiterung automatisch eine .py-Datei erzeugt.
- Die automatisch generierte .py-Datei muss in den Code eingebunden und verwendet werden.
-1
View File
@@ -1 +0,0 @@
CLAUDE.md
+14
View File
@@ -0,0 +1,14 @@
# agents.md
Entwicklung mit Python und PySide6
## Richtlinien
### Allgemeines zum Projekt
- In diesem Projekt wird der uv Packetmanager verwendet.
## PySide6-GUI
- Beim Erstellen neuer Dialoge sollte immer eine passende UI-Datei erstellt werden.
- Der Entwickler sollte später in der Lage sein, den neuen Dialog über die UI-Datei zu gestalten.
- Aus der UI-Datei wird in Visual Studio Code über eine Erweiterung automatisch eine .py-Datei erzeugt.
- Die automatisch generierte .py-Datei muss in den Code eingebunden und verwendet werden.
-169
View File
@@ -1,169 +0,0 @@
# CLAUDE.md
Spreche mit mir auf Deutsch! (Communicate with me in German!)
## Projektübersicht
DocuMentor (ehemals xsl-validator) ist eine PySide6-basierte Desktop-Anwendung zur Verwaltung und Validierung von XSL-Transformationen mit XML-Dateien. Sie bietet eine GUI zur Konfiguration von Transformations-Toolchains (Saxon, Apache FOP, diff-pdf) und zur Verwaltung von PDF-Generierungsprojekten mit PostgreSQL-Datenbankintegration.
## PySide6-GUI
- Beim Erstellen neuer Dialoge sollte immer eine passende UI-Datei erstellt werden
- Der Entwickler sollte später in der Lage sein, den neuen Dialog über die UI-Datei zu gestalten
- Aus der UI-Datei wird in Visual Studio Code über eine Erweiterung automatisch eine .py-Datei erzeugt
- Die automatisch generierte .py-Datei muss in den Code eingebunden und verwendet werden
## Entwicklungskommandos
### Paketverwaltung
Dieses Projekt verwendet den `uv` Paketmanager (nicht pip oder poetry):
```bash
uv sync # Abhängigkeiten installieren
uv run python src/main.py # Anwendung starten
uv run python test_hash_implementation.py # Hash-Tests ausführen
```
### Linting
```bash
uv run ruff check # Code-Style prüfen (Zeilenlänge: 120)
uv run ruff format # Code formatieren
```
## Architektur
### Konfigurationssystem (src/conf.py)
Die Anwendung verwendet ein zentralisiertes Konfigurationsmodell mit Pydantic:
- **AppSettings**: Globales Singleton (`app_settings`), das die gesamte Anwendungskonfiguration speichert
- Wird an plattformspezifischen Orten gespeichert:
- Linux: `~/.config/DocuMentor/config.json`
- Windows: `%APPDATA%\DocuMentor\config.json`
- macOS: `~/Library/Application Support/DocuMentor/config.json`
- Enthält Listen von Tools: `java_vms`, `saxon_jars`, `apache_fops`, `diff_pdfs`, `xsl_dirs`, `postgresql_dbs`
- **ProjectData**: Projektspezifische Einstellungen, die in `project.yaml` im jeweiligen Projektverzeichnis gespeichert werden
- Enthält hierarchische Baumstruktur von Transformationsknoten
- Verwendet `TreeNode` und `XslFile` zur Organisation
- Jede `XmlFile` hat eine optionale `hashsum` (blake2b) zur Änderungsverfolgung
### Wichtige Datenmodelle
1. **Tool-Konfigurationsmodelle** (JavaVm, SaxonJar, ApacheFop, DiffPdf, XslDir, PostgreSqlDb):
- Jedes hat eine `id` und `version`
- Speichert Pfade zu Binärdateien/Verzeichnissen
2. **Project-Modell**:
- Referenziert Tool-Konfigurationen über ID
- Verlinkt zu einem Projektverzeichnis mit `project.yaml`
- Hat Hilfsmethoden wie `getXsl()`, `getJavaVm()` um IDs in Namen/Versionen aufzulösen
3. **Baumstruktur** (TreeNode → XslFile → XmlFile):
- Hierarchische Organisation von Transformations-Workflows
- `TreeNode`: Organisationseinheit mit `xslt_params` und Kindknoten/-dateien
- `XslFile`: XSL-Stylesheet mit zugehörigen XML-Dateien und XSLT-Parametern
- `XmlFile`: XML-Eingabedatei mit optionalem blake2b-Hash
### UI-Architektur (src/ui/)
Die Anwendung folgt einem spezifischen PySide6-Muster:
1. **UI-Definitionsdateien** (`*_ui.py`): Automatisch generiert aus UI-Designer-Dateien
- Diese Dateien definieren die UI-Struktur als Klassen (z.B. `Ui_MainWindow`)
- Sollten NICHT manuell bearbeitet werden
2. **Implementierungsdateien** (ohne `_ui` Suffix): Tatsächliche Dialog-/Fenster-Implementierungen
- Importieren und verwenden die entsprechende `*_ui.py` Datei
- Enthalten Business-Logik und Signal/Slot-Verbindungen
- Beispiel: `MainWindow.py` verwendet `Ui_MainWindow` aus `MainWinddow_ui.py`
Beim Erstellen neuer Dialoge:
- Immer zuerst eine entsprechende UI-Datei erstellen
- Die UI-Datei wird automatisch als `.py`-Datei von einer VS Code Extension generiert
- Die generierte UI-Klasse in der Implementierungsdatei importieren und verwenden
### Hauptfenster (src/ui/MainWindow.py)
Zentrale Schaltstelle der Anwendung mit mehreren wichtigen Verantwortlichkeiten:
1. **Projektverwaltung**:
- Öffnet und verwaltet PDF-Transformationsprojekte
- Lädt/speichert `ProjectData` aus `project.yaml` Dateien
2. **Tree Widget**: Zeigt hierarchische Struktur von Transformationsknoten an
- Kontextmenüs zum Hinzufügen/Bearbeiten/Löschen von Knoten, XSL-Dateien und XML-Dateien
- Drag-and-Drop-Unterstützung für XML-Dateien
3. **PDF-Vergleichsansicht**:
- Drei-Panel-Ansicht (Referenz, Diff, Neu)
- Alpha-Blending für visuellen Vergleich
- Zoom- und Pan-Funktionalität
4. **Asynchrone Operationen**:
- `XmlHashCalculatorThread`: Hintergrund-blake2b-Hash-Berechnung für XML-Dateien
- `DatabaseTestThread` (in PostgreSqlConfigDialog): Asynchrones Testen von Datenbankverbindungen
### Hash-Berechnungssystem
Die Anwendung verwendet blake2b-Hashing zur Verfolgung von XML-Dateiänderungen:
- **Automatisch**: Hashes werden berechnet, wenn Projekte geladen werden (nur für Dateien ohne existierenden Hash)
- **Asynchron**: Hintergrund-Thread (`XmlHashCalculatorThread`) um die UI reaktionsfähig zu halten
- **Format**: `blake2b:<64-Zeichen-Hexdigest>`
- **Speicherung**: Persistiert in `project.yaml` innerhalb jedes `XmlFile`-Objekts
- **Details**: Siehe `docs/blake2b_hash_implementation.md`
### Theme-System
Die Anwendung unterstützt mehrere Qt-Themes:
- Theme-Auswahlmenü wird dynamisch aus `QStyleFactory.keys()` befüllt
- Theme-Präferenz wird in `AppSettings.theme` gespeichert
- Dark-Theme-Unterstützung via `qdarktheme` Paket (aktuell in main.py auskommentiert)
### Datenbankintegration
PostgreSQL-Integration mit Polars und ConnectorX:
- Konfiguration wird im `PostgreSqlDb`-Modell mit SSL-Modus-Unterstützung gespeichert
- SQL-Abfragen werden via `_execute_sql_query()` im MainWindow ausgeführt
- Ergebnisse werden in Polars DataFrames geladen
## Wichtige Konventionen
### Deutsche Sprache
Die Codebasis verwendet Deutsch für:
- UI-Texte und Labels
- Kommentare und Dokumentation
- Variablennamen wo kontextuell passend
- Log-Meldungen
### Pfadbehandlung
- Immer `pathlib.Path`-Objekte verwenden, keine Strings
- `expanduser()` und `expandvars()` für Benutzer-/Umgebungspfade verwenden
- Projektrelative Pfade werden als relativ gespeichert, zur Laufzeit gegen `project_dir` aufgelöst
### ID-basierte Lookups
Konfigurationsentitäten (Tools, Datenbanken) werden in Projekten über ID referenziert. Die Hilfsmethoden des `Project`-Modells (`getXsl()`, `getJavaVm()`, etc.) verwenden, um IDs in Anzeigewerte aufzulösen.
### Einstellungspersistenz
- Globale Einstellungen: `app_settings.save()` nach Änderungen aufrufen
- Projekteinstellungen: `project_data.writeSettings(project_dir)` nach Änderungen aufrufen
## Arbeiten mit der Codebasis
### Neue Tool-Konfigurationen hinzufügen
1. Modell zu `conf.py` hinzufügen (ähnlich wie `JavaVm`, `SaxonJar`)
2. Listenfeld zu `AppSettings` hinzufügen
3. Konfigurationsdialog in `src/ui/` erstellen (UI-Datei + Implementierung)
4. Zu `AppSettings.py` Tabs hinzufügen
5. `Project`-Modell aktualisieren, falls das Tool projektspezifisch sein soll
### Neue Baumoperationen hinzufügen
1. Aktion zum Kontextmenü in `_create_context_menu_for_type()` hinzufügen
2. Handler-Methode implementieren nach Namensschema `_action_tree_node()`, `_action_xsl_file()`, etc.
3. Baum nach Änderungen mit `_load_nodes_to_tree()` aktualisieren
4. `self.project_data.writeSettings(self.project.project_dir)` aufrufen um Änderungen zu persistieren
### Projektstruktur modifizieren
Das `ProjectData`-Modell ist die Quelle der Wahrheit. Alle Änderungen an der Baumstruktur müssen:
1. Die `project_data.nodes` Liste modifizieren
2. `project_data.writeSettings()` aufrufen um zu persistieren
3. Baum mit `_load_nodes_to_tree()` neu laden um Änderungen in der UI zu reflektieren
+8
View File
@@ -0,0 +1,8 @@
# Projekt allgemeines
- In diesem Projekt wirt uv Packetmanager verwendet.
## PySide6-GUI
- Beim Erstellen neuer Dialoge sollte stets eine passende UI-Datei erstellt werden.
- Der Entwickler soll den neuen Dialog später über die UI-Datei gestalten können.
- Die UI-Datei wird in Visual Studio Code durch eine Erweiterung automatisch als .py-Datei generiert.
- Die automatisch generierte .py-Datei muss in den Code eingebunden und genutzt werden.
+1 -6
View File
@@ -19,10 +19,5 @@ dependencies = [
# ...but use a different line length.
line-length = 120
# Ignoriere automatisch generierte UI-Dateien
extend-exclude = ["*_ui.py"]
[dependency-groups]
dev = [
"ruff>=0.14.8",
]
dev = []
+8 -21
View File
@@ -1,5 +1,4 @@
import os
import sys
from os import path
from pathlib import Path
from sys import platform
from typing import Tuple, Type
@@ -19,15 +18,15 @@ app_name = "DocuMentor"
if platform == "win32":
tmp_config_path = f"%APPDATA%\\{app_name}\\config.json"
config_path = f"%APPDATA%\\{app_name}\\config.json"
elif platform in ("linux", "linux2"):
tmp_config_path = f"~/.config/{app_name}/config.json"
config_path = f"~/.config/{app_name}/config.json"
elif platform == "darwin":
tmp_config_path = f"~/Library/Application Support/{app_name}/͏͏͏͏config.json"
config_path = f"~/Library/Application Support/{app_name}/͏͏͏͏config.json"
else:
tmp_config_path = f"~/.config/{app_name}/config.json"
config_path = f"~/.config/{app_name}/config.json"
config_path = Path(os.path.expandvars(tmp_config_path)).expanduser()
config_path = Path(path.expandvars(config_path))
class JavaVm(BaseModel):
@@ -72,7 +71,6 @@ class SSLMode(str, Enum):
VERIFY_CA = "verify-ca"
VERIFY_FULL = "verify-full"
class PostgreSqlDb(BaseModel):
id: int
name: str
@@ -94,7 +92,6 @@ class Project(BaseModel):
apache_fop_id: int = Field(..., description="ID der Apache FOP Konfiguration", gt=0)
xsl_dir_id: int = Field(..., description="ID des XSL-Verzeichnisses", gt=0)
postgre_sql_db_id: int = Field(..., description="ID der PostgreSQL Datenbank", gt=0)
fop_config_dir: Path | None = Field(None, description="Optionaler Pfad zum Apache FOP Config-Verzeichnis")
def getXsl(self) -> str:
global app_settings
@@ -142,12 +139,6 @@ class AppSettings(BaseSettings):
pdf_projects: list[Project] = []
postgresql_dbs: list[PostgreSqlDb] = []
theme: str | None = None
max_workers: int = 8 # Anzahl paralleler Worker für Transformationen (Standard: 8)
# UI-Zustand
window_geometry: tuple[int, int, int, int] | None = None # (x, y, width, height)
splitter_sizes: list[int] | None = None # Splitter-Positionen
tree_column_widths: list[int] | None = None # TreeWidget-Spaltenbreiten
model_config = SettingsConfigDict(json_file=config_path)
@@ -168,10 +159,6 @@ class AppSettings(BaseSettings):
if not config_path.parent.exists():
config_path.parent.mkdir(parents=True, exist_ok=True)
if not config_path.parent.is_dir() or not os.access(config_path.parent, os.W_OK):
logger.exception(f"{config_path.parent} ist kein Verzeichnis oder es gibt keine Schreibrechte")
sys.exit(1)
# Konfiguration speichern
with open(config_path, "wb") as c:
c.write(app_settings.model_dump_json(indent=4).encode())
@@ -212,8 +199,8 @@ class ProjectData(BaseModel):
def readSettings(cls, project_dir: Path):
# Explizit UTF-8 Encoding verwenden
project_yaml_path = project_dir / "project.yaml"
with open(project_yaml_path, "r", encoding="utf-8") as f:
yaml = YAML(typ="safe")
with open(project_yaml_path, 'r', encoding='utf-8') as f:
yaml = YAML(typ='safe')
yaml_data = yaml.load(f)
return cls.model_validate(yaml_data)
+5 -35
View File
@@ -1,5 +1,4 @@
import sys
import logging
from PySide6.QtWidgets import QApplication
@@ -7,46 +6,17 @@ from ui.MainWindow import MainWindow
from ui.AppSettings import AppSettingsDlg
from conf import app_settings
# import qdarktheme
def main():
"""Haupteinstiegspunkt der Anwendung."""
# Logging konfigurieren - sowohl Datei als auch Konsole
from datetime import datetime
# Log-Verzeichnis erstellen (im selben Verzeichnis wie config.json)
from conf import config_path
log_dir = config_path.parent / "logs"
log_dir.mkdir(exist_ok=True)
# Log-Dateiname mit Timestamp
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
log_file = log_dir / f"documentor_{timestamp}.log"
# Root-Logger konfigurieren
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
# Formatter für alle Handler
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s", datefmt="%H:%M:%S")
# Handler 1: Datei (alles ab DEBUG)
file_handler = logging.FileHandler(log_file, encoding="utf-8")
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
# Handler 2: Konsole (alles ab INFO)
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
logging.info(f"Logging initialisiert: {log_file}")
# QApplication-Instanz erstellen
app = QApplication(sys.argv)
# Dark Theme aktivieren
# qdarktheme.setup_theme("auto")
# Hauptfenster erstellen
window = MainWindow()
-442
View File
@@ -1,442 +0,0 @@
"""
Saxon Worker Pool - Persistente JVM-Prozesse für schnelle XSLT-Transformationen.
Eliminiert JVM-Startup-Overhead durch Vorinitialisierung von N Worker-Prozessen.
Jeder Worker läuft als Daemon und verarbeitet mehrere Transformationen nacheinander.
"""
import logging
import subprocess
import threading
from pathlib import Path
from queue import Queue
from typing import Optional
import tempfile
logger = logging.getLogger(__name__)
# Java-Worker-Code (wird zur Laufzeit kompiliert)
SAXON_WORKER_JAVA = """
import javax.xml.transform.*;
import javax.xml.transform.stream.*;
import java.io.*;
import java.util.*;
public class SaxonWorker {
public static void main(String[] args) {
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
String line;
// Create TransformerFactory once and reuse
TransformerFactory factory = TransformerFactory.newInstance();
System.err.println("SaxonWorker started and ready (using JAXP Transformer API)");
System.err.flush();
try {
while ((line = reader.readLine()) != null) {
System.err.println("DEBUG: Received line: " + line.substring(0, Math.min(100, line.length())));
System.err.flush();
if ("EXIT".equals(line.trim())) {
System.err.println("SaxonWorker exiting");
break;
}
try {
// Parse job
System.err.println("DEBUG: Parsing job...");
System.err.flush();
String[] parts = line.split("\\t");
System.err.println("DEBUG: Parts count: " + parts.length);
System.err.flush();
if (parts.length < 3) {
System.out.println("ERROR: Invalid job format");
System.out.flush();
continue;
}
String sourceXml = parts[0];
String xslStylesheet = parts[1];
String outputFo = parts[2];
System.err.println("DEBUG: Creating transformer from stylesheet...");
System.err.flush();
// Create Source and Result objects
StreamSource xslSource = new StreamSource(new File(xslStylesheet));
StreamSource xmlSource = new StreamSource(new File(sourceXml));
StreamResult result = new StreamResult(new File(outputFo));
System.err.println("DEBUG: Compiling stylesheet...");
System.err.flush();
// Create transformer from stylesheet
Transformer transformer = factory.newTransformer(xslSource);
// Set parameters if present
if (parts.length > 3 && !parts[3].isEmpty()) {
String[] params = parts[3].split("\\\\|\\\\|\\\\|");
for (String param : params) {
if (!param.isEmpty() && param.contains("=")) {
String[] kv = param.split("=", 2);
transformer.setParameter(kv[0], kv[1]);
System.err.println("DEBUG: Set parameter: " + kv[0] + " = " + kv[1]);
}
}
System.err.flush();
}
System.err.println("DEBUG: Running transformation...");
System.err.flush();
// Capture errors via ErrorListener
final StringBuilder errors = new StringBuilder();
transformer.setErrorListener(new ErrorListener() {
@Override
public void warning(TransformerException e) {
errors.append("WARNING: ").append(e.getMessage()).append("\\n");
}
@Override
public void error(TransformerException e) {
errors.append("ERROR: ").append(e.getMessage()).append("\\n");
}
@Override
public void fatalError(TransformerException e) throws TransformerException {
errors.append("FATAL: ").append(e.getMessage()).append("\\n");
throw e;
}
});
// Run transformation
transformer.transform(xmlSource, result);
System.err.println("DEBUG: Transformation completed");
System.err.flush();
// Check for errors
if (errors.length() > 0) {
System.out.println("ERROR: " + errors.toString().trim());
} else {
System.out.println("OK");
}
System.out.flush();
} catch (TransformerException e) {
System.err.println("DEBUG: Transformer exception: " + e.getClass().getName());
System.err.flush();
e.printStackTrace(System.err);
String errorMsg = e.getMessage();
if (errorMsg == null || errorMsg.isEmpty()) {
errorMsg = e.getClass().getSimpleName();
}
System.out.println("ERROR: " + errorMsg);
System.out.flush();
} catch (Exception e) {
System.err.println("DEBUG: Job processing exception: " + e.getClass().getName());
System.err.flush();
e.printStackTrace(System.err);
System.out.println("ERROR: " + (e.getMessage() != null ? e.getMessage() : e.getClass().getName()));
System.out.flush();
}
}
} catch (IOException e) {
System.err.println("SaxonWorker I/O error: " + e.getMessage());
e.printStackTrace(System.err);
}
}
}
"""
class SaxonWorkerPool:
"""
Pool von lang-laufenden JVM-Prozessen für Saxon-Transformationen.
Eliminiert JVM-Startup-Overhead durch Wiederverwendung von N Worker-Prozessen.
"""
def __init__(
self,
num_workers: int,
java_vm_path: Path,
saxon_jar_path: Path,
classpath_cache: dict[Path, str],
log_dir: Optional[Path] = None,
):
"""
Initialisiert den Saxon-Worker-Pool.
Args:
num_workers: Anzahl der Worker-Prozesse
java_vm_path: Pfad zur Java VM Binary
saxon_jar_path: Pfad zur Saxon JAR-Datei
classpath_cache: Cache für Saxon-Classpaths
log_dir: Optionales Verzeichnis für Worker-Logs (Standard: temp_dir/temp)
"""
self.num_workers = num_workers
self.java_vm_path = java_vm_path
self.saxon_jar_path = saxon_jar_path
self.classpath_cache = classpath_cache
self.log_dir = log_dir
# Worker-Prozesse und Queues
self.workers: list[subprocess.Popen] = []
self.job_queue: Queue = Queue()
self.result_queue: Queue = Queue()
self.worker_locks: list[threading.Lock] = []
# Temporäres Verzeichnis für kompilierte Java-Klasse
self.temp_dir: Optional[Path] = None
self.worker_class_path: Optional[Path] = None
self.worker_log_dir: Optional[Path] = None
# Initialisierung
self._compile_worker_class()
self._start_workers()
logger.info(f"SaxonWorkerPool initialisiert mit {num_workers} Workern")
def _compile_worker_class(self):
"""Kompiliert die SaxonWorker-Java-Klasse."""
try:
# Erstelle temporäres Verzeichnis
self.temp_dir = Path(tempfile.mkdtemp(prefix="saxon_worker_"))
# Schreibe Java-Quellcode
java_file = self.temp_dir / "SaxonWorker.java"
java_file.write_text(SAXON_WORKER_JAVA, encoding="utf-8")
# Hole Classpath
saxon_dir = self.saxon_jar_path.parent
if saxon_dir in self.classpath_cache:
classpath = self.classpath_cache[saxon_dir]
else:
# Fallback: Baue Classpath neu
import glob
import sys
all_jars = glob.glob(str(saxon_dir / "*.jar"))
lib_dir = saxon_dir / "lib"
if lib_dir.exists():
all_jars.extend(glob.glob(str(lib_dir / "*.jar")))
classpath_separator = ";" if sys.platform == "win32" else ":"
classpath = classpath_separator.join(all_jars)
# Kompiliere Java-Klasse
javac_cmd = [str(self.java_vm_path).replace("java", "javac"), "-cp", classpath, str(java_file)]
logger.debug(f"Kompiliere SaxonWorker: {' '.join(javac_cmd)}")
result = subprocess.run(javac_cmd, capture_output=True, text=True, timeout=30)
if result.returncode != 0:
raise RuntimeError(f"Java-Kompilierung fehlgeschlagen: {result.stderr}")
self.worker_class_path = self.temp_dir
logger.info(f"SaxonWorker erfolgreich kompiliert: {self.temp_dir}")
except Exception as e:
logger.error(f"Fehler beim Kompilieren von SaxonWorker: {e}")
raise
def _start_workers(self):
"""Startet N Worker-Prozesse."""
# Hole Classpath
saxon_dir = self.saxon_jar_path.parent
classpath = self.classpath_cache.get(saxon_dir, "")
# Füge Worker-Classpath hinzu
import sys
classpath_separator = ";" if sys.platform == "win32" else ":"
full_classpath = str(self.worker_class_path) + classpath_separator + classpath
# Bestimme Log-Verzeichnis
self.worker_log_dir = self.log_dir if self.log_dir else self.temp_dir
if self.log_dir:
self.worker_log_dir.mkdir(parents=True, exist_ok=True)
for i in range(self.num_workers):
try:
# Starte JVM-Prozess mit SaxonWorker
cmd = [str(self.java_vm_path), "-cp", full_classpath, "SaxonWorker"]
# Öffne stderr-Log-Datei für diesen Worker
stderr_log = self.worker_log_dir / f"worker_{i}_stderr.log"
stderr_file = open(stderr_log, "w", encoding="utf-8")
process = subprocess.Popen(
cmd,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=stderr_file, # Redirect stderr to file
text=True,
bufsize=1, # Line buffered
)
self.workers.append(process)
self.worker_locks.append(threading.Lock())
logger.debug(f"Worker {i} gestartet (PID: {process.pid}, stderr: {stderr_log})")
# Warte kurz damit Worker initialisieren kann
import time
time.sleep(0.1)
# Prüfe ob Worker noch läuft
if process.poll() is not None:
# Worker ist bereits beendet - Fehler!
stderr_file.close()
with open(stderr_log, "r") as f:
stderr_content = f.read()
raise RuntimeError(
f"Worker {i} ist sofort beendet (Exit Code: {process.returncode})\nstderr:\n{stderr_content}"
)
except Exception as e:
logger.error(f"Fehler beim Starten von Worker {i}: {e}")
raise
logger.info(f"{len(self.workers)} Saxon-Worker erfolgreich gestartet")
def transform(
self, source_xml: Path, xsl_stylesheet: Path, output_fo: Path, xslt_params: dict[str, str]
) -> tuple[bool, str]:
"""
Führt eine XSLT-Transformation mit einem Worker aus dem Pool aus.
Args:
source_xml: Pfad zur XML-Eingabedatei
xsl_stylesheet: Pfad zur XSL-Stylesheet-Datei
output_fo: Pfad zur FO-Ausgabedatei
xslt_params: Dictionary mit XSLT-Parametern
Returns:
tuple[bool, str]: (Erfolg, Fehlermeldung/Info)
"""
# Finde freien Worker
worker_idx = None
for i, lock in enumerate(self.worker_locks):
if lock.acquire(blocking=False):
worker_idx = i
break
if worker_idx is None:
# Kein freier Worker, warte auf ersten verfügbaren
for i, lock in enumerate(self.worker_locks):
lock.acquire()
worker_idx = i
break
try:
worker = self.workers[worker_idx]
# Prüfe ob Worker noch läuft
if worker.poll() is not None:
# Worker ist tot!
stderr_log = self.worker_log_dir / f"worker_{worker_idx}_stderr.log"
try:
with open(stderr_log, "r") as f:
stderr_content = f.read()
error_msg = (
f"Worker {worker_idx} ist beendet (Exit: {worker.returncode})\nstderr:\n{stderr_content}"
)
except Exception:
error_msg = f"Worker {worker_idx} ist beendet (Exit: {worker.returncode})"
logger.error(error_msg)
return False, error_msg
# Formatiere Parameter
params_str = "|||".join([f"{key}={value}" for key, value in xslt_params.items()])
# Erstelle Job-String (Tab-separated)
job = f"{source_xml}\t{xsl_stylesheet}\t{output_fo}\t{params_str}\n"
logger.debug(f"Sende Job an Worker {worker_idx}: {source_xml.name}")
# Sende Job an Worker
worker.stdin.write(job)
worker.stdin.flush()
# Warte auf Antwort
response = worker.stdout.readline().strip()
logger.debug(f"Worker {worker_idx} Antwort: '{response}'")
if response == "OK":
return True, "Erfolgreich"
elif response.startswith("ERROR:"):
error_msg = response[6:].strip()
return False, f"Saxon-Fehler: {error_msg}"
else:
# Leere Antwort bedeutet Worker ist crashed
if not response:
stderr_log = self.worker_log_dir / f"worker_{worker_idx}_stderr.log"
try:
with open(stderr_log, "r") as f:
stderr_content = f.read()[-500:] # Letzte 500 Zeichen
return False, f"Worker {worker_idx} crashed (keine Antwort)\nstderr:\n{stderr_content}"
except Exception:
return False, f"Worker {worker_idx} crashed (keine Antwort)"
return False, f"Unerwartete Antwort: {response}"
except Exception as e:
logger.error(f"Fehler bei Worker {worker_idx}: {e}")
return False, f"Worker-Fehler: {str(e)}"
finally:
# Gebe Worker-Lock frei
self.worker_locks[worker_idx].release()
def shutdown(self):
"""Beendet alle Worker-Prozesse sauber."""
logger.info("Beende Saxon-Worker-Pool...")
for i, worker in enumerate(self.workers):
try:
# Sende EXIT-Befehl
if worker.stdin and not worker.stdin.closed:
worker.stdin.write("EXIT\n")
worker.stdin.flush()
# Warte auf Beendigung (max 2 Sekunden)
worker.wait(timeout=2)
logger.debug(f"Worker {i} beendet")
except subprocess.TimeoutExpired:
# Force kill falls nötig
worker.kill()
logger.warning(f"Worker {i} musste gekillt werden")
except Exception as e:
logger.error(f"Fehler beim Beenden von Worker {i}: {e}")
# Lösche temporäres Verzeichnis
if self.temp_dir and self.temp_dir.exists():
try:
import shutil
shutil.rmtree(self.temp_dir)
logger.debug(f"Temporäres Verzeichnis gelöscht: {self.temp_dir}")
except Exception as e:
logger.warning(f"Konnte temporäres Verzeichnis nicht löschen: {e}")
logger.info("Saxon-Worker-Pool beendet")
def __enter__(self):
"""Context manager entry."""
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Context manager exit."""
self.shutdown()
-519
View File
@@ -1,519 +0,0 @@
"""
Transformations-Engine für XSL-FO PDF-Generierung.
Dieses Modul implementiert die Transformations-Pipeline:
1. XML → FO (Saxon XSLT Transformation)
2. FO → PDF (Apache FOP)
3. PDF-Vergleich (diff-pdf)
"""
import logging
import subprocess
from pathlib import Path
from datetime import datetime
from typing import Any, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from saxon_pool import SaxonWorkerPool
logger = logging.getLogger(__name__)
# Globaler Saxon-Worker-Pool (wird von MainWindow initialisiert)
_saxon_worker_pool: Optional["SaxonWorkerPool"] = None
def set_saxon_worker_pool(pool: Optional["SaxonWorkerPool"]):
"""Setzt den globalen Saxon-Worker-Pool."""
global _saxon_worker_pool
_saxon_worker_pool = pool
if pool:
logger.info(f"Saxon-Worker-Pool aktiviert mit {pool.num_workers} Workern")
else:
logger.info("Saxon-Worker-Pool deaktiviert (Fallback auf subprocess)")
class TransformationJob:
"""
Repräsentiert einen einzelnen Transformations-Job.
Ähnlich zur TestFall-Klasse in validate-xls.py, aber für DocuMentor angepasst.
"""
# Klassenweiter Cache für Saxon-Classpaths (Performance-Optimierung)
_classpath_cache: dict[Path, str] = {}
def __init__(
self,
project_dir: Path,
xml_file: Path,
xsl_file: Path,
xslt_params: dict[str, str],
java_vm_path: Path,
saxon_jar_path: Path,
apache_fop_dir: Path,
diff_pdf_path: Path,
diff_pdf_params: list[str],
xsl_id: tuple | None = None,
fop_config_dir: Path | None = None,
):
"""
Initialisiert einen Transformations-Job.
Args:
project_dir: Pfad zum Projekt-Verzeichnis
xml_file: Relative Pfad zur XML-Eingabedatei (relativ zu project_dir)
xsl_file: Absolute Pfad zur XSL-Stylesheet-Datei
xslt_params: Dictionary mit XSLT-Parametern
java_vm_path: Pfad zur Java VM Binary
saxon_jar_path: Pfad zur Saxon JAR-Datei
apache_fop_dir: Pfad zum Apache FOP-Verzeichnis
diff_pdf_path: Pfad zur diff-pdf Binary
diff_pdf_params: Standard-Parameter für diff-pdf
xsl_id: ID der XSL-Datei (als Tuple)
fop_config_dir: Optionaler Pfad zum FOP-Config-Verzeichnis (überschreibt Standardpfad)
"""
self.project_dir = project_dir
self.xml_file = xml_file # Relativ
self.xsl_file = xsl_file # Absolut
self.xslt_params = xslt_params
self.xsl_id = xsl_id
# Tool-Pfade
self.java_vm_path = java_vm_path
self.saxon_jar_path = saxon_jar_path
self.apache_fop_dir = apache_fop_dir
self.fop_config_dir = fop_config_dir
self.diff_pdf_path = diff_pdf_path
self.diff_pdf_params = diff_pdf_params
# Ausgabe-Verzeichnisse im Projektordner
self.new_dir = project_dir / "new"
self.ref_dir = project_dir / "ref"
self.diff_dir = project_dir / "diff"
# Stelle sicher, dass Ausgabe-Verzeichnisse existieren
self.new_dir.mkdir(exist_ok=True)
self.ref_dir.mkdir(exist_ok=True)
self.diff_dir.mkdir(exist_ok=True)
# Dateinamen basierend auf XML-Datei + XSL-ID
base_name = self.xml_file.stem
# Füge XSL-ID zum Dateinamen hinzu, falls vorhanden
if xsl_id:
# Konvertiere Tuple (1, 2, 3) zu String "1_2_3"
xsl_id_str = "_".join(str(x) for x in xsl_id)
file_name_base = f"{base_name}_xsl_{xsl_id_str}"
else:
file_name_base = base_name
self.temp_fo = self.new_dir / f"{file_name_base}.fo"
self.new_pdf = self.new_dir / f"{file_name_base}.pdf"
self.ref_pdf = self.ref_dir / f"{file_name_base}.pdf"
self.diff_pdf = self.diff_dir / f"{file_name_base}.pdf"
# Apache FOP Binaries (plattformabhängig)
import sys
if sys.platform == "win32":
self.fop_cmd = self.apache_fop_dir / "fop.cmd"
else:
self.fop_cmd = self.apache_fop_dir / "fop"
# FOP-Konfigurationsdatei: Verwende fop_config_dir falls angegeben, sonst Standardpfad
if self.fop_config_dir:
self.fop_conf = self.fop_config_dir / "fop.xconf"
else:
self.fop_conf = self.apache_fop_dir / "conf" / "fop.xconf"
def is_up_to_date(self) -> bool:
"""
Prüft, ob die Transformation aktuell ist.
Returns:
bool: True wenn New-PDF existiert und aktueller ist als alle Inputs
"""
if not self.new_pdf.exists():
logger.debug(f"New-PDF existiert nicht: {self.new_pdf}")
return False
output_mtime = self.new_pdf.stat().st_mtime
# Prüfe XML-Datei
xml_abs = self.project_dir / self.xml_file
if xml_abs.exists() and xml_abs.stat().st_mtime > output_mtime:
logger.debug(f"XML-Datei ist neuer: {xml_abs}")
return False
# Prüfe XSL-Datei
if self.xsl_file.exists() and self.xsl_file.stat().st_mtime > output_mtime:
logger.debug(f"XSL-Datei ist neuer: {self.xsl_file}")
return False
logger.debug(f"Transformation ist aktuell: {self.new_pdf}")
return True
def transform_saxon(self, force: bool = False) -> tuple[bool, str]:
"""
Führt XSLT-Transformation mit Saxon aus: XML → FO.
Args:
force: Wenn True, wird Transformation auch bei aktuellem Output durchgeführt
Returns:
tuple[bool, str]: (Erfolg, Fehlermeldung/Info)
"""
if not force and self.is_up_to_date():
logger.info(f"Transformation übersprungen (aktuell): {self.xml_file.name}")
return True, "Übersprungen (aktuell)"
xml_abs = self.project_dir / self.xml_file
# Prüfe ob Eingabedateien existieren
if not xml_abs.exists():
error_msg = f"XML-Datei nicht gefunden: {xml_abs}"
logger.error(error_msg)
return False, error_msg
if not self.xsl_file.exists():
error_msg = f"XSL-Datei nicht gefunden: {self.xsl_file}"
logger.error(error_msg)
return False, error_msg
logger.info(f"Starte Saxon-Transformation: {self.xml_file.name}")
# Versuche zuerst den Worker-Pool zu nutzen (schneller!)
global _saxon_worker_pool
if _saxon_worker_pool:
try:
success, message = _saxon_worker_pool.transform(
source_xml=xml_abs,
xsl_stylesheet=self.xsl_file,
output_fo=self.temp_fo,
xslt_params=self.xslt_params,
)
if success:
logger.info(f"Saxon-Transformation erfolgreich (Worker-Pool): {self.xml_file.name}")
else:
logger.error(f"Saxon-Transformation fehlgeschlagen (Worker-Pool): {message}")
return success, message
except Exception as e:
logger.warning(f"Worker-Pool-Fehler, Fallback auf subprocess: {e}")
# Fallback auf subprocess unten
# Fallback: Traditionelle subprocess-Methode (langsamer, aber robuster)
# XSLT-Parameter formatieren
params = [f"{key}={value}" for key, value in self.xslt_params.items()]
# Hole Classpath aus Cache oder erstelle ihn
saxon_dir = self.saxon_jar_path.parent
if saxon_dir not in TransformationJob._classpath_cache:
# Sammle alle JAR-Dateien im Saxon-Verzeichnis für den Classpath
import glob
all_jars = glob.glob(str(saxon_dir / "*.jar"))
# Sammle auch alle JARs aus dem lib-Unterordner (z.B. xmlresolver)
lib_dir = saxon_dir / "lib"
if lib_dir.exists() and lib_dir.is_dir():
lib_jars = glob.glob(str(lib_dir / "*.jar"))
all_jars.extend(lib_jars)
logger.debug(f"Zusätzliche JARs aus lib-Verzeichnis gefunden: {len(lib_jars)}")
# Verwende alle JARs im Classpath (getrennt durch : auf Linux/Mac, ; auf Windows)
import sys
classpath_separator = ";" if sys.platform == "win32" else ":"
classpath = classpath_separator.join(all_jars)
# Cache den Classpath für zukünftige Jobs
TransformationJob._classpath_cache[saxon_dir] = classpath
logger.debug(f"Classpath für {saxon_dir} gecacht")
else:
classpath = TransformationJob._classpath_cache[saxon_dir]
logger.debug("Classpath aus Cache verwendet")
# Saxon-Kommandozeile
cmd_line = [
str(self.java_vm_path),
"-cp",
classpath,
"net.sf.saxon.Transform",
f"-s:{xml_abs}",
f"-xsl:{self.xsl_file}",
f"-o:{self.temp_fo}",
*params,
]
logger.debug(f"Kommandozeile (subprocess fallback): {' '.join(cmd_line)}")
try:
result = subprocess.run(
cmd_line,
capture_output=True,
text=True,
timeout=120, # 2 Minuten Timeout
)
# Saxon Ausgaben loggen
if result.stdout:
logger.debug(f"Saxon StdOut:\n{result.stdout}")
if result.stderr:
logger.debug(f"Saxon StdErr:\n{result.stderr}")
if result.returncode == 0:
logger.info(f"Saxon-Transformation erfolgreich (subprocess): {self.xml_file.name}")
return True, "Erfolgreich"
else:
error_msg = (
f"Saxon-Fehler (Exit {result.returncode}):\nStdOut: {result.stdout}\nStdErr: {result.stderr}"
)
logger.error(error_msg)
return False, error_msg
except subprocess.TimeoutExpired:
error_msg = "Saxon-Transformation Timeout (>120s)"
logger.error(error_msg)
return False, error_msg
except Exception as e:
error_msg = f"Unerwarteter Fehler bei Saxon-Transformation: {str(e)}"
logger.error(error_msg)
return False, error_msg
def build_pdf(self, force: bool = False) -> tuple[bool, str]:
"""
Generiert PDF aus FO-Datei mit Apache FOP: FO → PDF.
Args:
force: Wenn True, wird Build auch bei aktuellem Output durchgeführt
Returns:
tuple[bool, str]: (Erfolg, Fehlermeldung/Info)
"""
if not force and self.is_up_to_date():
logger.info(f"PDF-Build übersprungen (aktuell): {self.xml_file.name}")
return True, "Übersprungen (aktuell)"
# Prüfe ob FO-Datei existiert
if not self.temp_fo.exists():
error_msg = f"FO-Datei nicht gefunden: {self.temp_fo}"
logger.error(error_msg)
return False, error_msg
# Apache FOP Kommandozeile
cmd_line = [
str(self.fop_cmd),
"-c",
str(self.fop_conf) if self.fop_conf.exists() else "",
"-r",
"-fo",
str(self.temp_fo),
"-pdf",
str(self.new_pdf),
]
# Entferne leere Config-Parameter wenn fop.xconf nicht existiert
if not self.fop_conf.exists():
cmd_line = [c for c in cmd_line if c not in ["-c", ""]]
logger.info(f"Starte Apache FOP PDF-Generierung: {self.xml_file.name}")
logger.debug(f"Kommandozeile: {' '.join(cmd_line)}")
try:
result = subprocess.run(
cmd_line,
capture_output=True,
text=True,
timeout=180, # 3 Minuten Timeout
)
# Apache FOP Ausgaben loggen
if result.stdout:
logger.debug(f"FOP StdOut:\n{result.stdout}")
if result.stderr:
logger.debug(f"FOP StdErr:\n{result.stderr}")
# Temporäre FO-Datei löschen
if self.temp_fo.exists():
try:
self.temp_fo.unlink()
logger.debug(f"Temporäre FO-Datei gelöscht: {self.temp_fo}")
except Exception as e:
logger.warning(f"Konnte FO-Datei nicht löschen: {e}")
if result.returncode == 0:
# Wenn kein Ref-PDF existiert, erstelle es
if not self.ref_pdf.exists():
try:
import shutil
shutil.copy2(self.new_pdf, self.ref_pdf)
logger.info(f"Ref-PDF erstellt: {self.ref_pdf}")
except Exception as e:
logger.warning(f"Konnte Ref-PDF nicht erstellen: {e}")
logger.info(f"PDF-Generierung erfolgreich: {self.new_pdf}")
return True, "Erfolgreich"
else:
error_msg = f"FOP-Fehler (Exit {result.returncode}):\nStdOut: {result.stdout}\nStdErr: {result.stderr}"
logger.error(error_msg)
return False, error_msg
except subprocess.TimeoutExpired:
error_msg = "FOP PDF-Generierung Timeout (>180s)"
logger.error(error_msg)
return False, error_msg
except Exception as e:
error_msg = f"Unerwarteter Fehler bei PDF-Generierung: {str(e)}"
logger.error(error_msg)
return False, error_msg
def compare_pdf(self) -> tuple[bool, str]:
"""
Vergleicht New-PDF mit Ref-PDF und erstellt ggf. Diff-PDF.
Returns:
tuple[bool, str]: (PDFs sind identisch, Fehlermeldung/Info)
"""
# Prüfe ob beide PDFs existieren
if not self.ref_pdf.exists():
info_msg = "Kein Ref-PDF vorhanden (wird beim nächsten Build erstellt)"
logger.info(info_msg)
return True, info_msg
if not self.new_pdf.exists():
error_msg = f"New-PDF nicht gefunden: {self.new_pdf}"
logger.error(error_msg)
return False, error_msg
logger.info(f"Vergleiche PDFs: {self.xml_file.name}")
# Erster Vergleich (ohne Diff-Generierung)
cmd_compare = [
str(self.diff_pdf_path),
*self.diff_pdf_params,
str(self.ref_pdf),
str(self.new_pdf),
]
logger.debug(f"Kommandozeile Vergleich: {' '.join(cmd_compare)}")
try:
result = subprocess.run(
cmd_compare,
capture_output=True,
text=True,
timeout=60, # 1 Minute Timeout
)
if result.returncode == 0:
# PDFs sind identisch
logger.info(f"PDFs sind identisch: {self.xml_file.name}")
# Lösche altes Diff-PDF falls vorhanden
if self.diff_pdf.exists():
try:
self.diff_pdf.unlink()
logger.debug(f"Diff-PDF gelöscht (nicht mehr nötig): {self.diff_pdf}")
except Exception as e:
logger.warning(f"Konnte Diff-PDF nicht löschen: {e}")
return True, "PDFs sind identisch"
else:
# PDFs unterscheiden sich - erstelle Diff-PDF
logger.info(f"PDFs unterscheiden sich, erstelle Diff-PDF: {self.xml_file.name}")
cmd_diff = [
str(self.diff_pdf_path),
f"--output-diff={self.diff_pdf}",
*self.diff_pdf_params,
"--mark-differences",
str(self.ref_pdf),
str(self.new_pdf),
]
logger.debug(f"Kommandozeile Diff: {' '.join(cmd_diff)}")
result_diff = subprocess.run(
cmd_diff,
capture_output=True,
text=True,
timeout=90, # 1.5 Minuten Timeout
)
if result_diff.returncode == 0 or self.diff_pdf.exists():
logger.info(f"Diff-PDF erstellt: {self.diff_pdf}")
return False, f"Unterschiede gefunden - Diff-PDF: {self.diff_pdf.name}"
else:
error_msg = f"Diff-PDF-Erstellung fehlgeschlagen: {result_diff.stderr}"
logger.error(error_msg)
return False, error_msg
except subprocess.TimeoutExpired:
error_msg = "PDF-Vergleich Timeout"
logger.error(error_msg)
return False, error_msg
except Exception as e:
error_msg = f"Unerwarteter Fehler beim PDF-Vergleich: {str(e)}"
logger.error(error_msg)
return False, error_msg
def run_full_pipeline(self, force: bool = False) -> dict[str, Any]:
"""
Führt die komplette Transformations-Pipeline aus:
1. Saxon-Transformation (XML → FO)
2. PDF-Generierung (FO → PDF)
3. PDF-Vergleich
Args:
force: Wenn True, werden alle Schritte ausgeführt (ignoriert Up-to-Date)
Returns:
dict: Ergebnis-Dictionary mit Status und Meldungen
"""
start_time = datetime.now()
result = {
"success": False,
"xml_file": str(self.xml_file),
"xsl_id": self.xsl_id,
"steps": {},
"duration": None,
"new_pdf": str(self.new_pdf) if self.new_pdf.exists() else None,
"diff_pdf": str(self.diff_pdf) if self.diff_pdf.exists() else None,
}
logger.info(f"Starte Transformations-Pipeline: {self.xml_file.name}")
# Schritt 1: Saxon-Transformation
success_saxon, msg_saxon = self.transform_saxon(force=force)
result["steps"]["saxon"] = {"success": success_saxon, "message": msg_saxon}
if not success_saxon:
result["success"] = False
result["duration"] = (datetime.now() - start_time).total_seconds()
return result
# Schritt 2: PDF-Generierung
success_build, msg_build = self.build_pdf(force=force)
result["steps"]["build"] = {"success": success_build, "message": msg_build}
if not success_build:
result["success"] = False
result["duration"] = (datetime.now() - start_time).total_seconds()
return result
# Schritt 3: PDF-Vergleich
pdfs_identical, msg_compare = self.compare_pdf()
result["steps"]["compare"] = {"identical": pdfs_identical, "message": msg_compare}
result["pdfs_identical"] = pdfs_identical
# Pipeline erfolgreich abgeschlossen
result["success"] = True
result["duration"] = (datetime.now() - start_time).total_seconds()
logger.info(f"Pipeline abgeschlossen: {self.xml_file.name} ({result['duration']:.2f}s)")
return result
+1 -10
View File
@@ -466,7 +466,6 @@ class AppSettingsDlg(QDialog):
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,
)
self.temp_pdf_projects.append(new_project)
@@ -618,9 +617,7 @@ class AppSettingsDlg(QDialog):
'diff_pdf_id': pdf_project.diff_pdf_id,
'saxon_jar_id': pdf_project.saxon_jar_id,
'apache_fop_id': pdf_project.apache_fop_id,
'xsl_dir_id': pdf_project.xsl_dir_id,
'postgre_sql_db_id': pdf_project.postgre_sql_db_id,
'fop_config_dir': str(pdf_project.fop_config_dir) if pdf_project.fop_config_dir else None
'xsl_dir_id': pdf_project.xsl_dir_id
}
# Dialog im Edit-Modus öffnen (Projekt-Name und -Ordner deaktiviert)
@@ -635,15 +632,9 @@ class AppSettingsDlg(QDialog):
pdf_project.saxon_jar_id = new_data['saxon_jar_id'] if new_data['saxon_jar_id'] != -1 else pdf_project.saxon_jar_id
pdf_project.apache_fop_id = new_data['apache_fop_id'] if new_data['apache_fop_id'] != -1 else pdf_project.apache_fop_id
pdf_project.xsl_dir_id = new_data['xsl_dir_id'] if new_data['xsl_dir_id'] != -1 else pdf_project.xsl_dir_id
pdf_project.postgre_sql_db_id = new_data['postgre_sql_db_id'] if new_data['postgre_sql_db_id'] != -1 else pdf_project.postgre_sql_db_id
pdf_project.fop_config_dir = Path(new_data['fop_config_dir']) if new_data.get('fop_config_dir') else None
self._populate_pdf_project_table()
# Einstellungen speichern
self.settings.pdf_projects = self.temp_pdf_projects.copy()
self.settings.save()
# PostgreSQL Methoden
def _add_postgresql_db(self):
"""Fügt eine neue PostgreSQL-Datenbank hinzu."""
+34 -54
View File
@@ -32,12 +32,6 @@
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="opaqueResize">
<bool>false</bool>
</property>
<property name="childrenCollapsible">
<bool>false</bool>
</property>
<widget class="QFrame" name="frame">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
@@ -70,7 +64,7 @@
</sizepolicy>
</property>
<property name="columnCount">
<number>3</number>
<number>2</number>
</property>
<attribute name="headerHighlightSections">
<bool>true</bool>
@@ -88,11 +82,6 @@
<string notr="true">2</string>
</property>
</column>
<column>
<property name="text">
<string notr="true">3</string>
</property>
</column>
</widget>
</item>
<item>
@@ -174,12 +163,6 @@
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Raised</enum>
</property>
<property name="midLineWidth">
<number>1</number>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
@@ -188,8 +171,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>68</width>
<height>728</height>
<width>54</width>
<height>718</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
@@ -225,7 +208,7 @@
</widget>
<widget class="QFrame" name="frame_3">
<property name="frameShape">
<enum>QFrame::Shape::StyledPanel</enum>
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Raised</enum>
@@ -246,7 +229,7 @@
<item>
<widget class="QFrame" name="frame_4">
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
<enum>QFrame::Shape::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Raised</enum>
@@ -353,40 +336,11 @@
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="accept_changes">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>✅ Änderungen übernehmen</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QScrollArea" name="scrollArea_2">
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Raised</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
@@ -395,8 +349,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>726</width>
<height>697</height>
<width>649</width>
<height>690</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
@@ -412,6 +366,20 @@
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
@@ -428,7 +396,7 @@
<x>0</x>
<y>0</y>
<width>1263</width>
<height>22</height>
<height>33</height>
</rect>
</property>
<widget class="QMenu" name="menuProjekt">
@@ -436,6 +404,7 @@
<string>Projekt</string>
</property>
<addaction name="actionNeu"/>
<addaction name="action_ffnen"/>
<addaction name="separator"/>
<addaction name="actionVorhandene_Projekte"/>
<addaction name="separator"/>
@@ -463,6 +432,17 @@
<string>Ctrl+N</string>
</property>
</action>
<action name="action_ffnen">
<property name="icon">
<iconset theme="folder-open"/>
</property>
<property name="text">
<string>Öffnen ...</string>
</property>
<property name="shortcut">
<string>Ctrl+O</string>
</property>
</action>
<action name="actionBeenden">
<property name="icon">
<iconset theme="QIcon::ThemeIcon::ApplicationExit"/>
+38 -35
View File
@@ -3,7 +3,7 @@
################################################################################
## Form generated from reading UI file 'MainWinddow.ui'
##
## Created by: Qt User Interface Compiler version 6.9.2
## Created by: Qt User Interface Compiler version 6.9.1
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
@@ -31,14 +31,18 @@ class Ui_MainWindow(object):
self.actionNeu.setObjectName(u"actionNeu")
icon = QIcon(QIcon.fromTheme(u"folder-new"))
self.actionNeu.setIcon(icon)
self.action_ffnen = QAction(MainWindow)
self.action_ffnen.setObjectName(u"action_ffnen")
icon1 = QIcon(QIcon.fromTheme(u"folder-open"))
self.action_ffnen.setIcon(icon1)
self.actionBeenden = QAction(MainWindow)
self.actionBeenden.setObjectName(u"actionBeenden")
icon1 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.ApplicationExit))
self.actionBeenden.setIcon(icon1)
icon2 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.ApplicationExit))
self.actionBeenden.setIcon(icon2)
self.actionEinstellungen = QAction(MainWindow)
self.actionEinstellungen.setObjectName(u"actionEinstellungen")
icon2 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.DocumentProperties))
self.actionEinstellungen.setIcon(icon2)
icon3 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.DocumentProperties))
self.actionEinstellungen.setIcon(icon3)
self.actionVorhandene_Projekte = QAction(MainWindow)
self.actionVorhandene_Projekte.setObjectName(u"actionVorhandene_Projekte")
self.actionVorhandene_Projekte.setEnabled(False)
@@ -50,8 +54,6 @@ class Ui_MainWindow(object):
self.splitter = QSplitter(self.centralwidget)
self.splitter.setObjectName(u"splitter")
self.splitter.setOrientation(Qt.Orientation.Horizontal)
self.splitter.setOpaqueResize(False)
self.splitter.setChildrenCollapsible(False)
self.frame = QFrame(self.splitter)
self.frame.setObjectName(u"frame")
sizePolicy = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred)
@@ -67,7 +69,6 @@ class Ui_MainWindow(object):
self.verticalLayout.setContentsMargins(-1, -1, -1, 0)
self.treeWidget = QTreeWidget(self.frame)
__qtreewidgetitem = QTreeWidgetItem()
__qtreewidgetitem.setText(2, u"3");
__qtreewidgetitem.setText(1, u"2");
__qtreewidgetitem.setText(0, u"1");
self.treeWidget.setHeaderItem(__qtreewidgetitem)
@@ -77,7 +78,7 @@ class Ui_MainWindow(object):
sizePolicy1.setVerticalStretch(0)
sizePolicy1.setHeightForWidth(self.treeWidget.sizePolicy().hasHeightForWidth())
self.treeWidget.setSizePolicy(sizePolicy1)
self.treeWidget.setColumnCount(3)
self.treeWidget.setColumnCount(2)
self.treeWidget.header().setHighlightSections(True)
self.treeWidget.header().setStretchLastSection(True)
@@ -92,16 +93,16 @@ class Ui_MainWindow(object):
self.pushButton = QPushButton(self.frame_2)
self.pushButton.setObjectName(u"pushButton")
self.pushButton.setLayoutDirection(Qt.LayoutDirection.LeftToRight)
icon3 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.MediaPlaybackStart))
self.pushButton.setIcon(icon3)
icon4 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.MediaPlaybackStart))
self.pushButton.setIcon(icon4)
self.horizontalLayout_2.addWidget(self.pushButton)
self.pushButton_2 = QPushButton(self.frame_2)
self.pushButton_2.setObjectName(u"pushButton_2")
self.pushButton_2.setAutoFillBackground(False)
icon4 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.MediaSeekForward))
self.pushButton_2.setIcon(icon4)
icon5 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.MediaSeekForward))
self.pushButton_2.setIcon(icon5)
self.horizontalLayout_2.addWidget(self.pushButton_2)
@@ -111,8 +112,8 @@ class Ui_MainWindow(object):
self.pB_lade_aus_fn2 = QPushButton(self.frame_2)
self.pB_lade_aus_fn2.setObjectName(u"pB_lade_aus_fn2")
icon5 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.GoDown))
self.pB_lade_aus_fn2.setIcon(icon5)
icon6 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.GoDown))
self.pB_lade_aus_fn2.setIcon(icon6)
self.horizontalLayout_2.addWidget(self.pB_lade_aus_fn2)
@@ -127,12 +128,10 @@ class Ui_MainWindow(object):
sizePolicy2.setVerticalStretch(0)
sizePolicy2.setHeightForWidth(self.scrollArea.sizePolicy().hasHeightForWidth())
self.scrollArea.setSizePolicy(sizePolicy2)
self.scrollArea.setFrameShadow(QFrame.Shadow.Raised)
self.scrollArea.setMidLineWidth(1)
self.scrollArea.setWidgetResizable(True)
self.scrollAreaWidgetContents = QWidget()
self.scrollAreaWidgetContents.setObjectName(u"scrollAreaWidgetContents")
self.scrollAreaWidgetContents.setGeometry(QRect(0, 0, 68, 728))
self.scrollAreaWidgetContents.setGeometry(QRect(0, 0, 54, 718))
self.verticalLayout_2 = QVBoxLayout(self.scrollAreaWidgetContents)
self.verticalLayout_2.setObjectName(u"verticalLayout_2")
self.label = QLabel(self.scrollAreaWidgetContents)
@@ -153,14 +152,14 @@ class Ui_MainWindow(object):
self.splitter.addWidget(self.scrollArea)
self.frame_3 = QFrame(self.splitter)
self.frame_3.setObjectName(u"frame_3")
self.frame_3.setFrameShape(QFrame.Shape.StyledPanel)
self.frame_3.setFrameShape(QFrame.Shape.NoFrame)
self.frame_3.setFrameShadow(QFrame.Shadow.Raised)
self.verticalLayout_4 = QVBoxLayout(self.frame_3)
self.verticalLayout_4.setObjectName(u"verticalLayout_4")
self.verticalLayout_4.setContentsMargins(0, 0, 0, 0)
self.frame_4 = QFrame(self.frame_3)
self.frame_4.setObjectName(u"frame_4")
self.frame_4.setFrameShape(QFrame.Shape.NoFrame)
self.frame_4.setFrameShape(QFrame.Shape.StyledPanel)
self.frame_4.setFrameShadow(QFrame.Shadow.Raised)
self.horizontalLayout_3 = QHBoxLayout(self.frame_4)
self.horizontalLayout_3.setObjectName(u"horizontalLayout_3")
@@ -209,30 +208,28 @@ class Ui_MainWindow(object):
self.horizontalLayout_3.addItem(self.horizontalSpacer_5)
self.accept_changes = QPushButton(self.frame_4)
self.accept_changes.setObjectName(u"accept_changes")
self.accept_changes.setEnabled(False)
self.horizontalLayout_3.addWidget(self.accept_changes)
self.horizontalSpacer_3 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
self.horizontalLayout_3.addItem(self.horizontalSpacer_3)
self.verticalLayout_4.addWidget(self.frame_4)
self.scrollArea_2 = QScrollArea(self.frame_3)
self.scrollArea_2.setObjectName(u"scrollArea_2")
self.scrollArea_2.setFrameShape(QFrame.Shape.NoFrame)
self.scrollArea_2.setFrameShadow(QFrame.Shadow.Raised)
self.scrollArea_2.setWidgetResizable(True)
self.scrollAreaWidgetContents_2 = QWidget()
self.scrollAreaWidgetContents_2.setObjectName(u"scrollAreaWidgetContents_2")
self.scrollAreaWidgetContents_2.setGeometry(QRect(0, 0, 726, 697))
self.scrollAreaWidgetContents_2.setGeometry(QRect(0, 0, 649, 690))
self.verticalLayout_3 = QVBoxLayout(self.scrollAreaWidgetContents_2)
self.verticalLayout_3.setObjectName(u"verticalLayout_3")
self.verticalLayout_3.setContentsMargins(0, 0, 0, 0)
self.label_3 = QLabel(self.scrollAreaWidgetContents_2)
self.label_3.setObjectName(u"label_3")
self.verticalLayout_3.addWidget(self.label_3)
self.label_4 = QLabel(self.scrollAreaWidgetContents_2)
self.label_4.setObjectName(u"label_4")
self.verticalLayout_3.addWidget(self.label_4)
self.scrollArea_2.setWidget(self.scrollAreaWidgetContents_2)
self.verticalLayout_4.addWidget(self.scrollArea_2)
@@ -244,7 +241,7 @@ class Ui_MainWindow(object):
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QMenuBar(MainWindow)
self.menubar.setObjectName(u"menubar")
self.menubar.setGeometry(QRect(0, 0, 1263, 22))
self.menubar.setGeometry(QRect(0, 0, 1263, 33))
self.menuProjekt = QMenu(self.menubar)
self.menuProjekt.setObjectName(u"menuProjekt")
self.menuThema = QMenu(self.menubar)
@@ -257,6 +254,7 @@ class Ui_MainWindow(object):
self.menubar.addAction(self.menuProjekt.menuAction())
self.menubar.addAction(self.menuThema.menuAction())
self.menuProjekt.addAction(self.actionNeu)
self.menuProjekt.addAction(self.action_ffnen)
self.menuProjekt.addSeparator()
self.menuProjekt.addAction(self.actionVorhandene_Projekte)
self.menuProjekt.addSeparator()
@@ -275,6 +273,10 @@ class Ui_MainWindow(object):
self.actionNeu.setText(QCoreApplication.translate("MainWindow", u"Neu ...", None))
#if QT_CONFIG(shortcut)
self.actionNeu.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+N", None))
#endif // QT_CONFIG(shortcut)
self.action_ffnen.setText(QCoreApplication.translate("MainWindow", u"\u00d6ffnen ...", None))
#if QT_CONFIG(shortcut)
self.action_ffnen.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+O", None))
#endif // QT_CONFIG(shortcut)
self.actionBeenden.setText(QCoreApplication.translate("MainWindow", u"Beenden", None))
self.actionEinstellungen.setText(QCoreApplication.translate("MainWindow", u"Einstellungen ...", None))
@@ -290,7 +292,8 @@ class Ui_MainWindow(object):
self.label_6.setText(QCoreApplication.translate("MainWindow", u"Vorher (Referenz)", None))
self.label_7.setText(QCoreApplication.translate("MainWindow", u"Nachher (Neu)", None))
self.label_5.setText(QCoreApplication.translate("MainWindow", u"Zoom", None))
self.accept_changes.setText(QCoreApplication.translate("MainWindow", u"\u2705 \u00c4nderungen \u00fcbernehmen", None))
self.label_3.setText(QCoreApplication.translate("MainWindow", u"TextLabel", None))
self.label_4.setText(QCoreApplication.translate("MainWindow", u"TextLabel", None))
self.menuProjekt.setTitle(QCoreApplication.translate("MainWindow", u"Projekt", None))
self.menuThema.setTitle(QCoreApplication.translate("MainWindow", u"Thema", None))
# retranslateUi
+418 -2531
View File
File diff suppressed because it is too large Load Diff
+1 -26
View File
@@ -50,9 +50,6 @@ class PdfProjectDlg(QDialog):
# Browse-Button für Projekt-Ordner
self.ui.pushButton.clicked.connect(self.browse_project_dir)
# Browse-Button für FOP-Config-Ordner
self.ui.btnBrowseFopConfig.clicked.connect(self.browse_fop_config_dir)
# OK/Cancel Buttons sind bereits in der UI-Datei verbunden
# self.ui.buttonBox.accepted.connect(self.accept)
# self.ui.buttonBox.rejected.connect(self.reject)
@@ -136,10 +133,6 @@ class PdfProjectDlg(QDialog):
if 'postgre_sql_db_id' in self.project_data:
self._select_combo_by_data(self.ui.cB_Postgres, self.project_data['postgre_sql_db_id'])
# FOP-Config-Ordner
if 'fop_config_dir' in self.project_data and self.project_data['fop_config_dir']:
self.ui.lineFopConfigDir.setText(str(self.project_data['fop_config_dir']))
def _select_combo_by_data(self, combo_box, data_value):
"""
Wählt einen ComboBox-Eintrag basierend auf dem data-Wert aus.
@@ -174,22 +167,6 @@ class PdfProjectDlg(QDialog):
project_name = os.path.basename(selected_dir)
self.ui.lineProjectName.setText(project_name)
def browse_fop_config_dir(self):
"""Öffnet einen Dialog zum Auswählen des FOP-Config-Ordners."""
current_dir = self.ui.lineFopConfigDir.text()
if not current_dir or not os.path.exists(current_dir):
current_dir = os.path.expanduser("~")
selected_dir = QFileDialog.getExistingDirectory(
self,
"FOP-Config-Ordner auswählen",
current_dir,
QFileDialog.Option.ShowDirsOnly | QFileDialog.Option.DontResolveSymlinks
)
if selected_dir:
self.ui.lineFopConfigDir.setText(selected_dir)
def validate_and_accept(self):
"""Validiert die Eingaben und akzeptiert den Dialog."""
# Projekt-Name prüfen
@@ -259,7 +236,6 @@ class PdfProjectDlg(QDialog):
Returns:
dict: Dictionary mit allen Projektdaten
"""
fop_config_dir = self.ui.lineFopConfigDir.text().strip()
return {
'name': self.ui.lineProjectName.text().strip(),
'project_dir': self.ui.lineProjectDir.text().strip(),
@@ -268,8 +244,7 @@ class PdfProjectDlg(QDialog):
'saxon_jar_id': self.ui.cB_SaxonJar.currentData(),
'apache_fop_id': self.ui.cB_ApacheFop.currentData(),
'diff_pdf_id': self.ui.cB_Diff_Pdf.currentData(),
'postgre_sql_db_id': self.ui.cB_Postgres.currentData(),
'fop_config_dir': fop_config_dir if fop_config_dir else None
'postgre_sql_db_id': self.ui.cB_Postgres.currentData()
}
def _configure_edit_mode(self):
+4 -45
View File
@@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>608</width>
<height>331</height>
<height>299</height>
</rect>
</property>
<property name="windowTitle">
@@ -109,66 +109,25 @@
<widget class="QComboBox" name="cB_ApacheFop"/>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_9">
<property name="text">
<string>FOP-Config-Ordner:</string>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>diff-pdf:</string>
</property>
</widget>
</item>
<item row="7" column="1">
<item row="6" column="1">
<widget class="QComboBox" name="cB_Diff_Pdf"/>
</item>
<item row="8" column="0">
<item row="7" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Postgres:</string>
</property>
</widget>
</item>
<item row="8" column="1">
<item row="7" column="1">
<widget class="QComboBox" name="cB_Postgres"/>
</item>
<item row="6" column="1">
<widget class="QFrame" name="frame_2">
<property name="frameShape">
<enum>QFrame::Shape::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Raised</enum>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLineEdit" name="lineFopConfigDir"/>
</item>
<item>
<widget class="QPushButton" name="btnBrowseFopConfig">
<property name="text">
<string>Durchsuchen ...</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
+6 -33
View File
@@ -3,7 +3,7 @@
################################################################################
## Form generated from reading UI file 'PdfProject.ui'
##
## Created by: Qt User Interface Compiler version 6.9.2
## Created by: Qt User Interface Compiler version 6.9.1
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
@@ -24,7 +24,7 @@ class Ui_projectDlg(object):
def setupUi(self, projectDlg):
if not projectDlg.objectName():
projectDlg.setObjectName(u"projectDlg")
projectDlg.resize(608, 331)
projectDlg.resize(608, 299)
self.verticalLayout = QVBoxLayout(projectDlg)
self.verticalLayout.setObjectName(u"verticalLayout")
self.widget = QWidget(projectDlg)
@@ -106,50 +106,25 @@ class Ui_projectDlg(object):
self.formLayout.setWidget(5, QFormLayout.ItemRole.FieldRole, self.cB_ApacheFop)
self.label_9 = QLabel(self.widget)
self.label_9.setObjectName(u"label_9")
self.formLayout.setWidget(6, QFormLayout.ItemRole.LabelRole, self.label_9)
self.label_7 = QLabel(self.widget)
self.label_7.setObjectName(u"label_7")
self.formLayout.setWidget(7, QFormLayout.ItemRole.LabelRole, self.label_7)
self.formLayout.setWidget(6, QFormLayout.ItemRole.LabelRole, self.label_7)
self.cB_Diff_Pdf = QComboBox(self.widget)
self.cB_Diff_Pdf.setObjectName(u"cB_Diff_Pdf")
self.formLayout.setWidget(7, QFormLayout.ItemRole.FieldRole, self.cB_Diff_Pdf)
self.formLayout.setWidget(6, QFormLayout.ItemRole.FieldRole, self.cB_Diff_Pdf)
self.label_8 = QLabel(self.widget)
self.label_8.setObjectName(u"label_8")
self.formLayout.setWidget(8, QFormLayout.ItemRole.LabelRole, self.label_8)
self.formLayout.setWidget(7, QFormLayout.ItemRole.LabelRole, self.label_8)
self.cB_Postgres = QComboBox(self.widget)
self.cB_Postgres.setObjectName(u"cB_Postgres")
self.formLayout.setWidget(8, QFormLayout.ItemRole.FieldRole, self.cB_Postgres)
self.frame_2 = QFrame(self.widget)
self.frame_2.setObjectName(u"frame_2")
self.frame_2.setFrameShape(QFrame.Shape.StyledPanel)
self.frame_2.setFrameShadow(QFrame.Shadow.Raised)
self.horizontalLayout_2 = QHBoxLayout(self.frame_2)
self.horizontalLayout_2.setObjectName(u"horizontalLayout_2")
self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0)
self.lineFopConfigDir = QLineEdit(self.frame_2)
self.lineFopConfigDir.setObjectName(u"lineFopConfigDir")
self.horizontalLayout_2.addWidget(self.lineFopConfigDir)
self.btnBrowseFopConfig = QPushButton(self.frame_2)
self.btnBrowseFopConfig.setObjectName(u"btnBrowseFopConfig")
self.horizontalLayout_2.addWidget(self.btnBrowseFopConfig)
self.formLayout.setWidget(6, QFormLayout.ItemRole.FieldRole, self.frame_2)
self.formLayout.setWidget(7, QFormLayout.ItemRole.FieldRole, self.cB_Postgres)
self.verticalLayout.addWidget(self.widget)
@@ -179,9 +154,7 @@ class Ui_projectDlg(object):
self.label_4.setText(QCoreApplication.translate("projectDlg", u"Java VM:", None))
self.label_5.setText(QCoreApplication.translate("projectDlg", u"Saxon Jar:", None))
self.label_6.setText(QCoreApplication.translate("projectDlg", u"Apache FOP:", None))
self.label_9.setText(QCoreApplication.translate("projectDlg", u"FOP-Config-Ordner:", None))
self.label_7.setText(QCoreApplication.translate("projectDlg", u"diff-pdf:", None))
self.label_8.setText(QCoreApplication.translate("projectDlg", u"Postgres:", None))
self.btnBrowseFopConfig.setText(QCoreApplication.translate("projectDlg", u"Durchsuchen ...", None))
# retranslateUi
+2 -5
View File
@@ -2,6 +2,7 @@ from PySide6.QtWidgets import QDialog, QTableWidgetItem, QMessageBox
from PySide6.QtCore import Qt
from ui.TreeNodeEditDialog_ui import Ui_TreeNodeEditDialog
from conf import TreeNode
class TreeNodeEditDialog(QDialog):
@@ -145,13 +146,9 @@ class TreeNodeEditDialog(QDialog):
if key: # Nur Parameter mit nicht-leerem Schlüssel hinzufügen
xslt_params[key] = value
# CheckBox für Force-Transformation prüfen
force_transform = self.ui.alle_xml_transformieren.isChecked()
return {
"bez": bez,
"xslt_params": xslt_params,
"force_transform": force_transform
"xslt_params": xslt_params
}
def accept(self):
+76 -156
View File
@@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>870</width>
<width>600</width>
<height>400</height>
</rect>
</property>
@@ -35,175 +35,95 @@
</layout>
</item>
<item>
<widget class="QFrame" name="frame">
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
<widget class="QGroupBox" name="xsltParamsGroupBox">
<property name="title">
<string>XSLT-Parameter</string>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Raised</enum>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<layout class="QVBoxLayout" name="xsltParamsLayout">
<item>
<widget class="QGroupBox" name="xsltParamsGroupBox">
<property name="title">
<string>XSLT-Parameter</string>
<widget class="QTableWidget" name="xsltParamsTable">
<property name="columnCount">
<number>2</number>
</property>
<layout class="QVBoxLayout" name="xsltParamsLayout">
<property name="leftMargin">
<number>0</number>
<attribute name="horizontalHeaderVisible">
<bool>true</bool>
</attribute>
<column>
<property name="text">
<string>Parameter</string>
</property>
<property name="topMargin">
<number>0</number>
</column>
<column>
<property name="text">
<string>Wert</string>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QTableWidget" name="xsltParamsTable">
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="columnCount">
<number>2</number>
</property>
<attribute name="horizontalHeaderVisible">
<bool>true</bool>
</attribute>
<column>
<property name="text">
<string>Parameter</string>
</property>
</column>
<column>
<property name="text">
<string>Wert</string>
</property>
</column>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="xsltParamsButtonLayout">
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="addParamButton">
<property name="text">
<string>Parameter hinzufügen</string>
</property>
<property name="icon">
<iconset theme="QIcon::ThemeIcon::ListAdd"/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="removeParamButton">
<property name="text">
<string>Parameter entfernen</string>
</property>
<property name="icon">
<iconset theme="QIcon::ThemeIcon::ListRemove"/>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</column>
</widget>
</item>
<item>
<widget class="QGroupBox" name="parentParamsGroupBox">
<property name="title">
<string>Geerbte XSLT-Parameter (nur anzeigen)</string>
</property>
<layout class="QVBoxLayout" name="parentParamsLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QTableWidget" name="parentParamsTable">
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="editTriggers">
<set>QAbstractItemView::EditTrigger::NoEditTriggers</set>
</property>
<property name="columnCount">
<number>2</number>
</property>
<attribute name="horizontalHeaderVisible">
<bool>true</bool>
</attribute>
<column>
<property name="text">
<string>Parameter</string>
</property>
</column>
<column>
<property name="text">
<string>Wert</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
<layout class="QHBoxLayout" name="xsltParamsButtonLayout">
<item>
<widget class="QPushButton" name="addParamButton">
<property name="text">
<string>Parameter hinzufügen</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="removeParamButton">
<property name="text">
<string>Parameter entfernen</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QCheckBox" name="alle_xml_transformieren">
<property name="text">
<string>Alle XML-Dateien neu transformieren (force)</string>
<widget class="QGroupBox" name="parentParamsGroupBox">
<property name="title">
<string>Geerbte XSLT-Parameter (nur anzeigen)</string>
</property>
<layout class="QVBoxLayout" name="parentParamsLayout">
<item>
<widget class="QTableWidget" name="parentParamsTable">
<property name="editTriggers">
<set>QAbstractItemView::EditTrigger::NoEditTriggers</set>
</property>
<property name="columnCount">
<number>2</number>
</property>
<attribute name="horizontalHeaderVisible">
<bool>true</bool>
</attribute>
<column>
<property name="text">
<string>Parameter</string>
</property>
</column>
<column>
<property name="text">
<string>Wert</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
</item>
<item>
+11 -40
View File
@@ -3,7 +3,7 @@
################################################################################
## Form generated from reading UI file 'TreeNodeEditDialog.ui'
##
## Created by: Qt User Interface Compiler version 6.9.2
## Created by: Qt User Interface Compiler version 6.9.1
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
@@ -15,18 +15,17 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QAbstractButton, QAbstractItemView, QApplication, QCheckBox,
QDialog, QDialogButtonBox, QFormLayout, QFrame,
QGroupBox, QHBoxLayout, QHeaderView, QLabel,
QLayout, QLineEdit, QPushButton, QSizePolicy,
QSpacerItem, QTableWidget, QTableWidgetItem, QVBoxLayout,
QWidget)
from PySide6.QtWidgets import (QAbstractButton, QAbstractItemView, QApplication, QDialog,
QDialogButtonBox, QFormLayout, QGroupBox, QHBoxLayout,
QHeaderView, QLabel, QLayout, QLineEdit,
QPushButton, QSizePolicy, QSpacerItem, QTableWidget,
QTableWidgetItem, QVBoxLayout, QWidget)
class Ui_TreeNodeEditDialog(object):
def setupUi(self, TreeNodeEditDialog):
if not TreeNodeEditDialog.objectName():
TreeNodeEditDialog.setObjectName(u"TreeNodeEditDialog")
TreeNodeEditDialog.resize(870, 400)
TreeNodeEditDialog.resize(600, 400)
TreeNodeEditDialog.setModal(True)
self.verticalLayout = QVBoxLayout(TreeNodeEditDialog)
self.verticalLayout.setObjectName(u"verticalLayout")
@@ -46,18 +45,10 @@ class Ui_TreeNodeEditDialog(object):
self.verticalLayout.addLayout(self.formLayout)
self.frame = QFrame(TreeNodeEditDialog)
self.frame.setObjectName(u"frame")
self.frame.setFrameShape(QFrame.Shape.NoFrame)
self.frame.setFrameShadow(QFrame.Shadow.Raised)
self.horizontalLayout = QHBoxLayout(self.frame)
self.horizontalLayout.setObjectName(u"horizontalLayout")
self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
self.xsltParamsGroupBox = QGroupBox(self.frame)
self.xsltParamsGroupBox = QGroupBox(TreeNodeEditDialog)
self.xsltParamsGroupBox.setObjectName(u"xsltParamsGroupBox")
self.xsltParamsLayout = QVBoxLayout(self.xsltParamsGroupBox)
self.xsltParamsLayout.setObjectName(u"xsltParamsLayout")
self.xsltParamsLayout.setContentsMargins(0, 0, 0, 0)
self.xsltParamsTable = QTableWidget(self.xsltParamsGroupBox)
if (self.xsltParamsTable.columnCount() < 2):
self.xsltParamsTable.setColumnCount(2)
@@ -66,7 +57,6 @@ class Ui_TreeNodeEditDialog(object):
__qtablewidgetitem1 = QTableWidgetItem()
self.xsltParamsTable.setHorizontalHeaderItem(1, __qtablewidgetitem1)
self.xsltParamsTable.setObjectName(u"xsltParamsTable")
self.xsltParamsTable.setFrameShape(QFrame.Shape.NoFrame)
self.xsltParamsTable.setColumnCount(2)
self.xsltParamsTable.horizontalHeader().setVisible(True)
@@ -74,21 +64,13 @@ class Ui_TreeNodeEditDialog(object):
self.xsltParamsButtonLayout = QHBoxLayout()
self.xsltParamsButtonLayout.setObjectName(u"xsltParamsButtonLayout")
self.horizontalSpacer_2 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
self.xsltParamsButtonLayout.addItem(self.horizontalSpacer_2)
self.addParamButton = QPushButton(self.xsltParamsGroupBox)
self.addParamButton.setObjectName(u"addParamButton")
icon = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.ListAdd))
self.addParamButton.setIcon(icon)
self.xsltParamsButtonLayout.addWidget(self.addParamButton)
self.removeParamButton = QPushButton(self.xsltParamsGroupBox)
self.removeParamButton.setObjectName(u"removeParamButton")
icon1 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.ListRemove))
self.removeParamButton.setIcon(icon1)
self.xsltParamsButtonLayout.addWidget(self.removeParamButton)
@@ -100,13 +82,12 @@ class Ui_TreeNodeEditDialog(object):
self.xsltParamsLayout.addLayout(self.xsltParamsButtonLayout)
self.horizontalLayout.addWidget(self.xsltParamsGroupBox)
self.verticalLayout.addWidget(self.xsltParamsGroupBox)
self.parentParamsGroupBox = QGroupBox(self.frame)
self.parentParamsGroupBox = QGroupBox(TreeNodeEditDialog)
self.parentParamsGroupBox.setObjectName(u"parentParamsGroupBox")
self.parentParamsLayout = QVBoxLayout(self.parentParamsGroupBox)
self.parentParamsLayout.setObjectName(u"parentParamsLayout")
self.parentParamsLayout.setContentsMargins(0, 0, 0, 0)
self.parentParamsTable = QTableWidget(self.parentParamsGroupBox)
if (self.parentParamsTable.columnCount() < 2):
self.parentParamsTable.setColumnCount(2)
@@ -115,7 +96,6 @@ class Ui_TreeNodeEditDialog(object):
__qtablewidgetitem3 = QTableWidgetItem()
self.parentParamsTable.setHorizontalHeaderItem(1, __qtablewidgetitem3)
self.parentParamsTable.setObjectName(u"parentParamsTable")
self.parentParamsTable.setFrameShape(QFrame.Shape.NoFrame)
self.parentParamsTable.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
self.parentParamsTable.setColumnCount(2)
self.parentParamsTable.horizontalHeader().setVisible(True)
@@ -123,15 +103,7 @@ class Ui_TreeNodeEditDialog(object):
self.parentParamsLayout.addWidget(self.parentParamsTable)
self.horizontalLayout.addWidget(self.parentParamsGroupBox)
self.verticalLayout.addWidget(self.frame)
self.alle_xml_transformieren = QCheckBox(TreeNodeEditDialog)
self.alle_xml_transformieren.setObjectName(u"alle_xml_transformieren")
self.verticalLayout.addWidget(self.alle_xml_transformieren)
self.verticalLayout.addWidget(self.parentParamsGroupBox)
self.buttonBox = QDialogButtonBox(TreeNodeEditDialog)
self.buttonBox.setObjectName(u"buttonBox")
@@ -164,6 +136,5 @@ class Ui_TreeNodeEditDialog(object):
___qtablewidgetitem2.setText(QCoreApplication.translate("TreeNodeEditDialog", u"Parameter", None));
___qtablewidgetitem3 = self.parentParamsTable.horizontalHeaderItem(1)
___qtablewidgetitem3.setText(QCoreApplication.translate("TreeNodeEditDialog", u"Wert", None));
self.alle_xml_transformieren.setText(QCoreApplication.translate("TreeNodeEditDialog", u"Alle XML-Dateien neu transformieren (force)", None))
# retranslateUi
+5 -18
View File
@@ -1,4 +1,3 @@
import logging
from PySide6.QtWidgets import QDialog, QTreeWidgetItem, QCheckBox, QMessageBox, QWidget, QHBoxLayout
from PySide6.QtCore import Qt
from pathlib import Path
@@ -7,9 +6,6 @@ from ui.XmlToXslAssignDialog_ui import Ui_XmlToXslAssignDialog
from conf import TreeNode, XslFile
logger = logging.getLogger(__name__)
class XmlToXslAssignDialog(QDialog):
"""Dialog zur Zuordnung einer XML-Datei zu XSL-Knoten."""
@@ -89,10 +85,10 @@ class XmlToXslAssignDialog(QDialog):
root = self.ui.xslNodesTree.invisibleRootItem()
self._add_checkboxes_recursive(root)
logger.debug(f"Checkboxen zu {len(self.xsl_checkboxes)} XSL-Knoten hinzugefügt")
print(f"Checkboxen zu {len(self.xsl_checkboxes)} XSL-Knoten hinzugefügt")
except Exception as e:
logger.error(f"Fehler beim Hinzufügen der Checkboxen: {e}")
print(f"Fehler beim Hinzufügen der Checkboxen: {e}")
def _add_checkboxes_recursive(self, parent_item):
"""
@@ -117,7 +113,7 @@ class XmlToXslAssignDialog(QDialog):
# Speichere Checkbox-Referenz
self.xsl_checkboxes[id(node)] = checkbox
logger.debug(f"Checkbox für XSL-Knoten '{node.bez}' hinzugefügt")
print(f"Checkbox für XSL-Knoten '{node.bez}' hinzugefügt")
# Rekursiv für Kinder
if item.childCount() > 0:
@@ -205,7 +201,7 @@ class XmlToXslAssignDialog(QDialog):
return item
except Exception as e:
logger.error(f"Fehler beim Erstellen des Tree-Items: {e}")
print(f"Fehler beim Erstellen des Tree-Items: {e}")
return None
def select_all(self):
@@ -239,7 +235,7 @@ class XmlToXslAssignDialog(QDialog):
return selected_nodes
except Exception as e:
logger.error(f"Fehler beim Sammeln der ausgewählten XSL-Knoten: {e}")
print(f"Fehler beim Sammeln der ausgewählten XSL-Knoten: {e}")
return []
def _find_xsl_node_by_id(self, node_id):
@@ -286,15 +282,6 @@ class XmlToXslAssignDialog(QDialog):
"""
return self.xml_file_path
def is_apply_to_all(self):
"""
Prüft, ob die Checkbox 'Alle XML-Dateien' aktiviert ist.
Returns:
bool: True wenn die Checkbox aktiviert ist, sonst False
"""
return self.ui.alle_xml.isChecked()
def accept(self):
"""Überschreibt accept() um Validierung durchzuführen."""
selected_nodes = self.get_selected_xsl_nodes()
-7
View File
@@ -97,13 +97,6 @@
</property>
</spacer>
</item>
<item>
<widget class="QCheckBox" name="alle_xml">
<property name="text">
<string>Alle XML-Dateien den ausgewählten XSL-Dateien zuordnen</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
+5 -11
View File
@@ -3,7 +3,7 @@
################################################################################
## Form generated from reading UI file 'XmlToXslAssignDialog.ui'
##
## Created by: Qt User Interface Compiler version 6.9.2
## Created by: Qt User Interface Compiler version 6.9.1
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
@@ -15,10 +15,10 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QAbstractButton, QApplication, QCheckBox, QDialog,
QDialogButtonBox, QHBoxLayout, QHeaderView, QLabel,
QPushButton, QSizePolicy, QSpacerItem, QTreeWidget,
QTreeWidgetItem, QVBoxLayout, QWidget)
from PySide6.QtWidgets import (QAbstractButton, QApplication, QDialog, QDialogButtonBox,
QHBoxLayout, QHeaderView, QLabel, QPushButton,
QSizePolicy, QSpacerItem, QTreeWidget, QTreeWidgetItem,
QVBoxLayout, QWidget)
class Ui_XmlToXslAssignDialog(object):
def setupUi(self, XmlToXslAssignDialog):
@@ -64,11 +64,6 @@ class Ui_XmlToXslAssignDialog(object):
self.buttonLayout.addItem(self.horizontalSpacer)
self.alle_xml = QCheckBox(XmlToXslAssignDialog)
self.alle_xml.setObjectName(u"alle_xml")
self.buttonLayout.addWidget(self.alle_xml)
self.verticalLayout.addLayout(self.buttonLayout)
@@ -99,6 +94,5 @@ class Ui_XmlToXslAssignDialog(object):
___qtreewidgetitem.setText(0, QCoreApplication.translate("XmlToXslAssignDialog", u"XSL-Knoten", None));
self.selectAllButton.setText(QCoreApplication.translate("XmlToXslAssignDialog", u"Alle ausw\u00e4hlen", None))
self.deselectAllButton.setText(QCoreApplication.translate("XmlToXslAssignDialog", u"Alle abw\u00e4hlen", None))
self.alle_xml.setText(QCoreApplication.translate("XmlToXslAssignDialog", u"Alle XML-Dateien den ausgew\u00e4hlten XSL-Dateien zuordnen", None))
# retranslateUi
+2 -5
View File
@@ -2,6 +2,7 @@ from PySide6.QtWidgets import QDialog, QTableWidgetItem, QMessageBox
from PySide6.QtCore import Qt
from ui.XslFileEditDialog_ui import Ui_XslFileEditDialog
from conf import XslFile
class XslFileEditDialog(QDialog):
@@ -145,13 +146,9 @@ class XslFileEditDialog(QDialog):
if key: # Nur Parameter mit nicht-leerem Schlüssel hinzufügen
xslt_params[key] = value
# CheckBox für Force-Transformation prüfen
force_transform = self.ui.alle_xml_transformieren.isChecked()
return {
"bez": bez,
"xslt_params": xslt_params,
"force_transform": force_transform
"xslt_params": xslt_params
}
def accept(self):
+76 -156
View File
@@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>865</width>
<width>600</width>
<height>400</height>
</rect>
</property>
@@ -35,175 +35,95 @@
</layout>
</item>
<item>
<widget class="QFrame" name="frame">
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
<widget class="QGroupBox" name="xsltParamsGroupBox">
<property name="title">
<string>XSLT-Parameter</string>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Raised</enum>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<layout class="QVBoxLayout" name="xsltParamsLayout">
<item>
<widget class="QGroupBox" name="xsltParamsGroupBox">
<property name="title">
<string>XSLT-Parameter</string>
<widget class="QTableWidget" name="xsltParamsTable">
<property name="columnCount">
<number>2</number>
</property>
<layout class="QVBoxLayout" name="xsltParamsLayout">
<property name="leftMargin">
<number>0</number>
<attribute name="horizontalHeaderVisible">
<bool>true</bool>
</attribute>
<column>
<property name="text">
<string>Parameter</string>
</property>
<property name="topMargin">
<number>0</number>
</column>
<column>
<property name="text">
<string>Wert</string>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QTableWidget" name="xsltParamsTable">
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="columnCount">
<number>2</number>
</property>
<attribute name="horizontalHeaderVisible">
<bool>true</bool>
</attribute>
<column>
<property name="text">
<string>Parameter</string>
</property>
</column>
<column>
<property name="text">
<string>Wert</string>
</property>
</column>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="xsltParamsButtonLayout">
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="addParamButton">
<property name="text">
<string>Parameter hinzufügen</string>
</property>
<property name="icon">
<iconset theme="QIcon::ThemeIcon::ListAdd"/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="removeParamButton">
<property name="text">
<string>Parameter entfernen</string>
</property>
<property name="icon">
<iconset theme="QIcon::ThemeIcon::ListRemove"/>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</column>
</widget>
</item>
<item>
<widget class="QGroupBox" name="parentParamsGroupBox">
<property name="title">
<string>Geerbte XSLT-Parameter (nur anzeigen)</string>
</property>
<layout class="QVBoxLayout" name="parentParamsLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QTableWidget" name="parentParamsTable">
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="editTriggers">
<set>QAbstractItemView::EditTrigger::NoEditTriggers</set>
</property>
<property name="columnCount">
<number>2</number>
</property>
<attribute name="horizontalHeaderVisible">
<bool>true</bool>
</attribute>
<column>
<property name="text">
<string>Parameter</string>
</property>
</column>
<column>
<property name="text">
<string>Wert</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
<layout class="QHBoxLayout" name="xsltParamsButtonLayout">
<item>
<widget class="QPushButton" name="addParamButton">
<property name="text">
<string>Parameter hinzufügen</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="removeParamButton">
<property name="text">
<string>Parameter entfernen</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QCheckBox" name="alle_xml_transformieren">
<property name="text">
<string>Alle XML-Dateien neu transformieren (force)</string>
<widget class="QGroupBox" name="parentParamsGroupBox">
<property name="title">
<string>Geerbte XSLT-Parameter (nur anzeigen)</string>
</property>
<layout class="QVBoxLayout" name="parentParamsLayout">
<item>
<widget class="QTableWidget" name="parentParamsTable">
<property name="editTriggers">
<set>QAbstractItemView::EditTrigger::NoEditTriggers</set>
</property>
<property name="columnCount">
<number>2</number>
</property>
<attribute name="horizontalHeaderVisible">
<bool>true</bool>
</attribute>
<column>
<property name="text">
<string>Parameter</string>
</property>
</column>
<column>
<property name="text">
<string>Wert</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
</item>
<item>
+11 -40
View File
@@ -3,7 +3,7 @@
################################################################################
## Form generated from reading UI file 'XslFileEditDialog.ui'
##
## Created by: Qt User Interface Compiler version 6.9.2
## Created by: Qt User Interface Compiler version 6.9.1
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
@@ -15,18 +15,17 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QAbstractButton, QAbstractItemView, QApplication, QCheckBox,
QDialog, QDialogButtonBox, QFormLayout, QFrame,
QGroupBox, QHBoxLayout, QHeaderView, QLabel,
QLayout, QLineEdit, QPushButton, QSizePolicy,
QSpacerItem, QTableWidget, QTableWidgetItem, QVBoxLayout,
QWidget)
from PySide6.QtWidgets import (QAbstractButton, QAbstractItemView, QApplication, QDialog,
QDialogButtonBox, QFormLayout, QGroupBox, QHBoxLayout,
QHeaderView, QLabel, QLayout, QLineEdit,
QPushButton, QSizePolicy, QSpacerItem, QTableWidget,
QTableWidgetItem, QVBoxLayout, QWidget)
class Ui_XslFileEditDialog(object):
def setupUi(self, XslFileEditDialog):
if not XslFileEditDialog.objectName():
XslFileEditDialog.setObjectName(u"XslFileEditDialog")
XslFileEditDialog.resize(865, 400)
XslFileEditDialog.resize(600, 400)
XslFileEditDialog.setModal(True)
self.verticalLayout = QVBoxLayout(XslFileEditDialog)
self.verticalLayout.setObjectName(u"verticalLayout")
@@ -46,18 +45,10 @@ class Ui_XslFileEditDialog(object):
self.verticalLayout.addLayout(self.formLayout)
self.frame = QFrame(XslFileEditDialog)
self.frame.setObjectName(u"frame")
self.frame.setFrameShape(QFrame.Shape.NoFrame)
self.frame.setFrameShadow(QFrame.Shadow.Raised)
self.horizontalLayout = QHBoxLayout(self.frame)
self.horizontalLayout.setObjectName(u"horizontalLayout")
self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
self.xsltParamsGroupBox = QGroupBox(self.frame)
self.xsltParamsGroupBox = QGroupBox(XslFileEditDialog)
self.xsltParamsGroupBox.setObjectName(u"xsltParamsGroupBox")
self.xsltParamsLayout = QVBoxLayout(self.xsltParamsGroupBox)
self.xsltParamsLayout.setObjectName(u"xsltParamsLayout")
self.xsltParamsLayout.setContentsMargins(0, 0, 0, 0)
self.xsltParamsTable = QTableWidget(self.xsltParamsGroupBox)
if (self.xsltParamsTable.columnCount() < 2):
self.xsltParamsTable.setColumnCount(2)
@@ -66,7 +57,6 @@ class Ui_XslFileEditDialog(object):
__qtablewidgetitem1 = QTableWidgetItem()
self.xsltParamsTable.setHorizontalHeaderItem(1, __qtablewidgetitem1)
self.xsltParamsTable.setObjectName(u"xsltParamsTable")
self.xsltParamsTable.setFrameShape(QFrame.Shape.NoFrame)
self.xsltParamsTable.setColumnCount(2)
self.xsltParamsTable.horizontalHeader().setVisible(True)
@@ -74,21 +64,13 @@ class Ui_XslFileEditDialog(object):
self.xsltParamsButtonLayout = QHBoxLayout()
self.xsltParamsButtonLayout.setObjectName(u"xsltParamsButtonLayout")
self.horizontalSpacer_2 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
self.xsltParamsButtonLayout.addItem(self.horizontalSpacer_2)
self.addParamButton = QPushButton(self.xsltParamsGroupBox)
self.addParamButton.setObjectName(u"addParamButton")
icon = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.ListAdd))
self.addParamButton.setIcon(icon)
self.xsltParamsButtonLayout.addWidget(self.addParamButton)
self.removeParamButton = QPushButton(self.xsltParamsGroupBox)
self.removeParamButton.setObjectName(u"removeParamButton")
icon1 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.ListRemove))
self.removeParamButton.setIcon(icon1)
self.xsltParamsButtonLayout.addWidget(self.removeParamButton)
@@ -100,13 +82,12 @@ class Ui_XslFileEditDialog(object):
self.xsltParamsLayout.addLayout(self.xsltParamsButtonLayout)
self.horizontalLayout.addWidget(self.xsltParamsGroupBox)
self.verticalLayout.addWidget(self.xsltParamsGroupBox)
self.parentParamsGroupBox = QGroupBox(self.frame)
self.parentParamsGroupBox = QGroupBox(XslFileEditDialog)
self.parentParamsGroupBox.setObjectName(u"parentParamsGroupBox")
self.parentParamsLayout = QVBoxLayout(self.parentParamsGroupBox)
self.parentParamsLayout.setObjectName(u"parentParamsLayout")
self.parentParamsLayout.setContentsMargins(0, 0, 0, 0)
self.parentParamsTable = QTableWidget(self.parentParamsGroupBox)
if (self.parentParamsTable.columnCount() < 2):
self.parentParamsTable.setColumnCount(2)
@@ -115,7 +96,6 @@ class Ui_XslFileEditDialog(object):
__qtablewidgetitem3 = QTableWidgetItem()
self.parentParamsTable.setHorizontalHeaderItem(1, __qtablewidgetitem3)
self.parentParamsTable.setObjectName(u"parentParamsTable")
self.parentParamsTable.setFrameShape(QFrame.Shape.NoFrame)
self.parentParamsTable.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
self.parentParamsTable.setColumnCount(2)
self.parentParamsTable.horizontalHeader().setVisible(True)
@@ -123,15 +103,7 @@ class Ui_XslFileEditDialog(object):
self.parentParamsLayout.addWidget(self.parentParamsTable)
self.horizontalLayout.addWidget(self.parentParamsGroupBox)
self.verticalLayout.addWidget(self.frame)
self.alle_xml_transformieren = QCheckBox(XslFileEditDialog)
self.alle_xml_transformieren.setObjectName(u"alle_xml_transformieren")
self.verticalLayout.addWidget(self.alle_xml_transformieren)
self.verticalLayout.addWidget(self.parentParamsGroupBox)
self.buttonBox = QDialogButtonBox(XslFileEditDialog)
self.buttonBox.setObjectName(u"buttonBox")
@@ -164,6 +136,5 @@ class Ui_XslFileEditDialog(object):
___qtablewidgetitem2.setText(QCoreApplication.translate("XslFileEditDialog", u"Parameter", None));
___qtablewidgetitem3 = self.parentParamsTable.horizontalHeaderItem(1)
___qtablewidgetitem3.setText(QCoreApplication.translate("XslFileEditDialog", u"Wert", None));
self.alle_xml_transformieren.setText(QCoreApplication.translate("XslFileEditDialog", u"Alle XML-Dateien neu transformieren (force)", None))
# retranslateUi
+2 -2
View File
@@ -225,11 +225,11 @@ def test_integration_workflow():
assert duplicate_found is not None, "Duplikat sollte gefunden werden"
assert duplicate_found.xml == Path("xml/existing3.xml"), "Falsches Duplikat gefunden"
print("Workflow-Test erfolgreich:")
print(f"Workflow-Test erfolgreich:")
print(f" - Neue Datei: {new_xml_file.name}")
print(f" - Berechneter Hash: {new_hash}")
print(f" - Duplikat gefunden: {duplicate_found.xml}")
print(" - Automatische Zuordnung würde erfolgen")
print(f" - Automatische Zuordnung würde erfolgen")
print("[OK] Integration Workflow funktioniert korrekt")
Generated
+1 -32
View File
@@ -44,11 +44,6 @@ dependencies = [
{ name = "pyside6" },
]
[package.dev-dependencies]
dev = [
{ name = "ruff" },
]
[package.metadata]
requires-dist = [
{ name = "polars", extras = ["connectorx", "pyarrow"], specifier = ">=1.31.0" },
@@ -59,7 +54,7 @@ requires-dist = [
]
[package.metadata.requires-dev]
dev = [{ name = "ruff", specifier = ">=0.14.8" }]
dev = []
[[package]]
name = "polars"
@@ -274,32 +269,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b0/85/e8e751d8791564dd333d5d9a4eab0a7a115f7e349595417fd50ecae3395c/ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3", size = 115190, upload-time = "2024-10-20T10:13:10.66Z" },
]
[[package]]
name = "ruff"
version = "0.14.8"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ed/d9/f7a0c4b3a2bf2556cd5d99b05372c29980249ef71e8e32669ba77428c82c/ruff-0.14.8.tar.gz", hash = "sha256:774ed0dd87d6ce925e3b8496feb3a00ac564bea52b9feb551ecd17e0a23d1eed", size = 5765385, upload-time = "2025-12-04T15:06:17.669Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/48/b8/9537b52010134b1d2b72870cc3f92d5fb759394094741b09ceccae183fbe/ruff-0.14.8-py3-none-linux_armv6l.whl", hash = "sha256:ec071e9c82eca417f6111fd39f7043acb53cd3fde9b1f95bbed745962e345afb", size = 13441540, upload-time = "2025-12-04T15:06:14.896Z" },
{ url = "https://files.pythonhosted.org/packages/24/00/99031684efb025829713682012b6dd37279b1f695ed1b01725f85fd94b38/ruff-0.14.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8cdb162a7159f4ca36ce980a18c43d8f036966e7f73f866ac8f493b75e0c27e9", size = 13669384, upload-time = "2025-12-04T15:06:51.809Z" },
{ url = "https://files.pythonhosted.org/packages/72/64/3eb5949169fc19c50c04f28ece2c189d3b6edd57e5b533649dae6ca484fe/ruff-0.14.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2e2fcbefe91f9fad0916850edf0854530c15bd1926b6b779de47e9ab619ea38f", size = 12806917, upload-time = "2025-12-04T15:06:08.925Z" },
{ url = "https://files.pythonhosted.org/packages/c4/08/5250babb0b1b11910f470370ec0cbc67470231f7cdc033cee57d4976f941/ruff-0.14.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9d70721066a296f45786ec31916dc287b44040f553da21564de0ab4d45a869b", size = 13256112, upload-time = "2025-12-04T15:06:23.498Z" },
{ url = "https://files.pythonhosted.org/packages/78/4c/6c588e97a8e8c2d4b522c31a579e1df2b4d003eddfbe23d1f262b1a431ff/ruff-0.14.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c87e09b3cd9d126fc67a9ecd3b5b1d3ded2b9c7fce3f16e315346b9d05cfb52", size = 13227559, upload-time = "2025-12-04T15:06:33.432Z" },
{ url = "https://files.pythonhosted.org/packages/23/ce/5f78cea13eda8eceac71b5f6fa6e9223df9b87bb2c1891c166d1f0dce9f1/ruff-0.14.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d62cb310c4fbcb9ee4ac023fe17f984ae1e12b8a4a02e3d21489f9a2a5f730c", size = 13896379, upload-time = "2025-12-04T15:06:02.687Z" },
{ url = "https://files.pythonhosted.org/packages/cf/79/13de4517c4dadce9218a20035b21212a4c180e009507731f0d3b3f5df85a/ruff-0.14.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1af35c2d62633d4da0521178e8a2641c636d2a7153da0bac1b30cfd4ccd91344", size = 15372786, upload-time = "2025-12-04T15:06:29.828Z" },
{ url = "https://files.pythonhosted.org/packages/00/06/33df72b3bb42be8a1c3815fd4fae83fa2945fc725a25d87ba3e42d1cc108/ruff-0.14.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:25add4575ffecc53d60eed3f24b1e934493631b48ebbc6ebaf9d8517924aca4b", size = 14990029, upload-time = "2025-12-04T15:06:36.812Z" },
{ url = "https://files.pythonhosted.org/packages/64/61/0f34927bd90925880394de0e081ce1afab66d7b3525336f5771dcf0cb46c/ruff-0.14.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c943d847b7f02f7db4201a0600ea7d244d8a404fbb639b439e987edcf2baf9a", size = 14407037, upload-time = "2025-12-04T15:06:39.979Z" },
{ url = "https://files.pythonhosted.org/packages/96/bc/058fe0aefc0fbf0d19614cb6d1a3e2c048f7dc77ca64957f33b12cfdc5ef/ruff-0.14.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb6e8bf7b4f627548daa1b69283dac5a296bfe9ce856703b03130732e20ddfe2", size = 14102390, upload-time = "2025-12-04T15:06:46.372Z" },
{ url = "https://files.pythonhosted.org/packages/af/a4/e4f77b02b804546f4c17e8b37a524c27012dd6ff05855d2243b49a7d3cb9/ruff-0.14.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:7aaf2974f378e6b01d1e257c6948207aec6a9b5ba53fab23d0182efb887a0e4a", size = 14230793, upload-time = "2025-12-04T15:06:20.497Z" },
{ url = "https://files.pythonhosted.org/packages/3f/52/bb8c02373f79552e8d087cedaffad76b8892033d2876c2498a2582f09dcf/ruff-0.14.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e5758ca513c43ad8a4ef13f0f081f80f08008f410790f3611a21a92421ab045b", size = 13160039, upload-time = "2025-12-04T15:06:49.06Z" },
{ url = "https://files.pythonhosted.org/packages/1f/ad/b69d6962e477842e25c0b11622548df746290cc6d76f9e0f4ed7456c2c31/ruff-0.14.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f74f7ba163b6e85a8d81a590363bf71618847e5078d90827749bfda1d88c9cdf", size = 13205158, upload-time = "2025-12-04T15:06:54.574Z" },
{ url = "https://files.pythonhosted.org/packages/06/63/54f23da1315c0b3dfc1bc03fbc34e10378918a20c0b0f086418734e57e74/ruff-0.14.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:eed28f6fafcc9591994c42254f5a5c5ca40e69a30721d2ab18bb0bb3baac3ab6", size = 13469550, upload-time = "2025-12-04T15:05:59.209Z" },
{ url = "https://files.pythonhosted.org/packages/70/7d/a4d7b1961e4903bc37fffb7ddcfaa7beb250f67d97cfd1ee1d5cddb1ec90/ruff-0.14.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:21d48fa744c9d1cb8d71eb0a740c4dd02751a5de9db9a730a8ef75ca34cf138e", size = 14211332, upload-time = "2025-12-04T15:06:06.027Z" },
{ url = "https://files.pythonhosted.org/packages/5d/93/2a5063341fa17054e5c86582136e9895db773e3c2ffb770dde50a09f35f0/ruff-0.14.8-py3-none-win32.whl", hash = "sha256:15f04cb45c051159baebb0f0037f404f1dc2f15a927418f29730f411a79bc4e7", size = 13151890, upload-time = "2025-12-04T15:06:11.668Z" },
{ url = "https://files.pythonhosted.org/packages/02/1c/65c61a0859c0add13a3e1cbb6024b42de587456a43006ca2d4fd3d1618fe/ruff-0.14.8-py3-none-win_amd64.whl", hash = "sha256:9eeb0b24242b5bbff3011409a739929f497f3fb5fe3b5698aba5e77e8c833097", size = 14537826, upload-time = "2025-12-04T15:06:26.409Z" },
{ url = "https://files.pythonhosted.org/packages/6d/63/8b41cea3afd7f58eb64ac9251668ee0073789a3bc9ac6f816c8c6fef986d/ruff-0.14.8-py3-none-win_arm64.whl", hash = "sha256:965a582c93c63fe715fd3e3f8aa37c4b776777203d8e1d8aa3cc0c14424a4b99", size = 13634522, upload-time = "2025-12-04T15:06:43.212Z" },
]
[[package]]
name = "shiboken6"
version = "6.9.2"