159 Commits

Author SHA1 Message Date
info 789fb5d77f docs: Projektnamen bereinigen, README und Web aktualisieren
Entfernt den Zusatz "ehemals xsl-validator" aus CLAUDE.md und README.
README mit korrekten Infos zu Theme, Worker-Pool und externen Tools ergänzt.
Download-Links auf Web-Seite auf Version 1.7.3 aktualisiert.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 19:37:17 +02:00
info d19c36191c Version 1.7.3: Fehlende Menü-Icons ergänzt
- MainWindow: Icons für drei bislang icon-lose Menü-Aktionen gesetzt
  - "Alle XML-Dateien transformieren" → play-circle
  - "Aus Datenbank laden" → database (neu)
  - "Worker-Pool-Metriken" → activity (neu)
- Zwei neue Feather-Icons (database, activity) ergänzt: Download-Skript,
  resources.qrc und resources_rc.py (via pyside6-rcc) aktualisiert

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 12:45:25 +02:00
info 3e20b186c7 Version 1.7.2: Icon-System – Theme-Reaktivität generalisiert, Render vereinfacht
- icons.py: IconRefreshMixin ergänzt – reagiert auf PaletteChange/StyleChange
  und färbt Icons nur bei tatsächlichem Textfarb-Wechsel neu ein (farb-gegated)
- icons.py: Render auf einen einzelnen 64px-Pixmap vereinfacht (Qt skaliert),
  statt drei fixe Größen (16/24/32)
- MainWindow: nutzt IconRefreshMixin; hasattr-Guards in _setup_icons entfernt
  (läuft jetzt nur im fertig initialisierten Zustand); change_theme entschlackt –
  Icon-/Baum-Aktualisierung erfolgt über changeEvent statt unbedingtem Aufruf
- XslDependencyDialog: nutzt IconRefreshMixin, färbt Button- und Baum-Icons bei
  Theme-Wechsel neu ein (bislang blieben offene non-modale Dialoge stale)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 12:36:58 +02:00
info 74b08e31c7 Version 1.7.1: Icon-System vereinfacht und gecacht
- icons.py: _ICON_MAP entfernt (war reine Identitätsabbildung), Pfad
  wird direkt aus dem Namen abgeleitet
- icons.py: Render-Cache mit Schlüssel (name, theme-farbe) ergänzt,
  vermeidet wiederholtes SVG-Rendering bei Baum-/Kontextmenü-Aufbau
- icons.py: Qt-Ressourcen-Registrierung gekapselt (Import aus main.py
  hierher verschoben)
- download_icons.py: toten folder-open-Eintrag entfernt (in Feather
  nicht vorhanden, nirgends genutzt)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 11:51:21 +02:00
info c837802fe7 docs(web): update download links and version to 1.7.0
Update index.html to reflect the latest release version 1.7.0,
including MSI and ZIP download links with corrected release tag format.
2026-05-30 16:50:05 +02:00
info 712bd8917e version-bump Skill: Git-Tag nach Commit setzen
Der Skill setzt jetzt nach einem erfolgreichen Commit mit Versionserhöhung
automatisch einen passenden annotated Git-Tag (z.B. v1.7.1).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 16:38:57 +02:00
info ec753a5770 Version 1.7.0: Feather Icons via Qt-Ressourcensystem eingebunden
Alle QIcon.fromTheme()-Aufrufe durch eingebettete Feather Icons ersetzt,
die unter Windows zuverlässig funktionieren. Icons passen sich automatisch
der Palette-Farbe des aktiven Themes an (QSvgRenderer + currentColor).

- scripts/download_icons.py: lädt 20 Feather-SVGs von GitHub
- src/res/icons/: 20 SVG-Dateien (MIT-Lizenz, stroke=currentColor)
- src/res/resources.qrc + resources_rc.py: Qt-Ressourcensystem
- src/icons.py: icon()-Hilfsfunktion mit Palette-Farb-Injection
- MainWindow, AppSettings, XslDependencyDialog, tree_manager,
  XsltParamsEditDialog, ProjectXsltParamsDialog: Icons gesetzt
- Theme-Wechsel aktualisiert Icons und Tree-Items sofort
- THIRD_PARTY_LICENSES.txt: Feather Icons (MIT) eingetragen

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 16:13:22 +02:00
info bf2db92040 Version 1.6.6: Versionsanzeige im Info-Dialog bei PyInstaller-Bundle gefixt
Im PyInstaller-Bundle fehlten .dist-info-Metadaten, wodurch importlib.metadata
keine Paketversionen liefern konnte. Lösung: DocuMentor.spec erzeugt beim Build
einen Versions-Snapshot (versions.json) der ins Bundle eingebettet wird.
license_parser.py liest diesen Snapshot im Bundle-Modus statt importlib.metadata.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 13:54:27 +02:00
info 19f7deec20 build_msi.py: Klare Fehlermeldung bei fehlendem wix-Befehl
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 13:34:30 +02:00
info 5f10b79906 Version 1.6.5: 'Bearbeiten' als Standard-Aktion im Tree-Kontextmenü
Der Menüpunkt 'Bearbeiten' steht nun ganz oben im Kontextmenü aller
Knotentypen (TreeNode, XslFile, XmlFile), wird fett dargestellt und
öffnet sich per Doppelklick auf den jeweiligen Knoten.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 13:17:01 +02:00
info 1bd20824b4 Version 1.6.4: Dep-Update-Check und Permission-Allowlist für Claude Code
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 12:41:34 +02:00
info 0d25dc4b35 Claude Code: Permission-Allowlist für read-only Befehle hinzugefügt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 12:31:56 +02:00
info ce7290d6e8 Merge branch 'master' of https://code.vitaligraf.de/info/xsl-validator 2026-05-23 16:58:45 +02:00
info e84dd6c232 version-bump Skill: uv version --bump statt direkter pyproject.toml-Bearbeitung
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 16:55:22 +02:00
info bc07c71f39 Neuer Skill: dep-update-check für Dependency- und Python-Updates
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 20:15:44 +02:00
info 47e98b88ef Neues Icon 2026-05-03 16:29:07 +02:00
info 799d3dc2ba Slogan geendert 2026-05-03 16:28:28 +02:00
info 938fc9bfeb alte Webseite gelöscht 2026-05-03 16:23:44 +02:00
info ea7fbaf8b8 README.md angepasst 2026-04-25 20:43:21 +02:00
info 9a2fb45802 Korrektur in index.html 2026-04-25 20:35:06 +02:00
info 1501f8208d Web: Datenschutz- und Impressumsseite zur Landing Page hinzugefügt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 20:25:00 +02:00
info 2b36afc451 Fix: WiX-Installationsbefehl mit expliziter Version 6.* präzisiert
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 16:12:16 +02:00
info 4ec5125ea2 Fix: Konsolenfenster bei Subprozessen unter Windows unterdrückt (v1.6.3)
subprocess.CREATE_NO_WINDOW-Flag in transform.py und worker_pool_base.py
gesetzt, damit beim Start aus einer PyInstaller-EXE keine Konsolenfenster
für Saxon, FOP und diff-pdf erscheinen.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 15:57:14 +02:00
info 0f3c0dd878 Fix: PyInstaller-EXE zeigt App-Symbol korrekt via sys._MEIPASS und datas (v1.6.2)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 15:26:09 +02:00
info 16de979524 Fix: Windows-Titelleistensymbol durch AppUserModelID und setWindowIcon korrekt gesetzt (v1.6.1)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 15:02:37 +02:00
info 394cbcf4be Refactor: Web-Landingpage mit industriellem Design neu gestaltet
Altes elegantes Dunkelmodus-Design durch ein industrielles Beton-Stahl-Theme ersetzt; nicht mehr benötigte Schriftarten-Dateien und alte HTML-Versionen entfernt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 11:24:20 +02:00
Vitali Graf ef2ec13ff8 Docs: WiX-Version in BUILD.md auf v6 fixiert und OSMF-EULA-Hinweis ergänzt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 12:47:24 +02:00
info 9fad317891 Feat: XML-Knoten-Bearbeiten-Dialog implementiert (v1.6.0)
Neuer Dialog ermöglicht es, einem XML-Knoten XSL-Zuordnungen hinzuzufügen
oder zu entfernen. XmlToXslAssignDialog wiederverwendet mit edit_mode,
Vorauswahl per preselected_xsl_ids und get_selection_diff(). Beim Entfernen
werden zugehörige PDF-Dateien gelöscht; bei verbleibend leerer Zuordnung
wird das physische Löschen der XML-Datei angeboten.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 21:10:58 +02:00
info 762523d669 Fix: Build-Skripte verwenden Versionsnummer aus pyproject.toml für Ausgabedateien (v1.5.1)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 17:23:12 +02:00
info b900455d69 Fix: Code-Qualität und Effizienz verbessern (v1.5.1)
- main.py: print() durch logging ersetzt, cleanup nach Logger-Init verschoben
- conf.py: funktionsloses global-Statement entfernt
- database.py: unerreichbaren zweiten Projekt-Check entfernt
- hash_calculation.py: deprecated _handle_xml_file_drop entfernt, nutzlosen
  _get_all_project_xml_files-Wrapper entfernt, seen_paths-Scope-Bug in
  rekursiver Traversierung behoben (O(N²) → O(N)), veraltete List[]-Syntax
  und ungenutzte Imports bereinigt
- transform.py: TOCTOU-Muster (exists+stat) durch direktes stat() mit
  FileNotFoundError ersetzt; fop_conf.exists() gecacht

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 17:02:59 +02:00
info 3d2efe628b Feat: Projekt-Baum verschlanken und XSL-Dateiname im Edit-Dialog anzeigen (v1.5.0)
Mittlere Spalte (Kontextinfos) aus dem Projekt-Baum entfernt, sodass nur noch
Bezeichnung und Diff-PDF-Anzahl angezeigt werden. XSL-Dateiname wird jetzt als
nur-lese Label oben im XslFileEditDialog angezeigt.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-04 12:38:37 +02:00
info 8c59187fe9 Feat: Suchfilter durchsucht auch XSL-Dateinamen (v1.4.1)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-04 12:02:06 +02:00
info a0626a78a3 Feat: Projektweite XSLT-Parameter mit Vererbungshierarchie (v1.4.0)
Ermöglicht die Definition von XSLT-Parametern auf Projektebene, die als
Basis für alle Transformationen dienen und von TreeNode- bzw. XslFile-
Parametern überschrieben werden können (Projekt < TreeNode < XslFile).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-04 10:58:01 +02:00
info d1def05607 Feat: Suchfilter für TreeNodes und XSL-Dateien im Hauptfenster (v1.3.0)
Neues Suchfeld über dem Baum filtert Knoten und XSL-Dateien per
case-insensitive Textsuche. Übergeordnete Knoten bleiben bei
Kind-Treffern sichtbar und werden automatisch expandiert. Der
gespeicherte Expand-Status wird beim Leeren der Suche wiederhergestellt.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 19:55:06 +02:00
info 0560dbafe4 Fix: Maximize-Button im XSL-Abhängigkeitsdialog unter Windows ermöglichen (v1.2.8)
WindowMinMaxButtonsHint gesetzt, damit der Dialog auch unter Windows maximiert werden kann.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 13:24:32 +02:00
info 7406c65bf9 Docs: Commit-Skills und Claude-Plugin-Einstellungen hinzufügen
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 20:50:16 +02:00
info 9370c03e90 Feat: Veraltete XSL-Einträge nach DB-Import erkennen und entfernen (v1.2.7)
Beim Import aus der PostgreSQL-Datenbank werden nun XSL-Einträge erkannt,
die nicht mehr in der DB vorhanden sind. Ein Dialog zeigt diese gruppiert
in einer Baumansicht an und bietet die Option, sie samt nicht mehr
verwendeter XML-/PDF-Dateien aus dem Projekt zu entfernen.
Leere TreeNodes werden automatisch bereinigt.

Zusätzlich: SQL-Filter `r3.export = 0` in data.sql ergänzt.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 20:38:36 +02:00
info d7282082f4 Fix: Version und Drittanbieter-Lizenzen im PyInstaller-Build anzeigen (v1.2.6)
pyproject.toml und THIRD_PARTY_LICENSES.txt werden nun ins PyInstaller-Bundle
eingebunden. Pfadauflösung nutzt sys._MEIPASS im Bundle-Kontext.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-28 19:32:03 +01:00
info 09312dbd66 Feat: Legende oben links und auto-fit bei Suche/Filter im Abhängigkeitsgraph (v1.2.5)
- Legende von unten links nach oben links verschoben, damit vis.js-Navigationspfeile nicht überdeckt werden
- network.fit() mit Animation bei jeder Suche und Checkbox-Änderung, sodass alle relevanten Knoten im Viewport sichtbar sind

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 21:11:14 +01:00
info 1265309b41 Anderes Icon für Eindstellungen im XSL-Dependencies-Dialog 2026-03-22 20:40:20 +01:00
info bf352a1fcd Feat: Individuelle Knoten-Styles im XSL-Abhängigkeitsgraph nach Dateistatus (v1.2.4)
Knoten im vis.js Netzwerkgraph werden nun farblich nach drei Kategorien
unterschieden: blau (nur im Verzeichnis), grün (im Projekt referenziert),
rot/gestrichelt (im Projekt, aber Datei fehlt). Inkl. Legende und
erweitertem Tooltip mit Projekt-Zugehörigkeit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 19:57:17 +01:00
info 84d0866f72 Feat: Doppelklick auf Netzwerkgraph-Knoten kopiert Dateiname ins Suchfeld (v1.2.3)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 18:46:45 +01:00
info cdca38eb98 Feat: Klickbare Webseite-Links in der AboutDialog-Abhängigkeitstabelle (v1.2.2)
URLs in der Spalte "Webseite" werden als anklickbare Links dargestellt,
die sich im Standardbrowser öffnen.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 20:01:58 +01:00
info 36d257e2e2 Fix: HTML-Tooltips im vis.js Abhängigkeitsgraph korrekt rendern (v1.2.1)
vis.js rendert String-Titles als Klartext. Node-Titles werden nun vor der
DataSet-Erstellung in DOM-Elemente konvertiert, damit HTML-Tags (<b>, <br>)
korrekt dargestellt werden.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 19:54:38 +01:00
info 4e65a6ad4c Feat: vis.js Layout-Switcher im XSL-Abhängigkeitsgraph (v1.2.0)
Layout-Umschaltung zwischen barnesHut, ForceAtlas2, Repulsion und hierarchischem
Layout mit konfigurierbaren Parametern pro Layout. Einstellungen werden persistent
in AppSettings gespeichert und beim Öffnen des Dialogs wiederhergestellt.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 19:32:29 +01:00
info f7ef90079a Feat: License-Check-Skill und lxml-Lizenzeintrag hinzugefügt (v1.1.1)
Neuer Skill prüft bei jedem Commit automatisch ob THIRD_PARTY_LICENSES.txt
mit pyproject.toml synchron ist. Fehlenden lxml-Eintrag ergänzt und
Kategorie-Spalte im AboutDialog verbreitert.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 17:20:07 +01:00
info a8b4fac085 Feat: Hilfe-Menü mit Info-Dialog und Lizenz-Parser hinzugefügt (v1.1.0)
Neues Menü "Hilfe > Info" zeigt Programmversion, Python-Version und alle
Drittanbieter-Bibliotheken mit installierten Versionen und Lizenzinfos an.
Der license_parser liest THIRD_PARTY_LICENSES.txt als Datenquelle und
ergänzt tatsächlich installierte Versionen via importlib.metadata.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 16:54:21 +01:00
info c69c163b34 Feat: Version-Bump-Skill hinzugefügt und installer.iss-Version synchronisiert
installer.iss hatte noch Version 0.1.0 statt 1.0.0. Neuer Skill fragt
bei jedem Commit, ob die Programmversion aktualisiert werden soll.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 16:02:05 +01:00
info 64408157ba Docs: AGENTS.md mit CLAUDE.md konsolidiert und Einzeldatei entfernt
Code-Style-Richtlinien (Imports, Type Annotations, Naming, Logging,
Docstrings), UI-Import-Pattern, Thread-Pattern, RAM-Optimierung und
Test-Infos aus AGENTS.md übernommen. Veraltete Einträge korrigiert
(qdarktheme entfernt, _execute_sql_query → DatabaseQueryThread,
XslDependencyDialog dokumentiert).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 15:25:10 +01:00
info 3dcbf783b1 Feat: Sidebar mit Suchfilter und lxml-Parser im XslDependencyDialog
- Ein-/ausblendbare Sidebar mit tab-übergreifender Suche hinzugefügt
- Graph-Suchfilter blendet nicht-betroffene XSL-Dateien aus dem Netzwerkgraph aus
- Regex-basierte XSL-Abhängigkeitserkennung durch lxml-Parser ersetzt
- Suchfilter wird beim Tab-Wechsel erneut angewendet

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 21:15:16 +01:00
info 48ab596476 Feat: UI-Datei für XslDependencyDialog hinzugefügt und _ui.py neu generiert
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 20:23:35 +01:00
info bd6827cb2f Docs: BUILD.md mit windows_distribution.md konsolidiert und Einzeldatei entfernt
Gesamte Windows-Distribution-Dokumentation (ZIP, Setup.exe, MSI) in BUILD.md
zusammengeführt. docs/windows_distribution.md entfernt, da redundant.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 19:37:10 +01:00
info 36911b111d Feat: Interaktiver XSL-Abhängigkeitsgraph mit vis.js und THIRD_PARTY_LICENSES aktualisiert
XslDependencyDialog mit zwei Tabs: Baumansicht (vorwärts/rückwärts-Abhängigkeiten)
und interaktiver Netzwerkgraph (vis.js in QWebEngineView mit Physics-Simulation,
Hover-Tooltips, Nachbar-Hervorhebung). Graceful Fallback wenn WebEngine fehlt.
THIRD_PARTY_LICENSES um psutil, PyInstaller, Pillow, vis-network ergänzt und
Versionen aktualisiert.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 20:50:09 +01:00
info 71fa48a514 Feat: XSL-Abhängigkeitsgraph für import/include-Erkennung in Transformations-Pipeline
is_up_to_date() prüft nun auch transitiv importierte/inkludierte XSL-Dateien.
Abhängigkeiten werden per Tooltip und Kontextmenü-Aktion im TreeWidget angezeigt.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 19:37:05 +01:00
info 140905af77 Refactor: Installationsbereich umstrukturiert und PostgreSQL-Hinweis entfernt
Reihenfolge: Windows-Pakete → Hinweis → Quellcode → Hinweis.
PostgreSQL aus den Installationshinweisen entfernt, da optional.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 18:12:09 +01:00
info 1a12da19f3 Feat: Windows-Download (MSI/ZIP) zum Installations-Bereich der Webseite hinzugefügt
Installationssektion um vorkompilierte Windows-Pakete (MSI-Installer und
portables ZIP-Archiv) erweitert, inkl. JDK-Empfehlung für Pool-Worker-Performance.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 17:43:10 +01:00
info 84bbf201d0 EInige Symbole zu den Buttons hinzugefügt 2026-03-14 17:16:27 +01:00
info 4f2d136d17 Feat: Ref-PDF automatisch im internen Viewer anzeigen wenn keine Diff-PDF vorhanden
Beim Auswählen eines XML-Knotens im Baum wird jetzt die Ref-PDF direkt im
internen Viewer geladen, sofern keine Diff-PDF existiert. Der Kontextmenü-Eintrag
"Ref-PDF öffnen" und der zugehörige Handler wurden entfernt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 17:01:19 +01:00
info 93f9bb90dd Fix: QApplication.processEvents() Anti-Pattern entfernt
_close_all_pdf_documents() führt bereits doc.close() + gc.collect() durch,
wodurch Dateihandles freigegeben werden. Das nachfolgende processEvents()
war redundant und birgt das Risiko von Re-Entrant-Events.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 20:20:36 +01:00
info 29574ce0dc Perf: PDF-Thumbnails progressiv rendern statt alle auf einmal
Placeholder-Labels werden sofort erstellt, das eigentliche Rendern
erfolgt asynchron über QTimer.singleShot(0) — ein Thumbnail pro
Event-Loop-Iteration. UI friert nicht mehr ein; RAM-Spitze wird verteilt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 20:19:42 +01:00
info 87b2d9273f Refactor: Worker-Pool-Zugriff über öffentliche Getter statt private Modul-Globals
get_saxon_worker_pool() und get_fop_worker_pool() in transform.py hinzugefügt.
worker_pool.py greift nicht mehr direkt auf transform._saxon_worker_pool /
transform._fop_worker_pool zu, sondern verwendet die Getter/Setter konsistent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 20:18:01 +01:00
info cfbdb8b7fa Perf: Tool-Konfigurationsauflösung in _create_transformation_job gecacht
Die 5 linearen Lookups (java_vm, saxon_jar, apache_fop, diff_pdf, xsl_dir)
werden jetzt nur einmalig pro Projekt aufgelöst (_get_cached_project_tools).
Bei Batch-Transformationen entfällt das wiederholte Durchsuchen der Listen.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 20:11:58 +01:00
info edfe424a6a Perf: O(n²) Baumtraversierung bei Dateinamen-Konfliktprüfung behoben
_is_filename_used_in_project wurde bei jedem while-Schleifen-Durchlauf
aufgerufen und traversierte den kompletten Baum neu. Ersetzt durch
_collect_project_filenames(), die einmalig ein Set aufbaut → O(1) Lookups.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 20:09:10 +01:00
info b954106a0d Refactor: Stringly-typed Knotentypen durch ItemType-Enum ersetzt
ItemType(Enum) mit TREE_NODE, XSL_FILE, XML_FILE, UNKNOWN statt
String-Konstanten in tree_manager.py. Verbessert Typsicherheit und IDE-Support.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 20:08:23 +01:00
info 22bc693d4d Perf: Polars-Import lazy gemacht (nur bei DB-Nutzung laden)
Polars wird nicht mehr beim App-Start importiert, sondern erst wenn
eine Datenbankabfrage tatsächlich ausgeführt wird. Beschleunigt den
Kaltstart der Anwendung.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 20:05:48 +01:00
info 9be6ac24e9 Refactor: AppSettings CRUD-Methoden durch generische Helfer vereinfacht
4 generische Helfer (_add_item, _remove_item, _edit_item, _update_remove_button)
ersetzen 26 nahezu identische CRUD-Methoden für 7 Tool-Typen.
_make_centered_item eliminiert QTableWidgetItem-Duplikation in populate-Methoden.
Ergebnis: 805 → 508 Zeilen (-37%).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 19:53:34 +01:00
info 37ebdff349 Refactor: Gemeinsame Basisklassen für Worker-Pools und Parameter-Dialoge
- BaseWorkerPool (worker_pool_base.py): Eliminiert ~450 Zeilen Duplikation
  aus saxon_pool.py, saxon_pool_s9api.py und fop_pool.py; behebt stderr-Handle-Leak
- XsltParamsEditDialog (XsltParamsEditDialog.py): Gemeinsame Basisklasse für
  TreeNodeEditDialog und XslFileEditDialog; reduziert je 162 auf 8 Zeilen

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 19:49:57 +01:00
info cb90f9e483 Refactor: Code-Duplikation reduziert und Dead Code entfernt
- blake2b-Hash-Berechnung in zentrale Utility-Funktion extrahiert (src/utils.py) mit chunk-basiertem Hashing für bessere RAM-Effizienz
- _transform_all_xml_files und _transform_all_xml_files_force zu einer Methode mit force-Parameter zusammengeführt
- Project-Lookup-Methoden (getXsl, getJavaVm, etc.) über gemeinsame _lookup()-Hilfsmethode konsolidiert
- Duplizierte XML-Sammel-Methoden entfernt, Set-basierte Duplikatsprüfung eingeführt
- Ungenutzte Imports, Dead Code und wirkungslose Ausdrücke entfernt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 20:21:02 +01:00
info 6fe61b9a42 Kleine fix für die Webseite 2026-03-07 20:10:59 +01:00
info ec9ac22296 Fonts werden lokal gespeichert. 2026-03-03 21:20:02 +01:00
info d5c035a316 Neue Seiten für den Projekt 2026-03-03 20:47:56 +01:00
info affba2a9ca Fix: PyInstaller-Bundle für installierte Version repariert (connectorx, SQL-Ressourcen)
- connectorx via collect_all() eingebunden statt hiddenimports (Rust-PYD + __init__.py + Metadaten als Einheit)
- SQL/CSV-Ressourcen (src/res/) ins PyInstaller-Bundle aufgenommen
- Pfadauflösung in database.py auf sys._MEIPASS umgestellt für installierten Modus
- connectorx als explizite Abhängigkeit in pyproject.toml ergänzt
- Dokumentation (windows_distribution.md) um collect_all-Pattern und _MEIPASS-Hinweise erweitert
- Version auf 1.0.0 aktualisiert, Hersteller-Informationen ergänzt
2026-02-15 19:51:58 +01:00
info ec33a5b586 Feature: Timeout-Einstellung und asynchrone DB-Abfrage mit Abbrechen-Dialog
DB-Abfragen laufen nun in einem Hintergrund-Thread mit QProgressDialog,
sodass die UI nicht mehr einfriert. connect_timeout wird als konfigurierbarer
Parameter (1-300s, Standard: 10) im Connection-String übergeben.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 21:31:40 +01:00
info 66496c26d8 Fix: Projektname wird beim Bearbeiten im PdfProject-Dialog gespeichert
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 20:53:00 +01:00
info a0bc55fa7b Refactor: TreeWidget-Styling in Qt Designer UI-Dateien verlagert
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 21:17:21 +01:00
info b985e1eeee Feature: Löschen-Funktion für TreeNode-Knoten im Baum implementiert
Rekursive Löschung von TreeNodes mit PDF-Bereinigung, automatischer
physischer Löschung nicht mehr verwendeter XML-Dateien und korrekter
"anderswo verwendet"-Prüfung durch vorheriges Entfernen aus dem
Datenmodell.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 21:08:23 +01:00
info 52180e38ce Feature: Löschen-Funktion für XSL-Knoten im Baum implementiert
Ersetzt den bisherigen Stub durch eine vollständige Implementierung mit
Bestätigungsdialog, automatischer PDF-Bereinigung, optionaler physischer
XML-Löschung und Datenmodell-Aktualisierung.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 20:12:24 +01:00
info 29f34c4d85 Refactor: projectPath-Widget in Qt Designer verlagert
- projectPath-Label jetzt vollständig in MainWinddow.ui definiert
- StyleSheet (padding, font-weight) in UI-Datei integriert
- Dynamische Widget-Erstellung aus MainWindow.py entfernt (9 Zeilen Code)
- MainWinddow_ui.py automatisch neu generiert mit Styling
2026-02-07 20:35:17 +01:00
info 747c6dbfec Feature: Projektpfad-Anzeige über TreeWidget hinzugefügt 2026-02-07 20:25:21 +01:00
info 9180fc342e Feature: Häkchen im Menü 'Vorhandene Projekte' für geladenes Projekt 2026-02-07 20:00:50 +01:00
info 9e58b3a9a1 Feature: Automatische PDF-Bereinigung beim Entfernen von XML-Dateien
Implementiert intelligente Löschlogik die zugehörige PDF-Dateien (new/ref/diff) automatisch entfernt wenn eine XML-Datei aus einem XSL-Knoten gelöscht wird. PDFs werden nur gelöscht wenn die XML+XSL-Kombination nicht mehr anderswo im Projektbaum verwendet wird.
2026-02-07 19:47:02 +01:00
info 0fd0703dbb Feature: Expand-Status beim Projektwechsel automatisch speichern
- open_existing_project() speichert nun das vorherige Projekt vor dem Wechsel
- Expand-Status der aufgeklappten Tree-Knoten bleibt beim Projektwechsel erhalten
- Umfassendes Logging für Debugging
- Fehlerbehandlung für robuste Ausführung
2026-02-01 15:51:50 +01:00
info e6b2743677 Feature: Expand-Status von Tree-Knoten bei jedem Speichern persistent sichern
- ProjectData um optionales Feld 'expanded_nodes' erweitert (abwärtskompatibel)
- _save_project_settings() speichert nun automatisch den Expand-Status
- Expand-Status wird bei allen Speicheroperationen gesichert:
  * Beim Bearbeiten von TreeNodes und XslFiles
  * Bei Drag&Drop-Operationen im Tree
  * Bei Hash-Berechnungen für XML-Dateien
  * Beim Laden von Daten aus der Datenbank
  * Beim Beenden der Anwendung
- Beim Laden eines Projekts werden aufgeklappte Knoten wiederhergestellt
- Rekursive Speicherung und Wiederherstellung für TreeNode und XslFile
- Umfassendes Logging für Debugging und Fehlerbehandlung
2026-02-01 15:44:55 +01:00
info 0f14418749 UI: PDF-Thumbnails kompakt und zentriert anzeigen
- Thumbnails horizontal zentriert im ScrollArea-Layout
- Layout-Spacing auf 5px reduziert für kompakte Darstellung
- Seitennummer-Labels auf 18px Höhe begrenzt
- Ränder um Layout und Labels entfernt
- Expandierenden Spacer am Ende hinzugefügt, damit Thumbnails oben bleiben
- Verbesserte Übersichtlichkeit der Thumbnail-Navigation
2026-02-01 15:06:22 +01:00
info 3bdc0a0daa Feature: Aktions-Menü mit Batch-Transformationen und UI-Bereinigung
- Neues Aktions-Menü mit Transformations- und Datenbankfunktionen
- Menü wird beim Projekt-Laden automatisch aktiviert
- Neue Aktion: Alle XML-Dateien transformieren (inkrementell)
- Neue Aktion: Alle XML-Dateien neu transformieren (force)
- Neue Aktion: Aus Datenbank laden (ersetzt Button)
- Entfernte obsolete Buttons (pushButton, pushButton_2, pB_lade_aus_fn2)
- UI-Bereinigung: Button-Frame unterhalb TreeWidget entfernt
- Batch-Transformationen sammeln rekursiv alle XML/XSL-Paare
- Bestätigungsdialoge mit Job-Anzahl und Warnungen
- Deutsche Log-Meldungen und Fehlertexte
2026-01-25 15:23:32 +01:00
info 2858d46ef1 Refactor: Worker-Log-Verzeichnis von 'temp' zu 'tmp' umbenennen 2026-01-23 19:38:57 +01:00
info afff427f67 Feature: Prüfung auf Existenz von XML- und XSL-Dateien vor Transformation
- Prüfe ob XML- und XSL-Dateien existieren bevor TransformationJobs erstellt werden
- Zeige detaillierte Fehlermeldungen wenn Dateien fehlen
- Zähle und melde übersprungene Jobs bei Batch-Verarbeitung
- Verhindere mehrfache Fehlerdialoge durch zentrale Fehlerbehandlung
- Logge fehlende Dateien mit vollständigen Pfaden für einfaches Debugging
2026-01-22 19:37:53 +01:00
info d74dfd20e5 Fix: QIcon.ThemeIcon.TextXGeneric durch korrekten String-Namen ersetzen 2026-01-22 19:33:15 +01:00
info bad4d55ebd Feature: Automatische Aktualisierung des Projekte-Menüs und Validierung der Projekt-Verzeichnisse
- AppSettings: Menü-Update nach jedem app_settings.save() Aufruf
- MainWindow: Prüfung der Projekt-Verzeichnis-Existenz beim Start
- Nur gültige Projekte werden im Menü angezeigt
- Ungültige Projekte werden ausgeblendet mit Logging-Warnung
- Memory-Leak-Prävention durch korrektes Aufräumen alter Menüs
2026-01-20 20:29:34 +01:00
info c8e1a541cd Fix: Log-Verzeichnis mit parents=True erstellen, um FileNotFoundError zu vermeiden 2026-01-20 19:50:42 +01:00
info df1bb65136 Refactor: Entferne ungenutzten datetime-Import in cleanup_old_logs 2026-01-18 19:08:12 +01:00
info 43bd0ec8e6 Füge WiX v6-kompatiblen MSI-Installer hinzu
- Erstelle generate_wix_files.py zum Ersetzen von 'wix heat'
- Migriere DocuMentor.wxs auf WiX v4/v6-Syntax
- Füge build_msi.py für automatisierten Build hinzu
- Aktualisiere Dokumentation für WiX v6
- Erweitere .gitignore für WiX-Artefakte

WiX v6 hat das 'heat' Tool entfernt, daher wurde ein Python-Skript
erstellt, das automatisch alle Dateien aus dist/DocuMentor harvested
und eine WiX-konforme ProductFiles.wxs generiert.

Der neue Build-Prozess:
1. uv run python build_windows.py
2. uv run python generate_wix_files.py
3. wix build DocuMentor.wxs ProductFiles.wxs -o DocuMentor.msi

Oder vereinfacht: uv run python build_msi.py
2026-01-18 19:01:05 +01:00
info c3d1bbc74c Docs: WiX MSI-Installer Anleitung zu Windows Distribution hinzugefügt
- Vollständige WiX-Konfiguration (DocuMentor.wxs)
- Heat-Tool für automatisches File Harvesting
- Build-Automatisierung mit build_msi.py
- Silent Installation und GPO-Deployment
- MSI-Vorteile vs. Inno Setup detailliert erklärt
2026-01-18 17:58:37 +01:00
info b1f259e443 Build: Ungenutzte pyqtdarktheme-Dependency aus PyInstaller-Config entfernt 2026-01-18 17:55:17 +01:00
info 70c12a91dc Merge: Branch 'dist' in 'master' integriert
- Windows-Distribution-Infrastruktur (PyInstaller, Inno Setup)
- FOP Worker Pool für Performance-Steigerung
- PDF-Viewer Zoom-Feature
- Performance-Einstellungen in UI
- Build-Dokumentation und Icon-Ressourcen

Konflikte gelöst:
- pyproject.toml: Neueste ruff-Version beibehalten, Build-Dependencies hinzugefügt
- uv.lock: Automatisch neu generiert
2026-01-18 16:59:48 +01:00
info 5ab8674833 Feature: Icons für TreeNodes, XSL-Nodes und XML-Nodes hinzufügen
Fügt visuelle Icons für bessere Übersichtlichkeit im TreeWidget hinzu:
- TreeNode: Ordner-Icon (folder-open mit Fallback)
- XslFile: Script/Code-Icon (text-x-script)
- XmlFile: XML-Dokument-Icon (text-xml)

Icons verwenden QIcon.ThemeIcon für bessere Kompatibilität und
System-Theme-Integration mit automatischen Fallbacks.
2026-01-18 13:48:20 +01:00
info f4d2d4b944 Docs: Entwicklerrichtlinien für KI-Coding-Agenten hinzufügen
Fügt AGENTS.md mit umfassenden Richtlinien für Coding-Agenten hinzu:
- Sprachkonventionen (Deutsch)
- Code-Style & Type Annotations
- Build-/Test-Kommandos mit uv
- PySide6 UI-Integration
- Pydantic Models & Settings
- Import-Organisation & Error Handling
2026-01-18 13:40:15 +01:00
info 8a37992bea Feature: XSL-Knoten bei fehlenden Dateien deaktivieren
Beim Laden des Projekts werden nun auch XSL-Knoten automatisch deaktiviert (ausgegraut), wenn die entsprechende XSL-Datei nicht im XSL-Verzeichnis vorhanden ist.

Zusätzlich werden alle untergeordneten XML-Knoten ebenfalls deaktiviert, wenn die übergeordnete XSL-Datei fehlt. Dies verhindert, dass Transformationen mit fehlenden XSL-Dateien gestartet werden und gibt sofortige visuelle Rückmeldung.

- XSL-Knoten werden mit setDisabled(True) deaktiviert
- Tooltip zeigt den vollständigen Pfad der fehlenden XSL-Datei
- Alle untergeordneten XML-Knoten werden ebenfalls deaktiviert
- Warnung wird ins Log geschrieben

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-17 20:29:29 +01:00
info 91fe597301 Feature: XML-Knoten bei fehlenden Dateien deaktivieren
Beim Laden des Projekts in den TreeWidget werden nun XML-Knoten automatisch deaktiviert (ausgegraut), wenn die entsprechende XML-Datei nicht im Projekt vorhanden ist. Dies verbessert die Benutzerfreundlichkeit durch sofortige visuelle Rückmeldung über fehlende Dateien.

- XML-Knoten werden mit setDisabled(True) deaktiviert
- Tooltip zeigt den vollständigen Pfad der fehlenden Datei
- Warnung wird ins Log geschrieben

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-17 20:22:42 +01:00
info f91ffd83a2 Fix: Accept-Changes-Button beim PDF-Reset deaktivieren
Beim Zurücksetzen der PDF-Ansicht wird nun auch der "Änderungen akzeptieren" Button deaktiviert, um einen konsistenten UI-Zustand zu gewährleisten.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-17 20:08:54 +01:00
info ee9c40610a Feature: Automatische Bereinigung alter Log-Dateien beim Start
Beim Anwendungsstart werden nun automatisch Log-Dateien gelöscht, die älter als 24 Stunden sind. Dies hilft, Speicherplatz zu sparen und das Log-Verzeichnis übersichtlich zu halten.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-17 19:52:41 +01:00
info 3acdfbb5c8 Refactor: MainWindow in 7 Mixins aufgeteilt (80% Code-Reduktion)
MainWindow.py von 5025 auf 983 Zeilen reduziert durch Extraktion in:
- TreeManagerMixin: Baumstruktur-Verwaltung (~1136 Zeilen)
- PdfViewerMixin: PDF-Anzeige und Rendering
- WorkerPoolMixin: Saxon/FOP Worker-Pool-Verwaltung
- DatabaseMixin: PostgreSQL-Operationen
- DragDropMixin: Drag-and-Drop für XML-Dateien
- HashCalculationMixin: blake2b Hash-Berechnung
- TransformationMixin: XSL-Transformationen

Zusätzlich Thread-Klassen in threads.py ausgelagert.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 18:23:55 +01:00
info 5a2da7f264 Feature: Ref-PDF direkt aus Kontextmenü öffnen
- Neue Kontextmenü-Aktion "Ref-PDF öffnen" für XML-Dateien
- Aktion nur aktiv wenn Ref-PDF existiert und keine Diff-PDF vorhanden
- Öffnet Ref-PDF im System-PDF-Viewer via QDesktopServices
- Handler-Methode _open_ref_pdf_for_xml_file() mit Fehlerbehandlung

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-11 16:30:09 +01:00
info 4eb80c92b2 Update: Abhängigkeiten aktualisiert und pyqtdarktheme entfernt
- Python-Version auf >=3.13,<3.15 eingeschränkt
- pydantic-settings 2.9.1 → 2.12.0
- pyside6 6.9.1 → 6.10.1
- polars 1.31.0 → 1.37.0
- pydantic-yaml 1.5.1 → 1.6.0
- ruff 0.14.8 → 0.14.11
- pyqtdarktheme entfernt

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-11 16:14:58 +01:00
info 1558c376d1 Fix: Worker-Pool-Metriken cachen für Anzeige nach Shutdown
Problem:
- Worker-Pools werden nach Transformation beendet (Lazy Loading)
- Metriken waren danach nicht mehr verfügbar
- Metriken-Dialog zeigte "Nicht aktiviert"

Lösung:
- Metriken werden vor Pool-Shutdown mit deepcopy() gespeichert
- MainWindow speichert last_saxon_metrics und last_fop_metrics
- WorkerPoolMetricsDialog zeigt gecachte Metriken an
- Neue Methode: _update_metrics_tab_from_metrics()

Verhalten:
- Metriken bleiben bis zur nächsten Transformation verfügbar
- Dialog zeigt "Letzte Transformation" statt "Aktiviert"
- Ohne Metriken: Hinweis "bitte erst eine Transformation durchführen"

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-07 19:00:35 +01:00
info f59e51c081 Performance: Lazy Worker-Pool Init + XSL-Stylesheet-Caching
RAM-Optimierung (Lazy Loading):
- Worker-Pools werden erst bei Transformation gestartet (nicht beim Projekt-Öffnen)
- Worker-Pools werden nach Transformation automatisch beendet
- RAM im Ruhezustand: 0 MB (vorher: ~1.2 GB)
- Temporäre Verzeichnisse werden sauber aufgeräumt

XSL-Stylesheet-Caching (Massive Performance-Steigerung):
- Saxon s9api: HashMap<String, XsltExecutable> Cache
- Saxon JAXP: HashMap<String, Templates> Cache
- Kompilierte Stylesheets werden pro Worker wiederverwendet
- Bei 82 Transformationen mit 8 XSL-Dateien:
  * 1. Durchlauf: 8× Kompilierung
  * Weitere 74×: Cache-Treffer (sehr schnell!)

Technische Details:
- Worker-Pool-Init verschoben von _on_project_opened zu _start_transformation
- Worker-Pool-Shutdown in _on_all_transformations_finished
- Java-seitiger HashMap-Cache für beide Saxon-Varianten
- Cache-Logging für Debugging

Perfekt für Dauerbetrieb im Hintergrund!

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-07 18:44:32 +01:00
info d3dc07cbf3 Feature: Detaillierte Worker-Pool Performance-Metriken mit psutil
Neue Metrik-Erfassung für Saxon- und FOP-Worker-Pools:
- Kompilierungszeit der Java-Worker-Klassen
- Worker-Startzeiten (Summe + Durchschnitt pro Worker)
- RAM-Verbrauch vor/nach Transformation (Summe + Durchschnitt)
- Automatische Berechnung der RAM-Zunahme in MB und Prozent

Technische Details:
- Neue WorkerPoolMetrics-Datenklasse in worker_metrics.py
- RAM-Messung via psutil (v7.2.1, neu hinzugefügt)
- Metriken für beide Saxon-Varianten (JAXP + s9api)
- WorkerPoolMetricsDialog mit Tab-basierter UI
- Menüeintrag "Projekt → Worker-Pool-Metriken"

Metriken werden automatisch erfasst:
- Bei Worker-Pool-Initialisierung (Kompilierung + Start)
- Vor erster Transformation (RAM-Baseline)
- Nach allen Transformationen (RAM-Endwert)

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-06 20:58:37 +01:00
info cfbdc476fa Docs: Nutzungsszenario und Workflow in CLAUDE.md dokumentiert
Neuer Abschnitt "Anvisiertes Nutzungsszenario" erklärt:
- Einsatz in Flexnow für PDF-Dokumente (Urkunden, Zeugnisse, Bescheide)
- Struktur der ~100 verknüpften XSL-Dateien
- Typischer Entwicklungs-Workflow mit PDF-Diff-Prüfung
- Wichtigkeit von RAM-sparsamem Design

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-06 20:12:07 +01:00
info cbcae3222f Feature: s9api-basierte SaxonWorkerPool-Variante für XSLT 2.0/3.0
Die JAXP-basierte SaxonWorkerPool-Implementierung ist nur für XSLT 1.0
vollständig spezifiziert und kann bei XSLT 2.0/3.0 zu fehlerhaften
Ausgaben führen.

Änderungen:
- Neue SaxonWorkerPoolS9Api-Klasse mit Saxon s9api für XSLT 2.0/3.0
- XsltVersion-Enum in conf.py (XSLT_1_0, XSLT_2_0_3_0)
- ComboBox in Performance-Einstellungen zur XSLT-Version-Auswahl
- MainWindow wählt automatisch richtige Worker-Pool-Variante
- Verbesserte Classpath-Behandlung und Fehlerbehandlung

Standard-Einstellung: XSLT 2.0/3.0 (s9api) - empfohlen für moderne Stylesheets
Fallback: XSLT 1.0 (JAXP) - verfügbar für Legacy-Stylesheets

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-05 20:20:00 +01:00
info bb7cad9204 Build: Vollständige Windows-Distribution-Infrastruktur
Implementiert ein professionelles Build-System für Windows-Benutzer ohne Python-Installation:

PyInstaller-Integration:
- DocuMentor.spec mit automatischer Icon/Version-Einbindung
- Unterstützung für alle PySide6-UI-Dateien und Dependencies
- UPX-Kompression für kleinere Executable-Größe

Icon-System:
- create_icon.py generiert Standard-Icon oder konvertiert PNG zu ICO
- Multi-Size ICO (16x16 bis 256x256) für alle Windows-Kontexte
- Automatische Integration in Build-Prozess
- Prompts für Bild-KIs (Gemini, DALL-E, etc.)

Versionsinformationen:
- create_version_info.py liest Version aus pyproject.toml
- Windows-Datei-Eigenschaften (Rechtsklick → Details)
- Automatische Generierung bei jedem Build

Build-Automatisierung:
- build_windows.py orchestriert gesamten Build-Prozess
- Erstellt Icon und Versionsinformationen automatisch
- Generiert ZIP-Archiv für Distribution
- Cleanup alter Builds

Inno Setup-Integration:
- installer.iss für professionelle Setup.exe
- GUID-Generator (generate_guid.py)
- Desktop-Verknüpfungen und Start-Menü-Integration

Dokumentation:
- BUILD.md - Schnellstart-Anleitung
- docs/windows_distribution.md - Detaillierte Distribution-Dokumentation
- docs/icon_and_version_info.md - Icon- und Versions-System
- resources/icon_prompt.md - KI-Prompts für Icon-Generierung

Dependencies:
- pyinstaller>=6.0.0 für Executable-Erstellung
- pillow>=10.0.0 für Icon-Generierung

Externe Abhängigkeiten (Java, FOP, Saxon, diff-pdf) bleiben separat installierbar.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-04 20:37:30 +01:00
info 6976d21768 UX: Performance-Einstellungen in Programmeinstellungen integriert
Performance-Einstellungen wurden vom separaten Menüeintrag in den
Programmeinstellungen-Dialog als eigener Tab verschoben:

- Neuer "Performance"-Tab in AppSettings.ui mit drei Konfigurationsbereichen:
  • ThreadPoolExecutor: Worker-Anzahl (1-32, Standard: 8)
  • SaxonWorkerPool: Toggle für persistente JVM-Prozesse
  • FopWorkerPool: Toggle für persistente JVM-Prozesse

- AppSettings.py erweitert:
  • _populate_performance_tab(): Lädt aktuelle Performance-Einstellungen
  • accept(): Speichert Performance-Einstellungen in app_settings

- MainWindow.py bereinigt:
  • _setup_performance_menu() entfernt
  • _open_performance_settings() entfernt
  • Separater Menüeintrag im Projekt-Menü entfernt

- AppSettings_ui.py mit pyside6-uic neu generiert

Vorteile: Alle Programmeinstellungen sind nun zentral an einem Ort
verfügbar, bessere Benutzererfahrung durch konsistente UI-Struktur.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-04 19:01:46 +01:00
info a7a69d27d7 Performance: FOP Worker Pool für 5-10x schnellere PDF-Generierung
Implementiert persistente JVM-Prozesse für Apache FOP analog zum bestehenden
SaxonWorkerPool-System. Eliminiert JVM-Startup-Overhead durch Wiederverwendung
von Worker-Prozessen.

Änderungen:
- Neues Modul fop_pool.py mit FopWorkerPool und Java Worker-Klasse
- Integration in transform.py mit automatischem Fallback auf subprocess
- GUI-Einstellungen für FOP Worker Pool (aktivieren/deaktivieren)
- Automatische Neuinitialisierung bei Einstellungsänderungen
- Konfiguration: use_fop_worker_pool in AppSettings (Standard: aktiviert)

Performance: 5-10x schnellere PDF-Generierung bei vielen kleinen PDFs durch
Wiederverwendung von FopFactory und Font-Caches.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-04 17:24:19 +01:00
info 8d38c58d6f Feature: Zoom per STRG+Mausrad im PDF-Viewer
Neue Zoom-Funktion für bessere Bedienung:
- STRG+Mausrad rauf: Zoom vergrößern (+10% pro Schritt)
- STRG+Mausrad runter: Zoom verkleinern (-10% pro Schritt)
- Respektiert Slider-Grenzen (25% bis 300%)
- Funktioniert nur wenn PDF geladen ist (Slider aktiviert)

Implementierung:
- Event-Filter für scrollArea_2 (PDF-Viewer)
- eventFilter() Methode fängt Wheel-Events ab
- Prüft auf ControlModifier (STRG-Taste)
- Aktualisiert Zoom-Slider (triggert automatisch apply_zoom)

Technische Details:
- _setup_scroll_area_zoom() in __init__
- installEventFilter(self) für scrollArea_2
- QEvent.Type.Wheel mit Qt.KeyboardModifier.ControlModifier
- Zoom-Schrittweite: 10%

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-02 21:06:37 +01:00
info c55a628728 UX-Verbesserung: Slider-Aktivierung und aussagekräftige Tooltips
Slider (Alpha und Zoom) werden erst aktiviert, wenn PDF geladen:
- Initial deaktiviert (disabled=false in UI)
- Automatische Aktivierung beim Laden von Diff-PDFs
- Automatische Deaktivierung beim Leeren des Viewers

Verbesserte Tooltips für bessere Benutzerführung:
- Alpha-Slider: "Blendet zwischen Referenz-PDF (links) und neuer PDF (rechts) um. Doppelklick setzt auf Mitte zurück."
- Zoom-Slider: "Vergrößert oder verkleinert die PDF-Ansicht (25% bis 300%). Doppelklick setzt auf 100% zurück."

Implementierung:
- MainWinddow.ui: enabled=false für beide Slider
- MainWindow.py: Aktivierung in _load_pdf_for_comparison()
- MainWindow.py: Deaktivierung in _clear_pdf_viewer()

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-02 20:22:29 +01:00
info d914e9b06a Feature: Buttons zum Öffnen von Ref- und New-PDFs im System-Viewer
Neue Buttons im PDF-Vergleichs-Viewer:
- "Vorher (Referenz)" Button öffnet Referenz-PDF im System-PDF-Viewer
- "Nachher (Neu)" Button öffnet neue PDF im System-PDF-Viewer
- Beide Buttons sind initial deaktiviert
- Automatische Aktivierung beim Laden von Diff-PDFs
- Automatische Deaktivierung beim Leeren des Viewers

Implementierung:
- Neue Instanzvariablen: current_ref_pdf_path, current_new_pdf_path
- Handler-Methoden: _on_view_ref_pdf_clicked(), _on_view_new_pdf_clicked()
- QDesktopServices.openUrl() für plattformunabhängiges Öffnen
- Fehlerbehandlung und Logging

UI-Änderungen:
- Buttons in MainWinddow.ui hinzugefügt (view_ref_pdf, view_new_pdf)
- MainWinddow_ui.py automatisch generiert

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-02 20:11:56 +01:00
info b29fa633cb Lizenzierung: MIT License und Dokumentation
Projekt unter MIT License veröffentlicht:
- LICENSE-Datei mit MIT-Lizenztext
- LICENSES.md mit detaillierter Lizenzanalyse aller Dependencies
- THIRD_PARTY_LICENSES.txt mit allen verwendeten Bibliotheken
- README.md mit vollständiger Projektdokumentation
- pyproject.toml mit Lizenz-Metadaten aktualisiert

Lizenz-Übersicht:
- PySide6: LGPL-3.0/GPL-2.0/GPL-3.0 (kompatibel mit MIT)
- Pydantic, Polars, pyqtdarktheme: MIT
- PyArrow: Apache 2.0
- Saxon-HE: MPL-2.0 (externes Tool)
- Apache FOP: Apache 2.0 (externes Tool)

Keine Einschränkungen durch Dependencies - MIT License möglich.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-02 17:40:03 +01:00
info 12fe395ac0 Feature: Präsentationswebseite für DocuMentor
Statische Webseite zur Vorstellung der Anwendung mit:
- Hauptseite mit Features, Workflow, Technologie und Download-Bereichen
- Datenschutzerklärung (speziell für Desktop-App)
- Impressum-Vorlage
- Modernes Dark-Theme Design mit Gradient-Akzenten
- Vollständig responsiv (Desktop, Tablet, Mobile)
- Interaktive JavaScript-Features (Smooth Scrolling, Animationen)
- 8 Feature-Cards mit Hauptfunktionalitäten
- Drei-Panel PDF-Vergleich, Batch-Verarbeitung, Duplikatserkennung
- Installation und Download-Bereich für alle Plattformen

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-02 17:39:23 +01:00
info d7fbb178a7 Feature: Konfigurierbare SaxonWorkerPool-Aktivierung
Performance-Dialog erweitert um Checkbox zur Aktivierung/Deaktivierung des SaxonWorkerPool.
Benutzer können jetzt zwischen Worker-Pool (schnell, benötigt JDK) und Fallback-Modus (robust, nur JRE) wählen.

Änderungen:
- Neue Einstellung 'use_saxon_worker_pool' in AppSettings (Standard: aktiviert)
- Erweiterter Performance-Dialog mit zwei Sektionen (ThreadPoolExecutor + SaxonWorkerPool)
- Pool-Initialisierung prüft nun Einstellung vor Worker-Start
- Aktualisiertes Menü-Tooltip zeigt beide Einstellungen

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-28 17:33:00 +01:00
info d0cdcd6432 Performance: 4x schnellere XSLT-Transformationen durch Worker-Pool
Problem: 82 XML-Dateien brauchten 160 Sekunden (JVM-Startup-Overhead)

Lösung: Persistente JVM-Worker-Prozesse mit JAXP Transformer API
- Saxon Worker Pool mit N persistenten JVM-Prozessen
- Eliminiert JVM-Startup und Classpath-Scanning bei jedem Job
- Parallele Verarbeitung mit ThreadPoolExecutor
- JAXP Transformer API (javax.xml.transform) - stabil, kein System.exit()
- Konfigurierbare Worker-Anzahl über Performance-Menü

Ergebnis: 82 Dateien in 40 Sekunden (4x Speedup, ~0.49s pro Datei)

Zusätzliche Verbesserungen:
- Dual-Logging (Datei + Konsole) mit Timestamps
- Worker-stderr-Logs in Projektverzeichnis/temp/
- Umfangreiche Debug-Ausgaben für Fehlerdiagnose
- Robuste Fehlerbehandlung mit ErrorListener

Technische Details:
- SaxonWorkerPool: Verwaltet N Worker-Prozesse
- JAXP statt Transform.main() (kein System.exit!)
- Worker-Locks für thread-sichere Job-Verteilung
- Graceful Shutdown mit EXIT-Befehl
- Fallback auf subprocess bei Pool-Fehlern

Dateien:
- src/saxon_pool.py (NEU): Worker-Pool-Implementation
- src/transform.py: Integration mit Worker-Pool
- src/ui/MainWindow.py: Pool-Initialisierung, Performance-Menü
- src/conf.py: max_workers Einstellung
- src/main.py: Dual-Logging

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-28 16:46:39 +01:00
info 055428e8cf Code-Qualität: Robustere Prüfung für project_dir in Batch-Verarbeitung
Fügt zusätzliche Sicherheitsprüfung hinzu, bevor project_dir verwendet wird:
- Verhindert AttributeError wenn self.project None ist
- Konsistent mit anderen Stellen im Code (Zeilen 2578, 3040, 3162)
- Behebt Pylance Type-Checking-Warnung
- Zeigt benutzerfreundliche Fehlermeldung statt Absturz

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-28 13:06:12 +01:00
info 9f48a0d62a UX-Verbesserung: Gesamtdauer in Transformations-Zusammenfassung
Erweitert die Zusammenfassung nach Abschluss aller Transformationen um die Gesamtdauer:
- TransformationThread misst jetzt die Gesamtdauer aller Jobs
- Signal all_jobs_finished erweitert um total_duration Parameter
- Statusbar und MessageBox zeigen Gesamtdauer an (Format: "12.34s")
- Dauer wird sowohl bei Erfolg als auch bei Fehlern angezeigt

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-28 12:58:39 +01:00
info e7408eac7c Performance-Verbesserung: Asynchrone Batch-Verarbeitung und Progressbars
Änderungen:
- XML-Batch-Processing in separaten Thread verlagert (XmlBatchProcessingThread)
  • Verhindert UI-Freezing bei vielen XML-Dateien
  • Hash-Berechnung, Duplikatserkennung und Dateikopieren asynchron
  • Live Progress-Updates an Hauptthread
- Progressbar für XML-Batch-Verarbeitung in Statusbar
  • Zeigt "X/Y Dateien" während Verarbeitung
  • Aktueller Dateiname in Statusbar-Text
  • Automatisches Verstecken nach Abschluss
- Progressbar für Transformationen in Statusbar hinzugefügt
  • Zeigt "X/Y Jobs" während Transformation
  • Live-Update nach jedem abgeschlossenen Job
  • Funktioniert bei Erfolg und Fehlern
- Zusammenfassungsdialog am Ende statt einzelner Erfolgsdialoge

Technische Details:
- Neue Thread-Klasse mit Signalen für Progress-Updates
- Progressbar-Management-Methoden für beide Operationen
- Signal-Handler angepasst für Live-Updates
- Statistik-Sammlung für detaillierten Zusammenfassungsdialog

UX-Verbesserungen:
- UI bleibt während Verarbeitung reaktionsfähig
- Benutzer sieht Gesamtfortschritt in Echtzeit
- Kein wiederholtes Klicken auf OK-Dialoge mehr
- Transparente Anzeige laufender Operationen

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-27 20:31:54 +01:00
info e4b2272e61 UX-Verbesserung: Batch-Verarbeitung für Drag&Drop von XML-Dateien
- Checkbox 'Alle XML-Dateien zuordnen' im XmlToXslAssignDialog hinzugefügt
- Bei aktivierter Checkbox wird Dialog nur einmal angezeigt und Auswahl auf alle weiteren Dateien angewendet
- Einzelne Erfolgsdialoge durch einen zusammenfassenden Dialog ersetzt
- Zusammenfassungsdialog zeigt detaillierte Statistiken:
  • Anzahl neu hinzugefügter Dateien
  • Anzahl bereits vorhandener Dateien (Hash-Duplikate)
  • Anzahl bereits zugeordneter Dateien
  • Liste umbenannter Dateien
  • Fehlerberichte falls aufgetreten
- Deutliche Verbesserung der UX: Bei 30 Dateien nur 1 Dialog statt 30

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-27 20:01:46 +01:00
info 4634b6e027 UX-Verbesserung: Automatisches Öffnen von Tree-Knoten während Transformation
Beim Durchführen von Transformationen werden jetzt die betroffenen XML-Knoten im Baum automatisch geöffnet und sichtbar gemacht, damit der Benutzer den Fortschritt besser verfolgen kann.

Änderungen:
- Neue Hilfsmethode _expand_tree_item_parents() zum rekursiven Öffnen aller Eltern-Knoten
- _on_transformation_job_started() erweitert um Auto-Expand und Scroll-to-Item
- Verbesserte Log-Meldung für besseres Debugging

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-27 17:44:46 +01:00
info 2d866a0fb5 Code-Qualität: Robustere Fehlerbehandlung in MainWindow
Verbessert die Fehlerbehandlung und Code-Robustheit durch:
- Explizite bool()-Konvertierung für has_xml_files (Zeile 894)
- Frühere Initialisierung von pdf_basename für Exception-Handler (Zeile 3717)
- Null-Checks für self.project/project_dir mit Fallback-Logik (Zeile 3741)

Verhindert potenzielle AttributeError und UnboundLocalError.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-27 17:28:04 +01:00
info 5316c37d26 UI-Verbesserung: Automatisches Laden von Diff-PDFs bei Tree-Selektion
Änderungen:
- Diff-PDFs werden automatisch im Viewer geladen wenn XML-Knoten ausgewählt wird
- Viewer wird geleert wenn Knoten ohne Diff-PDF ausgewählt wird
- Diff-Icon ist nicht mehr klickbar, dient nur noch als visueller Indikator
- Tree-Selection-Handler (_on_tree_selection_changed) implementiert
- XSL-ID wird nun auch in XML-Items gespeichert für direkten Zugriff
- Debug-Logging hinzugefügt für bessere Fehlersuche

Verhalten:
- Nutzer kann durch den Baum navigieren
- Bei Auswahl eines XML-Knotens mit Diff-PDF: automatisches Laden
- Bei Auswahl eines Knotens ohne Diff-PDF: Viewer leeren
- Tooltip angepasst: "Diff-PDF vorhanden (wird automatisch geladen bei Selektion)"

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-26 13:25:22 +01:00
info 1da550879e Windows-Fix: PDF-Dateisperren beim Akzeptieren von Änderungen beheben
Problem:
Unter Windows trat Fehler [WinError 32] auf beim Versuch, ref-PDFs zu
löschen/verschieben, da Dateien von QPdfDocument-Instanzen gesperrt waren.

Lösung:
- Neue Methode _close_all_pdf_documents() zum expliziten Schließen aller PDFs
- Schließt QPdfDocument-Instanzen und löscht alle Referenzen
- Erzwingt Garbage Collection zur Freigabe von Dateihandles
- Leert UI-Layouts und verarbeitet Qt-Events vor Dateioperationen
- QApplication.processEvents() stellt sicher, dass Widgets/Ressourcen
  wirklich freigegeben werden

Unter Linux war das Problem nicht aufgetreten, da dort geöffnete Dateien
gelöscht werden können.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-26 12:57:38 +01:00
info 8c7db39f5f FOP-Config: Projektspezifischer Konfigurationsordner und erweitertes Logging
- Project-Modell um optionales fop_config_dir Feld erweitert
- TransformationJob verwendet nun projektspezifischen FOP-Config-Pfad
- Saxon und FOP stdout/stderr werden nun im Debug-Level geloggt
- UI-Elemente für FOP-Config-Ordner-Auswahl hinzugefügt
- AppSettings und MainWindow unterstützen neues Feld beim Laden/Speichern

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-26 12:45:44 +01:00
info 92930a3da4 Saxon-Classpath: lib-Unterordner für Dependencies unterstützen
Erweitert den Classpath-Mechanismus, sodass JAR-Dateien aus dem lib-Unterverzeichnis des Saxon-Ordners automatisch eingebunden werden. Dies behebt den NoClassDefFoundError für org.xmlresolver.Resolver.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-24 14:19:28 +01:00
info caa7bd757a UI-Verbesserungen: Splitter-Verhalten und Layout optimiert
Verbesserungen am Haupt-Layout:
- Splitter: opaqueResize deaktiviert für flüssigeres Resizing
- Splitter: childrenCollapsible deaktiviert (verhindert versehentliches Kollabieren)
- TreeWidget: Hover-Effekt auskommentiert (weniger visuelles Rauschen)
- Frame-Eigenschaften angepasst für konsistentes Styling
- Placeholder-Labels (label_3, label_4) aus UI entfernt

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-20 21:16:33 +01:00
info 1d953186ea Legacy-Code entfernen: _load_images() Funktion gelöscht
Die Funktion _load_images() war Legacy-Code aus der Entwicklungsphase und versuchte PDFs aus einem nicht existierenden Verzeichnis (ui/res/pdf/) zu laden. Die tatsächliche PDF-Anzeige erfolgt über _load_pdf_for_comparison(), die PDFs aus dem Projekt-Verzeichnis lädt.

Entfernt:
- _load_images() Funktion (144 Zeilen)
- Aufruf in __init__()
- Ungenutzte Imports: glob, os

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-20 19:54:51 +01:00
info 3fa9a1dac0 Logging: print()-Aufrufe durch strukturierte Logger-Ausgaben ersetzen
Alle print()-Statements in MainWindow.py (~88) und XmlToXslAssignDialog.py (5) wurden durch passende Logger-Aufrufe ersetzt. Die Log-Level (debug, info, warning, error) wurden entsprechend der Nachrichtenart gewählt. XmlToXslAssignDialog.py erhielt zudem einen Logger-Import.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-20 19:39:23 +01:00
info 984f08cfc5 Diff-View-Icon: Semantisch passende Icons mit Fallbacks verwenden
Das Icon zum Laden von Diff-PDFs verwendet jetzt semantisch korrekte Icons:
- Primär: view-split-left-right (zeigt Split-View-Konzept)
- Fallback: vcs-diff (universelles Diff-Symbol)
- Letzter Fallback: system-search

Dies verbessert die visuelle Semantik und Plattform-Kompatibilität.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-20 13:56:38 +01:00
info c27c649142 Kontextmenü: Neue Aktion "Alle Änderungen übernehmen" für TreeNode und XslFile
Neue Kontextmenü-Funktionalität zum Batch-Akzeptieren von Diff-PDFs:
- Neue Aktion "Alle Änderungen übernehmen" für TreeNode und XslFile
- Wird nur aktiviert, wenn mindestens eine Diff-PDF unter dem Knoten existiert
- Akzeptiert alle Diff-PDFs unter dem ausgewählten Knoten mit einem Klick

Verhalten:
- Verschiebt alle new-PDFs nach ref (alte ref-PDFs werden gelöscht)
- Löscht alle diff-PDFs
- Entfernt Diff-Icons bei allen betroffenen XML-Knoten
- Aktualisiert Diff-PDF-Zähler auf allen übergeordneten Ebenen
- Zeigt Bestätigungsdialog mit Anzahl der zu akzeptierenden Änderungen
- Zeigt Erfolgsmeldung nach Abschluss

Viewer-Behandlung:
- KEINE Diff-PDF wird in den Viewer geladen
- Falls eine akzeptierte Diff-PDF gerade im Viewer angezeigt wird, wird der Viewer geleert
- Kein XML-Knoten wird im TreeWidget ausgewählt

Bugfixes:
- XSL-ID wird korrekt als "2002_1_128" statt "(2002, 1, 128)" formatiert
- Relative Pfade werden für xml_item_map verwendet (statt absolute Pfade)
- Diff-Icons werden jetzt korrekt entfernt

Implementierungsdetails:
- _collect_all_diff_pdfs_under_node(): Sammelt alle Diff-PDFs unter TreeNode oder XslFile
- _accept_single_diff_pdf(): Akzeptiert eine einzelne Diff-PDF ohne Viewer-Update
- _accept_all_changes_under_node(): Handler für Batch-Accept-Operation
- Kontextmenü-Integration in _create_context_menu_for_type()
- Logging-Konfiguration in main.py hinzugefügt (DEBUG-Level)

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-16 21:38:59 +01:00
info 091096270a Edit-Dialoge: XSLT-Parameter nebeneinander + Force-Transformation
UI-Redesign für TreeNodeEditDialog und XslFileEditDialog:
- XSLT-Parameter-Tabellen werden jetzt nebeneinander angezeigt
- Eigene Parameter (editierbar) links, geerbte Parameter (read-only) rechts
- Bessere Übersicht durch direkten visuellen Vergleich
- Fensterbreite auf ~870px erhöht für optimale Darstellung
- Icons für Hinzufügen/Entfernen-Buttons hinzugefügt
- Kompakteres Layout durch reduzierte Margins

Neue Funktionalität: Force-Transformation nach Bearbeitung
- Neue CheckBox "Alle XML-Dateien neu transformieren (force)" in beiden Dialogen
- Beim Schließen mit OK werden alle untergeordneten XML-Dateien transformiert
- TreeNodeEditDialog: Transformiert rekursiv alle XML-Dateien unter dem Knoten
- XslFileEditDialog: Transformiert alle XML-Dateien der XSL-Datei
- Transformation erfolgt auch bei bereits aktuellem Output (force=True)

Implementierungsdetails:
- TreeNodeEditDialog.get_data() gibt jetzt force_transform zurück
- XslFileEditDialog.get_data() gibt jetzt force_transform zurück
- MainWindow._find_item_by_node() findet Item nach TreeWidget-Neuladen
- MainWindow._edit_tree_node() startet Force-Transformation bei Bedarf
- MainWindow._edit_xsl_file() startet Force-Transformation bei Bedarf

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-16 20:32:33 +01:00
info 5b64cf5890 Accept-Changes-Button: Änderungen akzeptieren und automatisch zur nächsten Diff-PDF springen
Neuer Button zum Akzeptieren von PDF-Änderungen mit automatischem Workflow:
- Verschiebt new-PDF nach ref (alte ref-PDF wird gelöscht)
- Löscht diff-PDF
- Entfernt Diff-Icon beim aktuellen XML-Knoten
- Aktualisiert Diff-PDF-Anzahl auf übergeordneten Ebenen
- Lädt automatisch nächste Diff-PDF oder leert Viewer falls keine mehr vorhanden
- Wählt den entsprechenden XML-Knoten im Baum aus für visuelle Rückmeldung

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-15 21:10:15 +01:00
info 1fc7decace UI-Zustand beim Schließen speichern und beim Start wiederherstellen
Fenstergeometrie, Splitter-Positionen und TreeWidget-Spaltenbreiten werden jetzt in der Konfiguration gespeichert und beim nächsten Start automatisch wiederhergestellt.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-15 20:07:53 +01:00
info e409f38da2 Diff-Icons werden jetzt beim Projektladen und nach Transformation korrekt angezeigt
Fix für zwei Probleme:
1. Icons fehlten beim ersten Transformationsdurchlauf via TreeNode
2. Icons fehlten direkt nach Projektladen

Lösung:
- _update_diff_icons_for_existing_pdfs() in _load_nodes_to_tree() hinzugefügt
- _update_diff_icons_for_existing_pdfs() in _on_all_transformations_finished() hinzugefügt
- Doppelten Aufruf in open_existing_project() entfernt

Icons werden jetzt zuverlässig aktualisiert nach:
- Projektladen
- Jeder Transformation (einzeln oder via TreeNode)

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-14 21:34:37 +01:00
info f5767da611 Diff-PDF-Anzahl in Spalte 3 für TreeNode und XslFile anzeigen
Zeigt die Anzahl der untergeordneten Diff-PDF-Dateien in der dritten Spalte
des TreeWidgets für TreeNode und XslFile Knoten (nicht für XML-Dateien).

Neue Funktionen:
- _count_diff_pdfs_under_node(): Zählt rekursiv existierende Diff-PDFs
- _update_diff_pdf_counts_recursive(): Aktualisiert Anzahl in Spalte 2
- _update_all_diff_pdf_counts(): Aktualisiert alle Knoten im TreeWidget

Automatische Aktualisierung:
- Nach jeder Transformation (_on_all_transformations_finished)
- Beim Laden eines Projekts (_load_nodes_to_tree)
- Initial beim Erstellen der Tree-Items

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-14 21:11:40 +01:00
info b5e004ad8b XslFile-Kontextmenü: Transformations-Aktionen nur bei vorhandenen XML-Dateien aktiv
Die beiden Transformations-Menüpunkte im XslFile-Kontextmenü werden jetzt nur
aktiviert, wenn mindestens eine XML-Datei zugeordnet ist.

Änderungen:
- Prüfung auf vorhandene XML-Dateien (bool(xsl_file_obj.xmls))
- setEnabled(has_xml_files) für beide Transformations-Aktionen
- Analog zur TreeNode-Implementierung

Fix: Explizite bool()-Konvertierung, da xmls eine Liste ist

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-14 20:54:48 +01:00
info 56f81ca858 TreeNode-Kontextmenü: Transformations-Aktionen für alle untergeordneten XML-Dateien
Neue Funktionalität:
- TreeNode-Kontextmenü hat jetzt "Alle XML-Dateien transformieren" Aktionen
- Beide Aktionen (normal und force) transformieren rekursiv alle untergeordneten XML-Dateien
- Menüpunkte sind nur aktiv, wenn mindestens eine XML-Datei vorhanden ist

Implementierung:
- _has_xml_files_recursive(): Prüft rekursiv auf vorhandene XML-Dateien
- _collect_all_xsl_xml_pairs_recursive(): Sammelt alle XSL/XML-Paare im Teilbaum
- _transform_tree_node(): Transformiert alle gefundenen XML-Dateien
- Kontextmenü erweitert mit intelligenter Aktivierung/Deaktivierung

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-14 20:45:53 +01:00
info f591be2ea9 Diff-Icon und Progress-Bar im TreeWidget linksbündig positioniert
Ändert die Ausrichtung von zentriert zu linksbündig in der dritten Spalte
des TreeWidgets für bessere Übersichtlichkeit.

Änderungen:
- _create_centered_progress_bar(): AlignCenter → AlignLeft
- _create_centered_diff_icon(): AlignCenter → AlignLeft
- Docstrings aktualisiert

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-14 20:32:40 +01:00
info 656f9e3b11 PDF-Viewer-Fehler behoben: Mehrfaches Laden von PDFs funktioniert jetzt
Fixes:
- fullsize_label wird nach _clear_layout korrekt auf None gesetzt
- Verhindert, dass beim zweiten PDF-Laden kein Widget erstellt wird
- RuntimeError-Handling für gelöschte C++-Widgets in update_current_display()
- Verbessertes Error-Logging mit logger.error() statt print()

Problem: Nach dem ersten PDF-Laden wurden weitere PDFs nicht mehr angezeigt,
da fullsize_label auf ein gelöschtes Widget zeigte und kein neues erstellt wurde.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-14 20:12:40 +01:00
info 03449b2fb9 Hierarchische XSL-Parameter-Sammlung implementiert
XML-Transformationen berücksichtigen jetzt alle XSLT-Parameter der übergeordneten
TreeNodes, nicht nur die des direkten XslFile-Knotens. Tiefere Ebenen überschreiben
höhere Ebenen (XslFile hat höchste Priorität).

Änderungen:
- _collect_parent_params(): Bug-Fix für korrekte Prioritätsreihenfolge
- _create_transformation_job(): Hierarchische Parameter-Sammlung mit TreeWidgetItem-Kontext
- _transform_xml_file() und _transform_xsl_file(): Weitergabe des TreeWidgetItem-Kontexts
- Verbessertes Logging mit logger statt print()

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-14 15:59:37 +01:00
info 2f7d5b5431 Diff-PDF-Icon auf Einfachklick statt Doppelklick umgestellt
Der PDF-Vergleich im integrierten Viewer wird jetzt durch einen einfachen
Klick auf das Diff-Icon geladen, nicht mehr durch Doppelklick.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-14 15:28:36 +01:00
info e49af98cc0 Ruff-Konfiguration erweitert und Code-Style-Fehler behoben
- extend-exclude für automatisch generierte *_ui.py Dateien hinzugefügt
- Unbenutzte Imports in Dialog-Dateien entfernt
- Unbenutzte Variable sample_keys in MainWindow entfernt
- f-strings ohne Platzhalter in Test-Datei korrigiert

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-14 15:16:57 +01:00
info fd38eb426f Erweiterte Validierung der Tool-Konfigurationspfade mit hasattr-Checks
Zusätzliche Sicherheitsprüfungen für path_to_binary_file, path_to_jar_file,
path_to_dir und path_to_root_dir Attribute, um NoneType-Fehler zu vermeiden.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-14 15:09:52 +01:00
info dd20067d42 PDF-Vergleich in integrierten Viewer umgeleitet und Alpha-Blending verbessert
Das Diff-PDF-Icon lädt nun alle drei PDFs (diff, ref, new) direkt in den eingebauten Vergleichs-Viewer, statt ein externes Programm zu öffnen. Zusätzlich wurde die Alpha-Blending-Logik für sanftere Übergänge zwischen den Ansichten korrigiert.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-14 13:45:20 +01:00
info 629485f5e4 Progress Bar und Diff-PDF-Icon im TreeWidget implementiert
Neue Features:
- Progress Bar in Spalte 2 während XML-Transformationen
- Diff-PDF-Icon erscheint nach Transformation bei vorhandener Diff-PDF
- Doppelklick auf Icon öffnet Diff-PDF mit System-Viewer
- Initial-Laden von Icons für existierende Diff-PDFs beim Projektstart

Technische Implementierung:
- XML-Item-Mapping mit eindeutigem Key-Format: "xml_path|xsl_id"
- Unterstützt mehrfache Verwendung derselben XML bei verschiedenen XSL-Dateien
- TransformationThread-Signale erweitert um XSL-ID-Parameter
- Widget-Factory-Methoden für zentrierte Progress Bar und klickbare Icons
- Result-Dictionary in transform.py enthält jetzt xsl_id

UI-Anpassungen:
- TreeWidget Spaltenanzahl von 2 auf 3 erhöht
- setItemWidget() für dynamische Widget-Verwaltung in Spalte 2

Dateien:
- src/ui/MainWindow.py: Hauptimplementierung mit Signal-Handlern
- src/transform.py: xsl_id im Result-Dictionary
- src/ui/MainWinddow.ui: Spalte 3 hinzugefügt
- src/ui/MainWinddow_ui.py: Auto-generiert aus UI-Datei

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-13 21:06:40 +01:00
info b961fe1e1a Type-Hints in transform.py korrigiert und verbessert
- 'any' zu 'Any' korrigiert (korrekter Import von typing)
- 'tuple' zu 'tuple | None' für optionale Parameter
- Import von typing.Any hinzugefügt

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-13 20:00:12 +01:00
info c699c53a14 PDF-Ordnerstruktur auf 'new', 'ref' und 'diff' umgestellt
Die PDF-Generierung verwendet nun die Ordner 'new', 'ref' und 'diff'
anstelle von 'output', 'valide' und 'diff'. Dies ermöglicht die
Integration mit MainWindow._load_images(), die PDFs in den Ordnern
'new', 'ref' und 'diff' sucht.

Änderungen:
- output_dir → new_dir (für neu generierte PDFs)
- valide_dir → ref_dir (für Referenz-PDFs)
- Alle Variablen und Log-Meldungen entsprechend angepasst
- Unused import entfernt (typing.Optional)

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-12 21:38:24 +01:00
info ee6ded95ab PDF-Dateinamen enthalten nun XSL-ID zur Vermeidung von Überschreibungen
Wenn eine XML-Datei mehreren XSL-Dateien zugeordnet ist, wurden die
generierten PDFs bisher überschrieben. Jetzt wird die XSL-ID in den
Dateinamen integriert (z.B. rechnung_xsl_1.pdf, rechnung_xsl_2.pdf),
sodass jede Transformation ihre eigene PDF-Datei erhält.

Änderungen:
- TransformationJob: xsl_id Parameter hinzugefügt
- Dateinamen-Generierung berücksichtigt XSL-ID (Tuple → String)
- MainWindow: XSL-ID wird an TransformationJob übergeben

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-12 21:24:54 +01:00
info c593ff145b XSL-Transformations-Engine mit Saxon, Apache FOP und diff-pdf implementiert
Fügt die komplette Transformations-Pipeline hinzu:
- Saxon XSLT-Transformation (XML → FO) mit vollständigem Classpath-Support
- Apache FOP PDF-Generierung (FO → PDF) mit plattformübergreifender Unterstützung
- Automatische diff-pdf Vergleichs- und Diff-Generierung
- Valide-PDF-Verwaltung (Referenz-PDFs beim ersten erfolgreichen Build)
- Up-to-Date-Prüfung basierend auf Datei-Zeitstempeln
- Asynchrone Ausführung via TransformationThread (QThread)
- Kontextmenü-Integration für XML- und XSL-Dateien
- Detailliertes Fehler-Reporting und Fortschritts-Feedback

Neue Dateien:
- src/transform.py: TransformationJob-Klasse mit vollständiger Pipeline

Erweiterte Dateien:
- src/ui/MainWindow.py: TransformationThread und Transformations-Methoden

Technische Details:
- Löst Saxon ClassNotFoundException durch Verwendung aller JARs im Saxon-Verzeichnis
- Verwendet -cp statt -jar für vollständigen Classpath-Zugriff
- Automatisches Cleanup temporärer FO-Dateien
- Thread-sicheres Shutdown-Handling

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-11 21:26:13 +01:00
info 6e4d28d3a8 Defensive Null-Checks in MainWindow hinzugefügt
Ergänzt umfassende Existenzprüfungen für pdf_project, project und nodes-Attribute
vor dem Zugriff, um NoneType-Fehler zu vermeiden. Verbessert die Robustheit der
Anwendung bei nicht initialisierten Projekten.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-07 20:34:29 +01:00
info d314cf5612 Hash-basierte XML-Duplikatserkennung und intelligente Dateinamen-Verwaltung
Implementiert automatische Erkennung von XML-Datei-Duplikaten basierend auf blake2b-Hashes. Bei Hash-Match wird die vorhandene Datei automatisch zugeordnet statt sie zu kopieren. Bei Dateinamen-Konflikten werden alternative Namen (datei_1.xml, datei_2.xml, etc.) mit Auswahl-Dialog angeboten.

Neue Features:
- Projekt-weite Hash-Duplikatserkennung
- Automatische Zuordnung vorhandener Dateien bei Hash-Match
- Alternative Dateinamen-Generierung mit Benutzer-Dialog
- Performance-Optimierung durch Set-basierte Dateinamen-Prüfung
- Umfassende Dokumentation und Test-Suite

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-07 20:15:38 +01:00
info 0417996db2 Konfigurationsstruktur konsolidiert und bereinigt
- agents.md Dateien aus verschiedenen Verzeichnissen entfernt
- .kilocode/rules/CLAUDE.md als zentrale Konfiguration hinzugefügt
- CLAUDE.md Dokumentation aktualisiert

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-07 15:39:54 +01:00
info caddfc68fb Deutsche Sprachanweisung und Code-Bereinigung
- CLAUDE.md: Deutsche Sprachanweisung am Anfang hinzugefügt
- src/main.py: Auskommentierten qdarktheme Code entfernt (Import und Setup)
- Verbessert Code-Qualität durch Entfernen von totem Code

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-07 12:51:33 +01:00
info 2e86a4befb CLAUDE.md Dokumentation hinzugefügt und Konfigurationspfad-Handling verbessert
- CLAUDE.md mit umfassender Projektdokumentation für Claude Code hinzugefügt
- Beschreibt Architektur, Datenmodelle, UI-Muster und Entwicklungsworkflows
- Konfigurationspfad-Verarbeitung in src/conf.py robuster gemacht:
  - os.path durch pathlib.Path ersetzt
  - Validierung für Schreibrechte und Verzeichnisexistenz hinzugefügt
  - Besseres Error-Handling mit sys.exit(1) bei fehlenden Berechtigungen

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-11 13:00:32 +02:00
info 5c91836d87 Bessere Unterstützung unter Linux fhür '~' als home dir 2025-10-02 21:00:19 +02:00
159 changed files with 24427 additions and 4585 deletions
+13
View File
@@ -0,0 +1,13 @@
{
"enabledPlugins": {
"feature-dev@claude-plugins-official": true
},
"permissions": {
"allow": [
"Bash(uv version)",
"Bash(uv run ruff check)",
"Bash(uv run ruff check *)",
"Bash(uv tree)"
]
}
}
+154
View File
@@ -0,0 +1,154 @@
---
name: dep-update-check
description: Prüft manuell, ob Python-Version und Projekt-Dependencies aktualisiert werden können, und testet die Kompatibilität. Verwende diesen Skill AUSSCHLIESSLICH wenn der Benutzer explizit danach fragt z.B. '/dep-update-check', 'Prüfe Dependencies', 'Sind Updates verfügbar?', 'Kann ich Python updaten?'. Niemals automatisch auslösen.
---
# Dependency & Python Update Check
Dieser Skill prüft systematisch, welche Updates für das DocuMentor-Projekt verfügbar sind und ob sie kompatibel miteinander sind. Verwendete Werkzeuge: `uv`, `pyproject.toml`.
## Ablauf
Führe alle Schritte der Reihe nach aus und erstelle am Ende einen übersichtlichen Report.
---
## Schritt 1: Python-Versionscheck
Lies die Python-Constraint aus `pyproject.toml` (Feld `requires-python`).
```bash
grep "requires-python" pyproject.toml
```
Prüfe dann, welche stabilen Python-Versionen im erlaubten Bereich verfügbar sind:
```bash
uv python list
```
**Stabile Versionen** sind jene ohne Suffix wie `b1`, `rc1`, `a1` oder `+freethreaded`. Filtere Beta- und Release-Candidate-Versionen heraus.
Vergleiche:
- Aktuell verwendete Version: `uv run python --version`
- Neueste stabile verfügbare Version innerhalb der Constraints
Falls eine neuere stabile Version verfügbar ist, notiere dies für den Report.
---
## Schritt 2: Veraltete Dependencies ermitteln
```bash
uv tree --outdated
```
Extrahiere daraus die **direkten** Projektabhängigkeiten (aus `pyproject.toml`, Abschnitte `[project] dependencies` und `[dependency-groups]`) und trenne sie von transitiven Abhängigkeiten.
Erstelle eine strukturierte Liste:
- Direkte Dependencies: Name, aktuelle Version, neueste Version
- Transitive Dependencies mit Updates (nur zur Information)
---
## Schritt 3: Kompatibilitätstest
Sichere zunächst die aktuelle Lock-Datei:
```bash
cp uv.lock uv.lock.backup
```
Versuche dann, alle Dependencies auf die neuesten kompatiblen Versionen aufzulösen:
```bash
uv lock --upgrade 2>&1
```
**Interpretation:**
- `Resolved X packages` ohne Fehler → alle Updates sind kompatibel
- Fehlermeldungen über Versionskonflikte → notiere welche Pakete sich gegenseitig blockieren
- Prüfe ob `uv.lock` verändert wurde: `diff uv.lock.backup uv.lock | head -50`
Stelle die Lock-Datei wieder her (wir wollen die Umgebung nicht tatsächlich ändern):
```bash
mv uv.lock.backup uv.lock
```
---
## Schritt 4: Report erstellen
Gib den Report im folgenden Format aus:
---
### 🐍 Python-Version
| | Version |
|---|---|
| Aktuell in Verwendung | z.B. 3.13.11 |
| Neueste stabile im erlaubten Bereich | z.B. 3.13.13 |
| Update empfohlen? | Ja / Nein |
Falls ein Python-Update verfügbar ist, zeige den Befehl:
```bash
uv python install 3.X.Y
# Dann in pyproject.toml requires-python anpassen falls nötig
# Danach: uv sync
```
---
### 📦 Direkte Dependencies
Tabelle mit Spalten: Paket | Aktuell | Verfügbar | Status
Status-Symbole:
- ✅ Aktuell
- ⬆️ Update verfügbar
- ⚠️ Update verfügbar, aber Kompatibilitätsproblem
---
### 🔗 Transitive Dependencies (Auswahl)
Nur falls es relevante Updates gibt, kurze Liste.
---
### 🔄 Kompatibilitäts-Ergebnis
Klares Fazit:
- Können alle direkten Dependencies gleichzeitig aktualisiert werden? Ja/Nein
- Falls Nein: Welche Konflikte bestehen und warum?
---
### 📋 Empfohlene Aktion
Falls Updates verfügbar und kompatibel:
```bash
uv sync --upgrade
```
Falls nur einzelne Pakete aktualisiert werden sollen:
```bash
uv add paketname>=neue.version
```
Falls Python aktualisiert werden soll (nur wenn innerhalb der Constraints):
```bash
uv python install 3.X.Y
uv sync
```
---
## Hinweise
- **Nie `uv sync --upgrade` automatisch ausführen** nur im Report vorschlagen, der Benutzer entscheidet.
- Beta/RC-Python-Versionen werden nicht empfohlen (erkennbar an Suffixen wie `b1`, `rc1`).
- `pyarrow` und andere native Pakete können bei Python-Upgrades besondere Anforderungen haben darauf hinweisen falls relevant.
- Wenn der `uv lock --upgrade`-Test fehlschlägt, die Lock-Datei **immer** aus dem Backup wiederherstellen.
+94
View File
@@ -0,0 +1,94 @@
---
name: license-check
description: "Prüft und aktualisiert THIRD_PARTY_LICENSES.txt bei Dependency-Änderungen. Verwende diesen Skill IMMER zusammen mit dem version-bump Skill wenn der Benutzer einen Git-Commit erstellen möchte, 'commit' erwähnt, oder nach /commit fragt. Der Skill erkennt automatisch ob sich Dependencies in pyproject.toml geändert haben und aktualisiert die Lizenzdatei entsprechend."
---
# License Check Skill
Dieser Skill stellt sicher, dass die Datei `THIRD_PARTY_LICENSES.txt` immer synchron mit den tatsächlichen Dependencies in `pyproject.toml` bleibt. Er wird automatisch als Teil des Commit-Workflows ausgeführt, parallel zum version-bump Skill.
## Warum das wichtig ist
DocuMentor listet alle verwendeten Drittanbieter-Bibliotheken mit Lizenzinformationen in `THIRD_PARTY_LICENSES.txt` auf. Wenn Dependencies hinzugefügt oder entfernt werden, muss diese Datei aktualisiert werden — sonst sind die Lizenzangaben unvollständig oder veraltet, was rechtliche Konsequenzen haben kann.
## Ablauf
### Schritt 1: Prüfskript ausführen
Führe das gebündelte Prüfskript aus:
```bash
uv run python .claude/skills/license-check/scripts/check_licenses.py
```
Das Skript gibt JSON aus mit:
- `missing`: Dependencies in pyproject.toml die in THIRD_PARTY_LICENSES.txt fehlen
- `removed`: Einträge in THIRD_PARTY_LICENSES.txt die nicht mehr in pyproject.toml stehen
- `info`: Automatisch ermittelte Lizenz-Metadaten für fehlende Pakete
### Schritt 2: Ergebnis auswerten
- Wenn `missing` und `removed` beide leer sind: **Keine Aktion nötig.** Fahre direkt mit dem Commit fort.
- Wenn es Änderungen gibt: Zeige dem Benutzer eine Zusammenfassung und frage ob die Lizenzdatei aktualisiert werden soll.
Beispiel-Zusammenfassung:
```
Lizenzdatei-Prüfung:
+ lxml (BSD License) — neu hinzuzufügen
- some-old-lib — aus Lizenzdatei zu entfernen
```
### Schritt 3: THIRD_PARTY_LICENSES.txt aktualisieren
#### Neue Dependencies hinzufügen
Füge neue Einträge in die passende Sektion ein (Python-Abhängigkeiten oder Eingebettete Bibliotheken). Verwende das bestehende Format:
```
N. PaketName
Version: >=X.Y.Z
Lizenz: Lizenzname
Webseite: https://...
GitHub: https://github.com/...
Beschreibung: Kurzbeschreibung auf Englisch
Copyright: Copyright (c) Jahr Autor
```
Dabei gilt:
- Die Nummerierung fortlaufend innerhalb der Sektion
- Dev-Dependencies bekommen den Suffix `(Development)` im Namen
- Transitive Dependencies bekommen den Suffix `(via HauptPaket)` im Namen
- Die Lizenzinfos aus dem `info`-Feld des Skripts verwenden
- Wenn das Skript keine Info liefert (Paket nicht installiert), recherchiere via Web
#### Sortierung
Die Reihenfolge der Einträge folgt der logischen Gruppierung:
1. Haupt-Dependencies (Runtime)
2. Transitive Dependencies direkt nach ihrem Eltern-Paket (z.B. ConnectorX nach Polars)
3. Dev-Dependencies am Ende der Python-Sektion
#### Entfernte Dependencies löschen
Entferne den kompletten Block (Name, Version, Lizenz, etc.) des entfernten Pakets und nummeriere die verbleibenden Einträge neu.
#### Neue Lizenztypen
Wenn eine neue Dependency eine Lizenz verwendet, die noch nicht im Abschnitt "Lizenztexte" am Ende der Datei aufgeführt ist, füge den Lizenztext dort hinzu. Gängige Lizenztexte (MIT, Apache 2.0, BSD-3-Clause) sind bereits vorhanden.
### Schritt 4: KNOWN_ALIASES aktualisieren
Wenn neue Dependencies hinzugefügt werden, prüfe ob das `KNOWN_ALIASES`-Dict in `.claude/skills/license-check/scripts/check_licenses.py` aktualisiert werden muss:
- Wenn der Paketname in pyproject.toml anders ist als in THIRD_PARTY_LICENSES.txt (z.B. durch Suffixe wie "(Development)" oder "(via X)")
- Wenn eine Dependency transitive Dependencies hat, die separat gelistet werden
### Schritt 5: Commit fortsetzen
Füge die geänderte `THIRD_PARTY_LICENSES.txt` (und ggf. `check_licenses.py`) zum Staging-Bereich hinzu und fahre mit dem normalen Commit-Workflow fort.
## Wichtige Hinweise
- Das Skript prüft nur **Python-Abhängigkeiten** und **eingebettete Bibliotheken** — externe Tools (Saxon, FOP, diff-pdf) werden ignoriert
- Die Lizenzdatei enthält auch den Abschnitt "Stand: Monat Jahr" am Ende — diesen bei Änderungen auf den aktuellen Monat aktualisieren
- `uv.lock` nicht manuell ändern — wird durch `uv sync` automatisch aktualisiert
@@ -0,0 +1,239 @@
#!/usr/bin/env python3
"""
Vergleicht die Dependencies aus pyproject.toml mit den Einträgen in THIRD_PARTY_LICENSES.txt.
Gibt eine JSON-Ausgabe mit:
- missing: Dependencies die in pyproject.toml stehen aber nicht in THIRD_PARTY_LICENSES.txt
- removed: Einträge in THIRD_PARTY_LICENSES.txt die nicht mehr in pyproject.toml stehen
- version_changed: Dependencies deren Mindestversion sich geändert hat
- info: Metadaten zu fehlenden Paketen (Lizenz, Homepage, etc.)
"""
import json
import re
import sys
import tomllib
from importlib.metadata import PackageNotFoundError, metadata
from pathlib import Path
def parse_pyproject(pyproject_path: Path) -> dict[str, str]:
"""Parst pyproject.toml und extrahiert Dependencies mit Mindestversionen."""
with open(pyproject_path, "rb") as f:
data = tomllib.load(f)
deps: dict[str, str] = {}
# dependencies-Sektion
for dep_str in data.get("project", {}).get("dependencies", []):
m = re.match(r"([a-zA-Z0-9_-]+)(?:\[.*?\])?(?:>=([0-9.]+))?", dep_str)
if m:
deps[m.group(1).lower()] = m.group(2) or ""
# dependency-groups dev
for dep_str in data.get("dependency-groups", {}).get("dev", []):
if isinstance(dep_str, str):
m = re.match(r"([a-zA-Z0-9_-]+)(?:\[.*?\])?(?:>=([0-9.]+))?", dep_str)
if m:
deps[m.group(1).lower()] = m.group(2) or ""
return deps
def parse_licenses_file(licenses_path: Path) -> tuple[dict[str, str], dict[str, str]]:
"""Parst THIRD_PARTY_LICENSES.txt und extrahiert Paketnamen nach Sektion.
Returns:
tuple[dict, dict]: (python_deps, embedded_libs) — jeweils lowercase key -> original name
"""
content = licenses_path.read_text(encoding="utf-8")
python_deps: dict[str, str] = {}
embedded_libs: dict[str, str] = {}
current_section = None
current_target = None
for line in content.splitlines():
if "Python-Abhängigkeiten" in line:
current_section = "python"
current_target = python_deps
continue
if "Eingebettete Bibliotheken" in line:
current_section = "embedded"
current_target = embedded_libs
continue
if "Externe Tools" in line or "Lizenztexte" in line:
current_section = None
current_target = None
continue
if current_target is None:
continue
# Nummerierter Eintrag: "1. PaketName" oder "1. PaketName (via X)"
entry_match = re.match(r"\s*\d+\.\s+(.+?)(?:\s+\(.*\))?\s*$", line)
if entry_match:
name = entry_match.group(1).strip()
current_target[name.lower()] = name
continue
# Version-Zeile: " Version: >=X.Y.Z"
version_match = re.match(r"\s+Version:\s*>=?([\d.]+)", line)
if version_match and current_target:
last_key = list(current_target.keys())[-1]
current_target[last_key] = current_target[last_key] + "|" + version_match.group(1)
return python_deps, embedded_libs
# Mapping: pyproject-Name -> zugehörige Einträge in THIRD_PARTY_LICENSES.txt
# Deckt transitive Dependencies und Aliase mit Suffixen ab.
KNOWN_ALIASES = {
"pyside6": ["pyside6"],
"pydantic-settings": ["pydantic-settings", "pydantic"], # pydantic ist transitive Dep
"pydantic-yaml": ["pydantic-yaml"],
"polars": ["polars", "connectorx (via polars)", "pyarrow (via polars)"],
"connectorx": ["connectorx (via polars)"],
"psutil": ["psutil"],
"lxml": ["lxml"], # BSD-3-Clause, XML/XSLT-Parsing
"ruff": ["ruff (development)"],
"pyinstaller": ["pyinstaller (development)"],
"pillow": ["pillow (development)"],
}
def get_package_info(pkg_name: str) -> dict:
"""Holt Paket-Metadaten via importlib.metadata."""
info = {"name": pkg_name, "installed": False}
try:
m = metadata(pkg_name)
info["installed"] = True
info["version"] = m.get("Version", "")
info["summary"] = m.get("Summary", "")
# Lizenz ermitteln
license_expr = m.get("License-Expression") or ""
if not license_expr:
classifiers = [c for c in (m.get_all("Classifier") or []) if "License" in c]
if classifiers:
license_expr = classifiers[0].split(" :: ")[-1]
else:
lic_text = m.get("License") or ""
if "MIT" in lic_text:
license_expr = "MIT License"
elif "BSD" in lic_text:
license_expr = "BSD License"
elif "Apache" in lic_text:
license_expr = "Apache License 2.0"
elif "LGPL" in lic_text or "GPL" in lic_text:
license_expr = lic_text[:80]
else:
license_expr = lic_text[:80] if lic_text else "Unbekannt"
info["license"] = license_expr
# Homepage/GitHub
urls = m.get_all("Project-URL") or []
for url_entry in urls:
if "," in url_entry:
label, url = url_entry.split(",", 1)
label = label.strip().lower()
url = url.strip()
if "homepage" in label or "home-page" in label:
info["homepage"] = url
elif "repository" in label or "github" in label or "source" in label:
info["github"] = url
if "homepage" not in info:
homepage = m.get("Home-page")
if homepage:
info["homepage"] = homepage
# Author/Copyright
author = m.get("Author") or m.get("Author-email") or ""
info["author"] = author
except PackageNotFoundError:
pass
return info
def normalize_name(name: str) -> str:
"""Normalisiert Paketnamen für Vergleich."""
return re.sub(r"[-_.]+", "-", name).lower().strip()
def main():
project_root = Path(__file__).resolve().parents[4] # .claude/skills/license-check/scripts -> root
pyproject_path = project_root / "pyproject.toml"
licenses_path = project_root / "THIRD_PARTY_LICENSES.txt"
if not pyproject_path.exists():
print(json.dumps({"error": f"pyproject.toml nicht gefunden: {pyproject_path}"}))
sys.exit(1)
if not licenses_path.exists():
print(json.dumps({"error": f"THIRD_PARTY_LICENSES.txt nicht gefunden: {licenses_path}"}))
sys.exit(1)
pyproject_deps = parse_pyproject(pyproject_path)
python_entries, embedded_entries = parse_licenses_file(licenses_path)
# Normalisiere Python-License-Entry-Keys
normalized_license_names = {}
for key in python_entries:
clean = re.sub(r"\s*\(.*?\)", "", key).strip()
normalized_license_names[normalize_name(clean)] = key
result = {
"pyproject_deps": {k: v for k, v in sorted(pyproject_deps.items())},
"python_license_entries": list(python_entries.keys()),
"embedded_license_entries": list(embedded_entries.keys()),
"missing": [],
"removed": [],
"info": {},
}
# Finde fehlende Dependencies
covered_in_licenses = set()
for dep_name in pyproject_deps:
norm = normalize_name(dep_name)
if norm in normalized_license_names:
covered_in_licenses.add(norm)
elif dep_name in KNOWN_ALIASES:
found = False
for alias in KNOWN_ALIASES[dep_name]:
alias_norm = normalize_name(re.sub(r"\s*\(.*?\)", "", alias))
if alias_norm in normalized_license_names:
found = True
covered_in_licenses.add(alias_norm)
if not found:
result["missing"].append(dep_name)
result["info"][dep_name] = get_package_info(dep_name)
else:
result["missing"].append(dep_name)
result["info"][dep_name] = get_package_info(dep_name)
# Finde entfernte Einträge (nur Python-Abhängigkeiten, NICHT eingebettete)
for norm_name, orig_key in normalized_license_names.items():
if norm_name not in covered_in_licenses:
# Prüfe ob es ein "via"-Eintrag ist
if "(via" in orig_key:
parent = re.search(r"\(via\s+(\w+)\)", orig_key)
if parent and normalize_name(parent.group(1)) in {normalize_name(d) for d in pyproject_deps}:
continue
# Prüfe ob es über KNOWN_ALIASES abgedeckt ist
is_alias = False
for dep, aliases in KNOWN_ALIASES.items():
if dep in pyproject_deps:
for alias in aliases:
if normalize_name(re.sub(r"\s*\(.*?\)", "", alias)) == norm_name:
is_alias = True
break
if is_alias:
break
if not is_alias:
result["removed"].append(orig_key)
print(json.dumps(result, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()
+83
View File
@@ -0,0 +1,83 @@
---
name: version-bump
description: "Versionsverwaltung für DocuMentor-Commits. Verwende diesen Skill IMMER wenn der Benutzer einen Git-Commit erstellen möchte, 'commit' erwähnt, oder nach /commit fragt. Der Skill fragt vor dem Commit, ob die Programmversion aktualisiert werden soll, und aktualisiert alle versionsbezogenen Dateien einheitlich."
---
# Version Bump Skill
Dieser Skill stellt sicher, dass bei jedem Commit die Programmversion bewusst behandelt wird. Bevor der eigentliche Commit erstellt wird, wird der Benutzer gefragt, ob und wie die Version angepasst werden soll.
## Warum das wichtig ist
DocuMentor speichert die Version an mehreren Stellen gleichzeitig (pyproject.toml, Installer-Dateien, Lizenz-Footer). Wenn diese aus dem Takt geraten, entstehen inkonsistente Builds. Dieser Skill verhindert das, indem er alle Stellen auf einmal aktualisiert.
## Ablauf
### Schritt 1: Benutzer fragen
Bevor du den Commit erstellst, frage den Benutzer mit dem AskUserQuestion-Tool:
**Frage:** "Soll die Programmversion aktualisiert werden?"
Optionen:
- **Patch (X.Y.Z+1)** — Bugfix, kleine Änderung
- **Minor (X.Y+1.0)** — Neues Feature, Erweiterung
- **Major (X+1.0.0)** — Breaking Change, großer Meilenstein
- **Nein, Version beibehalten** — Keine Versionsänderung
Zeige dabei die aktuelle Version aus `pyproject.toml` in der Frage an.
### Schritt 2: Version aktualisieren (falls gewünscht)
Wenn der Benutzer eine Versionserhöhung wählt:
1. **`pyproject.toml`** — über `uv version --bump` aktualisieren (niemals direkt bearbeiten):
```bash
uv version --bump patch # für Patch
uv version --bump minor # für Minor
uv version --bump major # für Major
```
Nach dem Befehl die neue Version aus `pyproject.toml` auslesen — sie ist die Single Source of Truth.
2. **`DocuMentor.wxs`** — WiX Installer (z. B. Zeile mit `Version=`):
```xml
Version="X.Y.Z"
```
3. **`installer.iss`** — Inno Setup (z. B. Zeile mit `#define MyAppVersion`):
```
#define MyAppVersion "X.Y.Z"
```
4. **`THIRD_PARTY_LICENSES.txt`** — Lizenz-Footer (letzte Zeile):
```
Erstellt für: DocuMentor vX.Y.Z
```
Führe zuerst `uv version --bump` aus, lese danach die neue Version aus `pyproject.toml`, und aktualisiere dann die übrigen drei Dateien auf diesen Wert.
### Schritt 3: Commit erstellen
Nachdem die Versionsdateien aktualisiert wurden (oder der Benutzer "Nein" gewählt hat), erstelle den Commit ganz normal nach den üblichen Commit-Konventionen. Falls die Version geändert wurde, füge die geänderten Versionsdateien zum Commit hinzu.
### Schritt 4: Git-Tag setzen (nur bei Versionserhöhung)
Wenn der Benutzer eine Versionserhöhung gewählt hat und der Commit erfolgreich war, setze einen annotated Git-Tag mit der neuen Version:
```bash
git tag -a "vX.Y.Z" -m "Version X.Y.Z"
```
Wobei `X.Y.Z` die neue Version aus `pyproject.toml` ist. Das Tag-Format ist immer `v` + Versionsnummer (z.B. `v1.7.1`).
Informiere den Benutzer danach kurz: „Tag `vX.Y.Z` gesetzt. Mit `git push origin vX.Y.Z` kannst du ihn pushen."
Wenn der Benutzer "Nein" gewählt hat, wird kein Tag gesetzt.
## Wichtige Hinweise
- `pyproject.toml` **niemals direkt bearbeiten** — immer `uv version --bump` verwenden
- Die Version in `pyproject.toml` ist die Single Source of Truth; nach dem Bump dort auslesen
- `create_version_info.py` liest automatisch aus `pyproject.toml` — diese Datei muss nicht manuell angepasst werden
- `uv.lock` wird durch `uv sync` automatisch aktualisiert — nicht manuell ändern
- Wenn der Benutzer "Nein" wählt, einfach normal mit dem Commit fortfahren
-14
View File
@@ -1,14 +0,0 @@
# 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.
+15
View File
@@ -6,5 +6,20 @@ dist/
wheels/ wheels/
*.egg-info *.egg-info
# PyInstaller
*.spec.bak
*.manifest
*.log
version_info.txt
# Generierte Icons (optional - entfernen falls Icons versioniert werden sollen)
# resources/icon.ico
# Virtual environments # Virtual environments
.venv .venv
# WiX Installer Build-Artefakte
ProductFiles.wxs
*.msi
*.wixpdb
.wix/
+1
View File
@@ -0,0 +1 @@
CLAUDE.md
-14
View File
@@ -1,14 +0,0 @@
# 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.
+404
View File
@@ -0,0 +1,404 @@
# DocuMentor Build-Anleitung
## Voraussetzungen
```bash
# Dependencies installieren (inkl. Pillow für Icon-Generierung und PyInstaller)
uv sync --all-groups
```
Für MSI-Installer zusätzlich:
- **WiX Toolset v6**: `dotnet tool install --global wix --version 6.*`
> **Hinweis**: WiX v7+ erfordert die Akzeptanz der OSMF-EULA (`wix eula accept`). WiX v6 vermeidet diese Einschränkung.
Für Setup.exe zusätzlich:
- **Inno Setup**: https://jrsoftware.org/isdl.php
## Schnellstart: ZIP-Distribution erstellen
```bash
# Automatischer Build (empfohlen)
uv run python build_windows.py
```
Dies erstellt automatisch:
1. **Icon** (falls nicht vorhanden): `resources/icon.ico`
2. **Versionsinformationen**: `version_info.txt`
3. **Executable**: `dist/DocuMentor/DocuMentor.exe` (mit Icon und Versionsinformationen)
4. **ZIP-Archiv**: `dist/DocuMentor-YYYYMMDD-Windows.zip`
### Manuelle Build-Schritte
```bash
# 1. Cleanup
rm -rf build/ dist/
# 2. PyInstaller ausführen
uv run pyinstaller --clean DocuMentor.spec
# 3. Testen
# Auf Windows: dist/DocuMentor/DocuMentor.exe
# Mit Wine: wine dist/DocuMentor/DocuMentor.exe
```
## MSI-Installer erstellen (WiX Toolset)
### Schritt 1: PyInstaller Build erstellen
```bash
uv run python build_windows.py
```
### Schritt 2: ProductFiles.wxs generieren
**WICHTIG**: WiX v6 hat das `heat` Tool entfernt. Stattdessen wird ein Python-Skript verwendet:
```bash
# Generiert automatisch ProductFiles.wxs mit allen Dateien aus dist/DocuMentor
uv run python generate_wix_files.py
```
### Schritt 3: MSI kompilieren
```bash
wix build DocuMentor.wxs ProductFiles.wxs -o DocuMentor.msi
```
### Schritt 4: MSI testen
```bash
# Installation (als Administrator)
msiexec /i DocuMentor.msi
# Silent Installation für Deployment
msiexec /i DocuMentor.msi /quiet /qn /norestart
# Deinstallation
msiexec /x DocuMentor.msi
```
### MSI-Vorteile
- Windows-Standard (`.msi` Format)
- Gruppen-Richtlinien-Deployment (GPO) für Enterprise
- Silent Installation (`msiexec /quiet`)
- Windows Installer Transaktionen und Rollback
- Patch-Unterstützung (.msp für Updates)
- Versionsupgrades automatisch verwaltet
### MSI-Build automatisieren
Alternativ zu den manuellen Schritten kann ein `build_msi.py` Skript verwendet werden:
```bash
uv run python build_msi.py
```
Dieses Skript führt `generate_wix_files.py` und `wix build` automatisch nacheinander aus.
## Setup.exe erstellen (Inno Setup)
1. **GUID generieren** für `installer.iss` (nur beim ersten Mal!):
```bash
uv run python generate_guid.py
```
Kopiere die generierte GUID und füge sie in `installer.iss` bei `AppId` ein (Zeile ~22).
**WICHTIG**: Die GUID nur EINMAL generieren! Bei Updates dieselbe GUID verwenden.
2. **Windows-Build erstellen**:
```bash
uv run python build_windows.py
```
3. **Installer kompilieren**:
```bash
iscc installer.iss
```
4. **Ergebnis**:
- `dist/installer/DocuMentor-Setup-0.1.0.exe` (mit Icon und Versionsinformationen)
## Konfiguration anpassen
### Icon anpassen
**Option 1: Standard-Icon generieren**
```bash
uv run python create_icon.py
```
**Option 2: Eigenes Icon aus PNG erstellen**
```bash
uv run python create_icon.py ihr-logo.png
```
**Option 3: Manuell ICO-Datei platzieren**
1. Icon erstellen oder downloaden (`.ico` Format mit mehreren Größen)
2. Als `resources/icon.ico` speichern
3. Beim nächsten Build wird es automatisch verwendet
Das Icon wird automatisch verwendet für:
- Windows-Executable (DocuMentor.exe)
- Inno Setup Installer
- Desktop-Verknüpfungen
**Anforderungen:**
- Multi-Size ICO (16x16 bis 256x256 Pixel)
- Das `create_icon.py` Skript erstellt alle Größen automatisch
### Versionsinformationen
Versionsinformationen werden automatisch aus `pyproject.toml` generiert:
```bash
uv run python create_version_info.py
```
**Version ändern:**
In `pyproject.toml`:
```toml
version = "0.2.0"
```
Auch aktualisieren in:
- `installer.iss` (Zeile 13: `#define MyAppVersion`)
Dann Versionsinformationen neu generieren:
```bash
uv run python create_version_info.py
```
**Was enthalten die Versionsinformationen:**
- Dateiversion und Produktversion
- Beschreibung und Copyright
- Anwendungsname
- Wird in Windows Explorer angezeigt (Rechtsklick → Eigenschaften → Details)
### Build-Größe reduzieren
In `DocuMentor.spec` Module ausschließen:
```python
excludes=[
'tkinter',
'matplotlib',
'test',
'unittest',
],
```
### One-File Build (alles in einer .exe)
In `DocuMentor.spec` ändern:
```python
exe = EXE(
pyz,
a.scripts,
a.binaries, # Uncomment
a.zipfiles, # Uncomment
a.datas, # Uncomment
[], # Comment out
exclude_binaries=False, # Ändern
# ...
name='DocuMentor',
onefile=True, # Hinzufügen
)
# COLLECT auskommentieren oder entfernen
```
**Achtung**: One-File ist langsamer beim Start (3-5 Sekunden).
## Testing
### Lokales Testing (Linux/WSL)
```bash
# Build erstellen
uv run python build_windows.py
# Mit Wine testen
wine dist/DocuMentor/DocuMentor.exe
```
### Testing auf Windows
1. ZIP-Datei auf Windows-System kopieren
2. Entpacken
3. `DocuMentor.exe` starten
4. Features testen:
- [ ] Programmstart
- [ ] Einstellungsdialog öffnet beim ersten Start
- [ ] Projekt öffnen/erstellen
- [ ] Tree-Navigation
- [ ] XSL/XML-Dateien hinzufügen
- [ ] PDF-Generierung (mit konfigurierten Tools)
- [ ] PDF-Vergleich
## Troubleshooting
### "Module not found" beim Start
**Lösung A** — Einfache Python-Module: Hidden imports in `DocuMentor.spec` ergänzen:
```python
hiddenimports=[
'missing_module',
]
```
**Lösung B** — Packages mit nativen Extensions (Rust/C, z.B. `connectorx`):
`hiddenimports` allein reicht nicht, da PyInstaller die `__init__.py` ins PYZ-Archiv packt, aber die `.pyd`-Extension separat im Dateisystem erwartet. Stattdessen `collect_all` verwenden:
```python
from PyInstaller.utils.hooks import collect_all
cx_datas, cx_binaries, cx_hiddenimports = collect_all('problematic_package')
a = Analysis(
# ...
binaries=cx_binaries,
datas=cx_datas,
hiddenimports=cx_hiddenimports,
)
```
### Antivirus blockiert die .exe
**Ursache**: Unsigned executables werden oft als verdächtig eingestuft.
**Lösungen**:
1. Code-Signing-Zertifikat kaufen und verwenden
2. Bei Microsoft SmartScreen einreichen
3. Exception in Antivirus eintragen (für Tests)
### Executable ist zu groß (>200 MB)
**Lösungen**:
1. UPX-Kompression ist bereits aktiv
2. Ungenutzte Module excluden (siehe oben)
3. Virtual Environment aufräumen: `uv sync --no-dev`
### UI-Dateien nicht gefunden
**Problem**: `.ui` Dateien werden nicht gefunden.
**Lösung**: In `DocuMentor.spec` prüfen:
```python
datas=ui_files, # Muss gesetzt sein
```
Im Code müssen Ressource-Pfade PyInstaller-kompatibel aufgelöst werden:
```python
import sys
from pathlib import Path
if hasattr(sys, "_MEIPASS"):
res_path = Path(sys._MEIPASS) / "res" / "data.sql"
else:
res_path = Path(__file__).parents[3] / "src" / "res" / "data.sql"
```
## Automatisierung
### GitHub Actions (CI/CD)
```yaml
name: Build Windows Release
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- name: Install uv
uses: astral-sh/setup-uv@v1
- name: Build
run: uv run python build_windows.py
- name: Create Release
uses: softprops/action-gh-release@v1
with:
files: dist/*.zip
```
### Lokales Release-Skript
```bash
#!/bin/bash
# release.sh - Erstellt vollständiges Release
VERSION="0.1.0"
echo "Building DocuMentor v$VERSION..."
# 1. Build
uv run python build_windows.py
# 2. Installer (falls Inno Setup installiert)
if command -v iscc &> /dev/null; then
iscc installer.iss
echo "✓ Installer erstellt"
fi
echo ""
echo "Release v$VERSION fertig!"
echo " • ZIP: dist/DocuMentor-*-Windows.zip"
echo " • Setup: dist/installer/DocuMentor-Setup-$VERSION.exe"
```
## Distributions-Formate im Vergleich
| | ZIP | Setup.exe (Inno Setup) | MSI (WiX) |
|---|---|---|---|
| **Portabel** | Ja | Nein | Nein |
| **Installation nötig** | Nein | Ja | Ja |
| **Deinstallation** | Nein | Ja | Ja |
| **Start-Menü** | Nein | Ja | Ja |
| **GPO-Deployment** | Nein | Nein | Ja |
| **Silent Install** | — | Ja | Ja |
| **Rollback** | — | Nein | Ja |
| **Patch-Support** | — | Nein | Ja (.msp) |
## Externe Abhängigkeiten
DocuMentor benötigt diese Tools (NICHT im Installer enthalten):
1. **Java Runtime Environment (JRE) 11+** — Für Saxon und Apache FOP (https://adoptium.net/)
2. **Apache FOP** — XSL-FO zu PDF Konvertierung (https://xmlgraphics.apache.org/fop/)
3. **Saxon XSLT Prozessor** — XSLT 2.0/3.0 Transformationen (https://www.saxonica.com/)
4. **diff-pdf** — PDF-Vergleich (https://vslavik.github.io/diff-pdf/)
Nach der Installation müssen die Pfade zu diesen Tools in den DocuMentor-Einstellungen konfiguriert werden.
## Dokumentation für Endbenutzer
Die `dist/DocuMentor/README.txt` wird automatisch erstellt und enthält:
- Installationsanweisungen
- Liste der externen Abhängigkeiten
- Konfigurationshinweise
## Versionierung
Version in folgenden Dateien aktualisieren:
1. `pyproject.toml` — `version = "X.Y.Z"`
2. `installer.iss` — `#define MyAppVersion "X.Y.Z"`
Dann Versionsinformationen neu generieren:
```bash
uv run python create_version_info.py
```
## Lizenz und Rechtliches
- PySide6 (LGPL): Dynamische Verlinkung ist OK
- Polars (MIT): Unproblematisch
- Pydantic (MIT): Unproblematisch
Externe Tools (Saxon, FOP) haben eigene Lizenzen und müssen separat installiert werden.
+356
View File
@@ -0,0 +1,356 @@
# CLAUDE.md
Spreche mit mir auf Deutsch! (Communicate with me in German!)
## Projektübersicht
DocuMentor 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.
## Anvisiertes Nutzungsszenario
Der primäre Einsatz ist die kontinuierliche Weiterentwicklung von PDF-Dokumenten in Flexnow (Software zur Prüfungsverwaltung). Dabei handelt es sich beispielsweise um amtliche Urkunden, Zeugnisse und Bescheide.
Die Basis bilden etwa 100 XSL-Dateien. Die meisten sind mittels `<xsl:import/>` bzw. `<xsl:include/>` miteinander verknüpft (ähnlich der Klassen-Vererbung). Daher können sich Änderungen in einer XSL-Datei auf (unerwartet) viele andere auswirken. Um diese Auswirkungen im Auge zu behalten, wird DocuMentor entwickelt.
**Typischer Workflow:**
1. Entwickler führt benötigte Änderungen an den XSL-Dateien durch
2. Entwickler startet die Transformation im DocuMentor und begutachtet die generierte PDF-Diff
3. Prüfung: Wurden die richtigen PDF-Dateien geändert?
4. Prüfung: Hat die Änderung der XSL-Dateien die erhoffte Änderung in den PDF-Dateien ergeben?
Diese Schritte können sich mehrfach wiederholen.
Da der DocuMentor permanent im Hintergrund läuft, ist ein sparsamer Umgang mit RAM wichtig:
- Worker-Pools nach Verwendung herunterfahren
- Große Datenstrukturen frühzeitig freigeben
- Polars DataFrames statt Pandas (geringerer RAM-Verbrauch)
- Lazy Loading wo möglich
## 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
```
### Linting
```bash
uv run ruff check # Code-Style prüfen (Zeilenlänge: 120)
uv run ruff format # Code formatieren
```
### Tests
Dieses Projekt verwendet KEINE pytest/unittest-Frameworks. Tests sind standalone Python-Skripte:
```bash
uv run python test_hash_implementation.py # Hash-Tests
uv run python test_xml_hash_duplicate_detection.py # Duplikatserkennung
```
### Commit
Jedes mal bei Commit diese Skills nutzen:
- /license-check
- /version-bump
## Code-Style-Richtlinien
### Import-Organisation
Reihenfolge (keine Leerzeilen zwischen Gruppen):
```python
# 1. Standard Library
import os
import sys
import logging
from pathlib import Path
from typing import TYPE_CHECKING
# 2. Drittanbieter
from PySide6.QtCore import Qt, QThread, Signal
from PySide6.QtWidgets import QDialog, QMainWindow
from pydantic import BaseModel, Field
# 3. Lokale Imports (IMMER absolute Imports, KEINE relativen .imports)
from conf import app_settings, TreeNode, XslFile
from ui.MainWindow import MainWindow
```
- `TYPE_CHECKING` für zirkuläre Import-Vermeidung nutzen
- Keine relativen Imports (`.` oder `..`)
### Type Annotations
Moderne Union-Syntax verwenden:
```python
# RICHTIG
def transform(xml_path: Path, params: dict[str, str]) -> tuple[bool, str]:
result: str | None = None
files: list[Path] = []
# FALSCH
def transform(xml_path, params): # Keine Annotations
result: Optional[str] = None # Alte Union-Syntax
files: List[Path] = [] # Großgeschriebene Types
```
### Naming Conventions
```python
# Klassen: PascalCase
class SaxonWorkerPool:
# Funktionen/Methoden: snake_case
def transform_saxon(xml_file: Path) -> bool:
# Private Methoden: _snake_case mit Unterstrich
def _create_tree_item(self, node: TreeNode):
# Konstanten: UPPER_CASE
SAXON_WORKER_JAVA = """..."""
```
### Formatierung
- **Zeilenlänge:** 120 Zeichen (via Ruff konfiguriert)
- **Strings:** Bevorzugt Double-Quotes `"..."`, aber konsistent im File
- **Trailing Commas:** Bei mehrzeiligen Strukturen verwenden
### Error Handling
IMMER Logging statt `print()` verwenden:
```python
import logging
logger = logging.getLogger(__name__)
def transform(xml_path: Path) -> tuple[bool, str]:
try:
logger.info(f"Transformation gestartet: {xml_path}")
result = do_transform(xml_path)
return True, "Erfolg"
except FileNotFoundError as e:
error_msg = f"XML-Datei nicht gefunden: {xml_path}"
logger.error(error_msg)
return False, error_msg
except Exception as e:
error_msg = f"Fehler bei Transformation: {str(e)}"
logger.exception(error_msg) # Mit Stack Trace
return False, error_msg
```
- `logger.debug()` für Debugging-Infos
- `logger.info()` für normale Operationen
- `logger.warning()` für Warnungen
- `logger.error()` für Fehler ohne Stack Trace
- `logger.exception()` für Fehler MIT Stack Trace
- Fehlermeldungen auf Deutsch
### Docstrings
Google-Style auf Deutsch:
```python
def transform_xml_to_pdf(xml_path: Path, xsl_path: Path, output_dir: Path) -> tuple[bool, str]:
"""
Transformiert eine XML-Datei mit XSL zu PDF.
Args:
xml_path: Pfad zur XML-Eingabedatei
xsl_path: Pfad zum XSL-Stylesheet
output_dir: Zielverzeichnis für PDF-Ausgabe
Returns:
tuple[bool, str]: (Erfolg, Fehlermeldung oder Info-Text)
Raises:
FileNotFoundError: Wenn XML- oder XSL-Datei nicht existiert
"""
```
### 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
## 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
**UI-Import-Pattern:**
```python
from PySide6.QtWidgets import QDialog
from ui.JavaVmConfigDialog_ui import Ui_JavaVmConfigDialog
class JavaVmConfigDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.ui = Ui_JavaVmConfigDialog()
self.ui.setupUi(self)
# Signale NACH setupUi() verbinden
self.ui.browseButton.clicked.connect(self._browse_file)
```
- UI-Klassen NIEMALS direkt erben, nur als `self.ui` Member
- Alle Widgets über `self.ui.widgetName` zugreifen
- Signal-Verbindungen immer NACH `setupUi()` aufrufen
### 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
### XSL-Abhängigkeitsgraph (src/ui/XslDependencyDialog.py)
Interaktiver Dialog zur Visualisierung von `<xsl:import/>`- und `<xsl:include/>`-Abhängigkeiten zwischen XSL-Dateien:
- Sidebar mit Suchfilter zur Navigation
- Abhängigkeitsgraph-Darstellung via vis.js
- Parsing der XSL-Dateien mit lxml
### 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
### Datenbankintegration
PostgreSQL-Integration mit Polars und ConnectorX:
- Konfiguration wird im `PostgreSqlDb`-Modell mit SSL-Modus-Unterstützung gespeichert
- SQL-Abfragen werden asynchron via `DatabaseQueryThread` im `DatabaseMixin` ausgeführt
- Ergebnisse werden in Polars DataFrames geladen
### Thread-basierte Operationen
```python
from PySide6.QtCore import QThread, Signal
class HashCalculatorThread(QThread):
progress = Signal(int)
finished = Signal(dict)
def __init__(self, files: list[Path]):
super().__init__()
self.files = files
def run(self):
for i, file_path in enumerate(self.files):
hash_value = calculate_hash(file_path)
self.progress.emit(i + 1)
self.finished.emit(results)
# Verwendung
thread = HashCalculatorThread(xml_files)
thread.progress.connect(self._on_progress)
thread.finished.connect(self._on_finished)
thread.start() # NICHT run() direkt aufrufen!
```
## Wichtige Konventionen
### Deutsche Sprache
Die Codebasis verwendet Deutsch für:
- UI-Texte und Labels
- Kommentare und Dokumentation
- Variablennamen wo kontextuell passend
- Log-Meldungen
### 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
+112
View File
@@ -0,0 +1,112 @@
# -*- mode: python ; coding: utf-8 -*-
"""
PyInstaller Konfiguration für DocuMentor
Erstellt eine eigenständige Windows-Executable ohne Python-Installation
"""
import json
import sys
from importlib.metadata import PackageNotFoundError
from importlib.metadata import version as pkg_version
from pathlib import Path
from PyInstaller.utils.hooks import collect_all
block_cipher = None
# Projektpfad
project_root = Path(SPECPATH)
src_path = project_root / 'src'
# Versions-Snapshot erzeugen und ins Bundle einbetten
_packages_to_snapshot = [
"PySide6", "pydantic", "pydantic-settings", "pydantic-yaml",
"polars", "connectorx", "pyarrow", "psutil", "lxml",
"ruff", "pyinstaller", "pillow",
]
_versions_snapshot: dict[str, str] = {}
for _pkg in _packages_to_snapshot:
try:
_versions_snapshot[_pkg.lower()] = pkg_version(_pkg)
except PackageNotFoundError:
_versions_snapshot[_pkg.lower()] = ""
_versions_file = project_root / "versions.json"
_versions_file.write_text(json.dumps(_versions_snapshot), encoding="utf-8")
# connectorx komplett sammeln (Python-Code, native .pyd und Metadaten)
# PyInstaller erkennt connectorx nicht automatisch, da es zur Laufzeit
# von polars per importlib.import_module() geladen wird
cx_datas, cx_binaries, cx_hiddenimports = collect_all('connectorx')
# Alle UI-Dateien sammeln
ui_files = []
for ui_file in (src_path / 'ui').glob('*.ui'):
ui_files.append((str(ui_file), 'ui'))
# Ressource-Dateien (SQL, CSV) einbinden
res_files = []
for res_file in (src_path / 'res').glob('*'):
res_files.append((str(res_file), 'res'))
a = Analysis(
[str(src_path / 'main.py')],
pathex=[str(src_path)],
binaries=cx_binaries,
datas=ui_files + res_files + cx_datas + [
(str(project_root / 'pyproject.toml'), '.'),
(str(project_root / 'THIRD_PARTY_LICENSES.txt'), '.'),
(str(_versions_file), '.'),
(str(project_root / 'resources' / 'icon.ico'), 'resources'),
],
hiddenimports=[
'PySide6.QtCore',
'PySide6.QtGui',
'PySide6.QtWidgets',
'pydantic',
'pydantic_settings',
'pydantic_yaml',
'polars',
'pyarrow',
] + cx_hiddenimports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name='DocuMentor',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=False, # Keine Konsole anzeigen (GUI-Anwendung)
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon=str(project_root / 'resources' / 'icon.ico') if (project_root / 'resources' / 'icon.ico').exists() else None,
version=str(project_root / 'version_info.txt') if (project_root / 'version_info.txt').exists() else None,
)
coll = COLLECT(
exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='DocuMentor',
)
+86
View File
@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
<!-- Paket-Definition (ersetzt Product in v4) -->
<Package
Name="DocuMentor"
Version="1.7.3"
Manufacturer="Vitali Graf / Software- und Datenbankentwicklung"
UpgradeCode="F498B66C-726D-44AA-95F4-CB4FBDCEF26E"
Language="1031"
Compressed="yes"
InstallerVersion="500">
<MajorUpgrade
DowngradeErrorMessage="Eine neuere Version ist bereits installiert."
AllowSameVersionUpgrades="yes" />
<!-- Media Template -->
<MediaTemplate EmbedCab="yes" />
<!-- Feature-Definition -->
<Feature Id="ProductFeature" Title="DocuMentor" Level="1">
<ComponentGroupRef Id="ProductComponents" />
<ComponentRef Id="ApplicationShortcut" />
<ComponentRef Id="DesktopShortcut" />
</Feature>
<!-- Minimal UI (Standard Windows Installer Dialog) -->
<!-- Icon -->
<Icon Id="icon.ico" SourceFile="resources\icon.ico"/>
<Property Id="ARPPRODUCTICON" Value="icon.ico" />
<Property Id="ARPHELPLINK" Value="https://github.com/IhrRepo/DocuMentor" />
</Package>
<!-- Fragment: Verzeichnisstruktur -->
<Fragment>
<StandardDirectory Id="ProgramFilesFolder">
<Directory Id="INSTALLFOLDER" Name="DocuMentor" />
</StandardDirectory>
<StandardDirectory Id="ProgramMenuFolder">
<Directory Id="ApplicationProgramsFolder" Name="DocuMentor"/>
</StandardDirectory>
<StandardDirectory Id="DesktopFolder" />
</Fragment>
<!-- Fragment: Shortcuts -->
<Fragment>
<Component Id="ApplicationShortcut" Directory="ApplicationProgramsFolder" Guid="A498B66C-726D-44AA-95F4-CB4FBDCEF26E">
<Shortcut
Id="ApplicationStartMenuShortcut"
Name="DocuMentor"
Description="XSL-Transformations-Verwaltung"
Target="[INSTALLFOLDER]DocuMentor.exe"
WorkingDirectory="INSTALLFOLDER"
Icon="icon.ico" />
<RemoveFolder Id="CleanUpShortCut" On="uninstall"/>
<RegistryValue
Root="HKCU"
Key="Software\DocuMentor"
Name="installed"
Type="integer"
Value="1"
KeyPath="yes"/>
</Component>
<Component Id="DesktopShortcut" Directory="DesktopFolder" Guid="B498B66C-726D-44AA-95F4-CB4FBDCEF26E">
<Shortcut
Id="DesktopShortcutId"
Name="DocuMentor"
Target="[INSTALLFOLDER]DocuMentor.exe"
WorkingDirectory="INSTALLFOLDER"
Icon="icon.ico" />
<RegistryValue
Root="HKCU"
Key="Software\DocuMentor"
Name="desktopShortcut"
Type="integer"
Value="1"
KeyPath="yes"/>
</Component>
</Fragment>
</Wix>
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 [Ihr Name / Your Name]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+260
View File
@@ -0,0 +1,260 @@
# Lizenzübersicht für DocuMentor
## Verwendete Bibliotheken und ihre Lizenzen
### Python-Abhängigkeiten (aus pyproject.toml)
| Bibliothek | Version | Lizenz | Einschränkungen |
|------------|---------|--------|-----------------|
| **PySide6** | ≥6.9.1 | **LGPL-3.0 ODER GPL-2.0 ODER GPL-3.0** | ⚠️ Copyleft-Lizenz, siehe unten |
| Pydantic | ≥2.9.1 | MIT | ✅ Keine Einschränkungen |
| Pydantic-Settings | ≥2.9.1 | MIT | ✅ Keine Einschränkungen |
| Pydantic-YAML | ≥1.5.1 | MIT | ✅ Keine Einschränkungen |
| Polars | ≥1.31.0 | MIT | ✅ Keine Einschränkungen |
| ConnectorX | (via Polars) | MIT | ✅ Keine Einschränkungen |
| PyArrow | (via Polars) | Apache 2.0 | ✅ Keine Einschränkungen |
| pyqtdarktheme | ≥2.1.0 | MIT | ✅ Keine Einschränkungen |
| Ruff | ≥0.14.8 | MIT | ✅ Keine Einschränkungen (nur Dev) |
### Externe Tools (nicht in Python integriert)
| Tool | Lizenz | Verwendung | Einschränkungen |
|------|--------|------------|-----------------|
| **Saxon-HE** | Mozilla Public License 2.0 (MPL-2.0) | XSLT-Transformationen | ⚠️ Weak Copyleft |
| **Apache FOP** | Apache License 2.0 | PDF-Generierung | ✅ Keine Einschränkungen |
| diff-pdf | GPL-2.0 (vermutlich) | PDF-Vergleich | ⚠️ Nur externes Tool |
---
## Kritische Lizenz: PySide6 (LGPL-3.0)
### Was bedeutet LGPL-3.0 für DocuMentor?
**LGPL (Lesser GNU Public License)** ist eine "schwächere" Version der GPL und wurde speziell für Bibliotheken entwickelt.
#### ✅ Was ist ERLAUBT:
- **Kommerzielle Nutzung** - Du kannst DocuMentor verkaufen
- **Proprietärer Code** - Dein Code muss NICHT Open Source sein
- **Private Nutzung** - Keine Verpflichtungen
- **Dynamisches Linking** - In Python automatisch gegeben (via pip/import)
#### ⚠️ Was ist ERFORDERLICH:
1. **LGPL-Lizenztext beilegen** - Du musst die LGPL-Lizenz mit verteilen
2. **Copyright-Hinweise** - Erwähne PySide6 und Qt in deiner Software
3. **Nutzern Bibliotheks-Austausch ermöglichen** - Bei Python automatisch erfüllt, da:
- Nutzer können `pip install pyside6==andere-version` ausführen
- Python-Module sind dynamisch geladen
4. **Änderungen an PySide6 veröffentlichen** - Falls du PySide6 selbst änderst (sehr unwahrscheinlich)
#### ❌ Was ist NICHT erforderlich:
- **Dein eigener Code** muss NICHT unter LGPL stehen
- **Dein Source Code** muss NICHT veröffentlicht werden
- **Deine Änderungen** an DocuMentor müssen NICHT Open Source sein
### Praktische Umsetzung für Python-Anwendungen
Da Python-Pakete über pip installiert werden und dynamisch importiert werden, sind die LGPL-Anforderungen bereits erfüllt:
- ✅ Dynamisches Linking durch `import PySide6`
- ✅ Nutzer können andere PySide6-Versionen installieren
- ✅ Keine statische Kompilierung in Binary
**Fazit:** Du kannst DocuMentor unter praktisch jeder Lizenz veröffentlichen, auch proprietär.
---
## Empfohlene Lizenzen für DocuMentor
### Option 1: MIT License (EMPFOHLEN) ⭐
**Vorteile:**
- ✅ Einfachste und permissivste Lizenz
- ✅ Kompatibel mit LGPL und allen anderen verwendeten Lizenzen
- ✅ Erlaubt kommerzielle Nutzung ohne Einschränkungen
- ✅ Kurz und leicht verständlich
- ✅ Sehr verbreitet in der Open-Source-Community
- ✅ Gleiche Lizenz wie die meisten Dependencies (Pydantic, Polars, etc.)
**Nachteile:**
- Kein Patent-Schutz
- Keine Copyleft-Schutz (andere können proprietäre Forks erstellen)
**Wann verwenden:**
Wenn du maximale Freiheit für Nutzer möchtest und Open Source fördern willst, ohne strenge Bedingungen.
---
### Option 2: Apache License 2.0
**Vorteile:**
- ✅ Kompatibel mit allen Dependencies
- ✅ Expliziter Patent-Schutz
- ✅ Erlaubt kommerzielle Nutzung
- ✅ Professioneller für größere Projekte
- ✅ Gleiche Lizenz wie Apache FOP
**Nachteile:**
- Etwas komplexer als MIT
- Erfordert NOTICE-Datei für Änderungen
**Wann verwenden:**
Wenn du Patent-Schutz möchtest und ein professionelleres Image für Enterprise-Nutzer brauchst.
---
### Option 3: GPL-3.0 (Copyleft)
**Vorteile:**
- ✅ Kompatibel mit PySide6 (gleiche Lizenz)
- ✅ Starker Copyleft-Schutz - Alle Derivate müssen Open Source sein
- ✅ Schützt vor proprietären Forks
- ✅ Für reine Open-Source-Projekte ideal
**Nachteile:**
- ❌ Strenge Copyleft-Anforderungen
- ❌ Nutzer können DocuMentor nicht in proprietäre Software integrieren
- ❌ Weniger flexibel für kommerzielle Nutzung
**Wann verwenden:**
Wenn du sicherstellen willst, dass alle Modifikationen Open Source bleiben.
---
### Option 4: LGPL-3.0
**Vorteile:**
- ✅ Gleiche Lizenz wie PySide6 (konsistent)
- ✅ Schwächerer Copyleft als GPL
- ✅ Erlaubt Verwendung in proprietärer Software
**Nachteile:**
- Komplexere Anforderungen als MIT/Apache
- Weniger verbreitet für Anwendungen (eher für Bibliotheken)
**Wann verwenden:**
Wenn du einen Kompromiss zwischen GPL und MIT möchtest.
---
## Lizenz-Kompatibilitätsmatrix
```
DocuMentor Lizenz → Kompatibilität mit Dependencies
MIT ✅ Kompatibel mit allen
Apache 2.0 ✅ Kompatibel mit allen
GPL-3.0 ✅ Kompatibel mit allen
LGPL-3.0 ✅ Kompatibel mit allen
Proprietär ✅ Kompatibel mit allen (LGPL-Bedingungen beachten)
```
Alle genannten Lizenzen sind mit den verwendeten Bibliotheken kompatibel!
---
## Konkrete Empfehlung
### Für DocuMentor: **MIT License** ⭐
**Begründung:**
1. ✅ Die meisten Dependencies (Pydantic, Polars, pyqtdarktheme) sind MIT-lizenziert
2. ✅ LGPL-3.0 (PySide6) erlaubt die Verwendung in MIT-lizenzierter Software
3. ✅ Maximale Freiheit für Nutzer und Entwickler
4. ✅ Einfach und unkompliziert
5. ✅ Fördert Adoption und Beiträge
---
## Nächste Schritte
### 1. LICENSE-Datei erstellen
Erstelle eine `LICENSE` Datei im Root-Verzeichnis mit dem MIT-Lizenztext:
```
MIT License
Copyright (c) 2025 [Dein Name]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
### 2. pyproject.toml aktualisieren
```toml
[project]
name = "DocuMentor"
version = "0.1.0"
license = {text = "MIT"} # Füge diese Zeile hinzu
# ... rest bleibt gleich
```
### 3. Copyright-Hinweise hinzufügen
Füge in jede Quellcode-Datei einen Header ein:
```python
# Copyright (c) 2025 [Dein Name]
# Licensed under the MIT License
```
### 4. THIRD_PARTY_LICENSES.txt erstellen
Erstelle eine Datei, die alle verwendeten Bibliotheken und ihre Lizenzen auflistet:
```
DocuMentor verwendet folgende Open-Source-Bibliotheken:
1. PySide6 - LGPL-3.0 OR GPL-2.0 OR GPL-3.0
https://www.qt.io/qt-for-python
2. Pydantic - MIT License
https://github.com/pydantic/pydantic
3. Polars - MIT License
https://github.com/pola-rs/polars
... (alle weiteren Bibliotheken)
```
---
## Externe Tools (Saxon, Apache FOP)
**Wichtig:** Saxon-HE und Apache FOP sind **externe Programme**, die nicht in DocuMentor eingebettet sind.
- **Saxon-HE**: MPL-2.0 - Du darfst es verwenden, musst aber nicht deine Software unter MPL lizenzieren
- **Apache FOP**: Apache 2.0 - Kompatibel mit MIT
Da diese Tools nur **aufgerufen** werden (nicht eingebettet), hast du keine zusätzlichen Verpflichtungen.
---
## Quellen
- [PySide6 License](https://doc.qt.io/qtforpython-6/licenses.html)
- [Pydantic MIT License](https://github.com/pydantic/pydantic/blob/main/LICENSE)
- [Polars License](https://github.com/pola-rs/polars)
- [PyQtDarkTheme License](https://github.com/5yutan5/PyQtDarkTheme)
- [Apache FOP License](https://xmlgraphics.apache.org/fop/license.html)
- [Saxon License](https://github.com/Saxonica/Saxon-HE)
---
**Stand:** Januar 2025
**Autor:** Lizenzanalyse für DocuMentor
+154
View File
@@ -0,0 +1,154 @@
# DocuMentor
**Professionelle XSL-Transformations-Verwaltung und PDF-Generierung**
DocuMentor ist eine leistungsstarke PySide6-basierte Desktop-Anwendung zur Verwaltung und Validierung von XSL-Transformationen mit automatischer PDF-Generierung. Die Anwendung bietet eine intuitive GUI zur Konfiguration von Transformations-Toolchains (Saxon, Apache FOP, diff-pdf) und zur Verwaltung komplexer PDF-Generierungsprojekte mit PostgreSQL-Datenbankintegration.
## Features
### 🌳 Hierarchische Projektverwaltung
- Organisieren Sie Ihre XSL-Transformationen in einer übersichtlichen Baumstruktur
- Flexible Workflow-Definitionen mit verschachtelten Knoten
- Projektspezifische Konfiguration mit `project.yaml`
### ⚡ Asynchrone Batch-Verarbeitung
- Verarbeiten Sie große Mengen von XML-Dateien im Hintergrund
- Fortschrittsanzeige für lange Transformationen
- Parallelisierte XSLT-Transformationen durch eine Worker-Pool-Architektur
### 🔍 Intelligente Duplikatserkennung
- Automatische Hash-basierte Erkennung von identischen XML-Dateien (Blake2b)
- Verhindert Redundanzen und spart Speicherplatz
- Asynchrone Hash-Berechnung im Hintergrund
### 📄 PDF-Vergleichsansicht
- Drei-Panel-Ansicht (Referenz, Diff, Neu)
- Alpha-Blending für visuellen Vergleich
- Zoom- und Pan-Funktionalität
### 🗄️ PostgreSQL-Integration
- Nahtlose Datenbankanbindung mit Polars und ConnectorX
- Performante SQL-Abfragen und Datenverarbeitung
- SSL-Modus-Unterstützung
### 🛠️ Konfigurierbare Toolchains
- Flexible Verwaltung von Saxon, Apache FOP und diff-pdf
- Versionierung von Tools
- Plattformübergreifende Unterstützung (Linux, Windows, macOS)
### 🎨 Modernes UI
- Dark/Light-Theme-Unterstützung
- Drag-and-Drop für XML-Dateien
- Responsive und intuitive Benutzeroberfläche
## Installation
### Voraussetzungen
- Python 3.13 oder höher
- [uv](https://github.com/astral-sh/uv) Paketmanager
### Abhängigkeiten installieren
```bash
# Mit uv (empfohlen)
uv sync
# Oder mit pip
pip install -e .
```
### Externe Tools
Für die volle Funktionalität benötigen Sie:
- **Saxon-HE**: XSLT 3.0 Prozessor ([Download](https://www.saxonica.com/download/))
- **Apache FOP**: PDF-Generierung aus XSL-FO ([Download](https://xmlgraphics.apache.org/fop/download.html))
- **diff-pdf**: PDF-Vergleich ([GitHub](https://github.com/vslavik/diff-pdf))
- **OpenJDK/JRE**: für Saxon und Apache FOP. JDK empfohlen für Worker-Pools ([Eclipse Temurin](https://adoptium.net))
## Verwendung
### Anwendung starten
```bash
uv run python src/main.py
```
### Erste Start
Konfigurieren Sie Ihre Tools (Saxon, Apache FOP, diff-pdf) in den Einstellungen
### Projekt erstellen
1. Legen Sie ein neues Projekt an
2. Organisieren Sie XSL-Stylesheets und XML-Dateien in der Baumstruktur
3. Führen Sie Transformationen aus
### Konfiguration
Die Anwendung speichert Konfigurationsdateien an folgenden Orten:
- **Linux**: `~/.config/DocuMentor/config.json`
- **Windows**: `%APPDATA%\DocuMentor\config.json`
- **macOS**: `~/Library/Application Support/DocuMentor/config.json`
Projektdaten werden in `project.yaml` im jeweiligen Projektverzeichnis gespeichert.
## Entwicklung
### Code-Qualität
```bash
# Code-Style prüfen
uv run ruff check
# Code formatieren
uv run ruff format
```
### Tests
```bash
# Hash-Implementierung testen
uv run python test_hash_implementation.py
# Duplikatserkennung testen
uv run python test_xml_hash_duplicate_detection.py
```
### Architektur
- **PySide6**: Native Qt-basierte GUI
- **Pydantic**: Typsichere Konfigurationsverwaltung
- **Polars**: Lightning-fast DataFrame-Verarbeitung
- **Blake2b**: Kryptographische Hash-Funktion für Integritätsprüfung
Siehe [CLAUDE.md](CLAUDE.md) für detaillierte Entwicklerdokumentation.
## Lizenz
DocuMentor ist unter der [MIT License](LICENSE) lizenziert.
### Third-Party-Lizenzen
Diese Software verwendet folgende Open-Source-Bibliotheken:
- **PySide6** - LGPL-3.0 OR GPL-2.0 OR GPL-3.0
- **Pydantic** - MIT License
- **Polars** - MIT License
- **pyqtdarktheme** - MIT License
- Weitere siehe [THIRD_PARTY_LICENSES.txt](THIRD_PARTY_LICENSES.txt)
Externe Tools (separat zu installieren):
- **Saxon-HE** - Mozilla Public License 2.0
- **Apache FOP** - Apache License 2.0
Vollständige Lizenzanalyse: [LICENSES.md](LICENSES.md)
## Danksagungen
Vielen Dank an alle Entwickler der verwendeten Open-Source-Bibliotheken!
---
**DocuMentor** - Professionelle XSL-Transformations-Verwaltung für anspruchsvolle Projekte
+267
View File
@@ -0,0 +1,267 @@
================================================================================
THIRD PARTY LICENSES
================================================================================
DocuMentor verwendet die folgenden Open-Source-Bibliotheken und Tools.
Vielen Dank an alle Entwickler und Maintainer dieser Projekte!
================================================================================
Python-Abhängigkeiten
================================================================================
1. PySide6
Version: >=6.10.1
Lizenz: LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
Webseite: https://www.qt.io/qt-for-python
GitHub: https://github.com/qt/pyside-pyside-setup
Beschreibung: Qt for Python - Official Python bindings for Qt
Copyright: Copyright (C) The Qt Company Ltd.
2. Pydantic
Version: >=2.12.0
Lizenz: MIT License
Webseite: https://pydantic.dev
GitHub: https://github.com/pydantic/pydantic
Beschreibung: Data validation using Python type hints
Copyright: Copyright (c) 2017 to present Pydantic Services Inc.
3. Pydantic-Settings
Version: >=2.12.0
Lizenz: MIT License
GitHub: https://github.com/pydantic/pydantic-settings
Beschreibung: Settings management using Pydantic
Copyright: Copyright (c) 2023 Pydantic Services Inc.
4. Pydantic-YAML
Version: >=1.6.0
Lizenz: MIT License
GitHub: https://github.com/NowanIlfideme/pydantic-yaml
Beschreibung: YAML support for Pydantic models
Copyright: Copyright (c) 2020 Anatoly Makarevich
5. Polars
Version: >=1.37.0
Lizenz: MIT License
Webseite: https://pola.rs
GitHub: https://github.com/pola-rs/polars
Beschreibung: Lightning-fast DataFrame library
Copyright: Copyright (c) 2025 Ritchie Vink
6. ConnectorX (via Polars)
Lizenz: MIT License
GitHub: https://github.com/sfu-db/connector-x
Beschreibung: Fast database connector for DataFrames
Copyright: Copyright (c) 2021 SFU Database Group
7. PyArrow (via Polars)
Lizenz: Apache License 2.0
Webseite: https://arrow.apache.org/docs/python/
GitHub: https://github.com/apache/arrow
Beschreibung: Python library for Apache Arrow
Copyright: Copyright (c) 2016-2025 The Apache Software Foundation
8. psutil
Version: >=6.1.1
Lizenz: BSD-3-Clause License
GitHub: https://github.com/giampaolo/psutil
Beschreibung: Cross-platform lib for process and system monitoring
Copyright: Copyright (c) 2009 Giampaolo Rodola
9. lxml
Version: >=6.0.2
Lizenz: BSD-3-Clause License
Webseite: https://lxml.de/
GitHub: https://github.com/lxml/lxml
Beschreibung: Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API
Copyright: Copyright (c) 2004 Infrae. All rights reserved.
10. Ruff (Development)
Version: >=0.14.11
Lizenz: MIT License
GitHub: https://github.com/astral-sh/ruff
Beschreibung: An extremely fast Python linter and code formatter
Copyright: Copyright (c) 2022 Charlie Marsh
11. PyInstaller (Development)
Version: >=6.0.0
Lizenz: GPL-2.0 mit Bootloader-Ausnahme
Webseite: https://pyinstaller.org
GitHub: https://github.com/pyinstaller/pyinstaller
Beschreibung: Bundles Python applications into stand-alone executables
Copyright: Copyright (c) 2010-2025 PyInstaller Development Team
12. Pillow (Development)
Version: >=10.0.0
Lizenz: HPND License (Historical Permission Notice and Disclaimer)
Webseite: https://python-pillow.org
GitHub: https://github.com/python-pillow/Pillow
Beschreibung: Python Imaging Library (Fork)
Copyright: Copyright (c) 2010-2025 Jeffrey A. Clark and contributors
================================================================================
Eingebettete Bibliotheken
================================================================================
Diese Bibliotheken sind direkt im Quellcode von DocuMentor enthalten.
1. vis-network (vis.js)
Version: 9.1.9
Lizenz: Apache License 2.0 ODER MIT License (Dual-Lizenz)
Webseite: https://visjs.github.io/vis-network/
GitHub: https://github.com/visjs/vis-network
Beschreibung: A dynamic, browser-based network visualization library
Copyright: Copyright (c) 2011-2017 Almende B.V, http://almende.com
Copyright (c) 2017-2019 visjs contributors, https://github.com/visjs
Datei: src/res/vis-network.min.js
Hinweis: Wird inline in QWebEngineView für den XSL-Abhängigkeitsgraph verwendet
2. Feather Icons
Version: 4.29.2
Lizenz: MIT License
Webseite: https://feathericons.com
GitHub: https://github.com/feathericons/feather
Beschreibung: Simply beautiful open source icons
Copyright: Copyright (c) 2013-2017 Cole Bemis
Datei: src/res/icons/, src/res/resources_rc.py
Hinweis: SVG-Icons werden via Qt-Ressourcensystem eingebettet; Farbe wird zur Laufzeit aus der Qt-Palette gesetzt
================================================================================
Externe Tools (nicht eingebettet)
================================================================================
Diese Tools werden als externe Programme aufgerufen und sind nicht
Bestandteil der DocuMentor-Distribution. Sie müssen separat installiert werden.
1. Saxon-HE (Home Edition)
Lizenz: Mozilla Public License 2.0 (MPL-2.0)
Webseite: https://www.saxonica.com/
GitHub: https://github.com/Saxonica/Saxon-HE
Beschreibung: XSLT 3.0 and XQuery 3.1 processor
Copyright: Copyright (c) Saxonica Limited
Hinweis: Für XSLT-Transformationen verwendet
2. Apache FOP (Formatting Objects Processor)
Lizenz: Apache License 2.0
Webseite: https://xmlgraphics.apache.org/fop/
Beschreibung: XSL-FO to PDF converter
Copyright: Copyright (c) 1999-2025 The Apache Software Foundation
Hinweis: Für PDF-Generierung verwendet
3. diff-pdf
Lizenz: GPL-2.0 (vermutlich)
GitHub: https://github.com/vslavik/diff-pdf
Beschreibung: Tool for visually comparing PDF files
Hinweis: Optional für PDF-Vergleich verwendet
================================================================================
Lizenztexte
================================================================================
--------------------------------------------------------------------------------
MIT License
--------------------------------------------------------------------------------
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
--------------------------------------------------------------------------------
Apache License 2.0
--------------------------------------------------------------------------------
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
--------------------------------------------------------------------------------
BSD-3-Clause License (psutil)
--------------------------------------------------------------------------------
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
LGPL-3.0 / GPL-2.0 / GPL-3.0 (PySide6)
--------------------------------------------------------------------------------
PySide6 ist unter LGPL-3.0, GPL-2.0 oder GPL-3.0 lizenziert.
Die vollständigen Lizenztexte finden Sie unter:
LGPL-3.0: https://www.gnu.org/licenses/lgpl-3.0.html
GPL-2.0: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
GPL-3.0: https://www.gnu.org/licenses/gpl-3.0.html
Weitere Informationen: https://doc.qt.io/qtforpython-6/licenses.html
--------------------------------------------------------------------------------
Mozilla Public License 2.0 (Saxon-HE)
--------------------------------------------------------------------------------
Der vollständige Lizenztext der Mozilla Public License 2.0 ist verfügbar unter:
https://www.mozilla.org/en-US/MPL/2.0/
================================================================================
HINWEISE
================================================================================
1. PySide6 (LGPL-3.0):
DocuMentor verwendet PySide6 über dynamisches Linking (Python import).
Nutzer können die PySide6-Version über pip austauschen.
Der Quellcode von DocuMentor muss nicht unter LGPL veröffentlicht werden.
2. Externe Tools:
Saxon-HE, Apache FOP und diff-pdf sind separate Programme, die von
DocuMentor aufgerufen werden, aber nicht in die Distribution eingebettet sind.
Nutzer müssen diese Tools selbst installieren.
3. Aktualisierungen:
Bitte überprüfen Sie regelmäßig die Lizenzen der verwendeten Bibliotheken,
da sich diese ändern können.
================================================================================
Stand: Mai 2026
Erstellt für: DocuMentor v1.7.3
================================================================================
-8
View File
@@ -1,8 +0,0 @@
# 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.
+79
View File
@@ -0,0 +1,79 @@
"""
WiX MSI Build-Skript für DocuMentor (WiX v6)
Erstellt einen MSI-Installer aus dem PyInstaller Build.
"""
import subprocess
import sys
import tomllib
from pathlib import Path
def get_version(project_root: Path) -> str:
"""Liest die Versionsnummer aus pyproject.toml."""
pyproject_path = project_root / "pyproject.toml"
with pyproject_path.open("rb") as f:
data = tomllib.load(f)
return data["project"]["version"]
def build_msi():
"""Erstellt MSI-Installer mit WiX v6."""
project_root = Path(__file__).parent
dist_dir = project_root / "dist" / "DocuMentor"
if not dist_dir.exists():
print("FEHLER: PyInstaller Build nicht gefunden!")
print("Bitte zuerst ausführen: uv run python build_windows.py")
sys.exit(1)
version = get_version(project_root)
msi_output = project_root / "dist" / f"DocuMentor-{version}.msi"
print(f"DocuMentor Build gefunden: {dist_dir}")
print(f"Version: {version}")
print(f"MSI-Ausgabe: {msi_output}")
print()
# Schritt 1: ProductFiles.wxs generieren (ersetzt WiX heat)
print("Schritt 1/2: Generiere ProductFiles.wxs...")
result = subprocess.run(["uv", "run", "python", "generate_wix_files.py"], check=False)
if result.returncode != 0:
print("\nFEHLER: ProductFiles.wxs Generierung fehlgeschlagen!")
sys.exit(1)
print()
# Schritt 2: MSI kompilieren mit WiX v6
print("Schritt 2/2: Kompiliere MSI-Installer...")
try:
result = subprocess.run(
["wix", "build", "DocuMentor.wxs", "ProductFiles.wxs", "-o", str(msi_output)],
check=False,
)
except FileNotFoundError:
print("\nFEHLER: 'wix' wurde nicht gefunden!")
print("WiX v6 muss installiert sein. Installationsschritte:")
print(" 1. .NET SDK installieren: https://dot.net")
print(" 2. WiX als dotnet tool installieren:")
print(" dotnet tool install --global wix --version 6.*")
print(" 3. Neues Terminal öffnen (PATH aktualisieren)")
sys.exit(1)
if result.returncode != 0:
print("\nFEHLER: MSI-Kompilierung fehlgeschlagen!")
print("Stelle sicher, dass WiX v6 installiert ist:")
print(" dotnet tool install --global wix --version 6.*")
sys.exit(1)
print()
print(f"[OK] MSI erfolgreich erstellt: {msi_output}")
print()
print("Installation testen mit:")
print(f" msiexec /i \"{msi_output}\"")
if __name__ == "__main__":
build_msi()
+148
View File
@@ -0,0 +1,148 @@
#!/usr/bin/env python3
"""
Build-Skript für Windows-Distribution von DocuMentor
Erstellt:
1. Eigenständige Executable mit PyInstaller
2. Optional: ZIP-Archiv für portable Distribution
"""
import shutil
import subprocess
import sys
import tomllib
from pathlib import Path
def get_version(project_root: Path) -> str:
"""Liest die Versionsnummer aus pyproject.toml."""
with (project_root / "pyproject.toml").open("rb") as f:
return tomllib.load(f)["project"]["version"]
def main():
project_root = Path(__file__).parent
dist_dir = project_root / "dist"
build_dir = project_root / "build"
resources_dir = project_root / "resources"
icon_path = resources_dir / "icon.ico"
version_info_path = project_root / "version_info.txt"
print("=" * 60)
print("DocuMentor Windows Build")
print("=" * 60)
# 1. Icon und Versionsinformationen generieren
print("\n[1/6] Icon und Versionsinformationen generieren...")
# Icon erstellen falls nicht vorhanden
if not icon_path.exists():
print(" Erstelle Standard-Icon...")
try:
subprocess.run([sys.executable, "create_icon.py"], check=True, cwd=project_root)
print(" ✓ Icon erstellt")
except subprocess.CalledProcessError as e:
print(f" ✗ Icon-Erstellung fehlgeschlagen: {e}")
print(" ⚠ Fahre ohne Icon fort...")
else:
print(f" ✓ Icon vorhanden: {icon_path.name}")
# Versionsinformationen erstellen
print(" Erstelle Versionsinformationen...")
try:
subprocess.run([sys.executable, "create_version_info.py"], check=True, cwd=project_root)
print(" ✓ Versionsinformationen erstellt")
except subprocess.CalledProcessError as e:
print(f" ✗ Versionsinformationen-Erstellung fehlgeschlagen: {e}")
print(" ⚠ Fahre ohne Versionsinformationen fort...")
# 2. Cleanup alter Builds
print("\n[2/6] Cleanup alter Builds...")
if dist_dir.exists():
shutil.rmtree(dist_dir)
print(" ✓ dist/ gelöscht")
if build_dir.exists():
shutil.rmtree(build_dir)
print(" ✓ build/ gelöscht")
# 3. PyInstaller ausführen
print("\n[3/6] PyInstaller Build starten...")
try:
subprocess.run(
["pyinstaller", "--clean", "DocuMentor.spec"],
check=True,
cwd=project_root
)
print(" ✓ Build erfolgreich")
except subprocess.CalledProcessError as e:
print(f" ✗ Build fehlgeschlagen: {e}")
return 1
# 4. README für Distribution erstellen
print("\n[4/6] README erstellen...")
readme_content = """DocuMentor - XSL-Transformations-Verwaltung
============================================
Installation:
1. Entpacken Sie dieses Archiv in ein Verzeichnis Ihrer Wahl
2. Führen Sie DocuMentor.exe aus
Externe Abhängigkeiten (separat zu installieren):
- Java Runtime Environment (JRE) oder JDK
- Apache FOP (für PDF-Generierung)
- Saxon XSLT-Prozessor (JAR-Datei)
- diff-pdf (für PDF-Vergleiche)
Beim ersten Start werden Sie aufgefordert, die Pfade zu diesen
Tools in den Programmeinstellungen zu konfigurieren.
Konfiguration und Logs:
- Windows: %APPDATA%\\DocuMentor\\
- Konfiguration: config.json
- Logs: logs\\
Support:
Bei Fragen oder Problemen erstellen Sie bitte ein Issue auf GitHub.
"""
readme_path = dist_dir / "DocuMentor" / "README.txt"
readme_path.write_text(readme_content, encoding='utf-8')
print(" ✓ README.txt erstellt")
# 5. Icon ins dist-Verzeichnis kopieren (für Installer)
print("\n[5/6] Icon für Installer vorbereiten...")
if icon_path.exists():
dist_icon = dist_dir / "DocuMentor" / "icon.ico"
shutil.copy2(icon_path, dist_icon)
print(" ✓ Icon kopiert")
else:
print(" ⚠ Kein Icon vorhanden")
# 6. ZIP-Archiv erstellen
print("\n[6/6] ZIP-Archiv erstellen...")
version = get_version(project_root)
zip_name = f"DocuMentor-{version}"
zip_path = dist_dir / zip_name
shutil.make_archive(
str(zip_path),
'zip',
dist_dir,
'DocuMentor'
)
print(f"{zip_name}.zip erstellt")
print("\n" + "=" * 60)
print("Build abgeschlossen!")
print("=" * 60)
print(f"\nErgebnisse:")
print(f" • Executable: dist/DocuMentor/DocuMentor.exe")
print(f" • ZIP-Archiv: dist/{zip_name}.zip")
print("\nNächste Schritte:")
print(" 1. Testen Sie DocuMentor.exe auf einem Windows-System")
print(" 2. Optional: Erstellen Sie einen Installer mit Inno Setup")
return 0
if __name__ == "__main__":
sys.exit(main())
+164
View File
@@ -0,0 +1,164 @@
#!/usr/bin/env python3
"""
Icon-Generator für DocuMentor
Erstellt ein Icon aus einem PNG oder generiert ein Standard-Icon.
Unterstützt Windows (.ico) und verschiedene Größen.
Verwendung:
python create_icon.py # Generiert Standard-Icon
python create_icon.py source.png # Konvertiert PNG zu ICO
"""
import sys
from pathlib import Path
try:
from PIL import Image, ImageDraw, ImageFont
except ImportError:
print("Fehler: Pillow ist nicht installiert.")
print("Installation: uv pip install pillow")
sys.exit(1)
def create_default_icon(output_path: Path):
"""Erstellt ein Standard-Icon mit DocuMentor-Branding."""
sizes = [256, 128, 64, 48, 32, 16]
images = []
for size in sizes:
# Neues Bild erstellen mit Farbverlauf
img = Image.new('RGB', (size, size), color='white')
draw = ImageDraw.Draw(img)
# Hintergrund: Blau-Verlauf (vereinfacht als solides Blau)
bg_color = (41, 128, 185) # Professionelles Blau
draw.rectangle([0, 0, size, size], fill=bg_color)
# Dokument-Symbol (vereinfachte Darstellung)
margin = size // 8
doc_left = margin
doc_top = margin
doc_right = size - margin
doc_bottom = size - margin
# Weißes Dokument
draw.rectangle(
[doc_left, doc_top, doc_right, doc_bottom],
fill='white',
outline=(52, 73, 94),
width=max(1, size // 64)
)
# Ecke umgeknickt (rechts oben)
fold_size = size // 6
points = [
(doc_right - fold_size, doc_top),
(doc_right, doc_top + fold_size),
(doc_right - fold_size, doc_top + fold_size),
]
draw.polygon(points, fill=(220, 220, 220), outline=(52, 73, 94))
# Text-Linien im Dokument (nur bei größeren Icons)
if size >= 32:
line_margin = doc_left + size // 12
line_width = doc_right - doc_left - size // 6
line_count = min(3, size // 32)
line_spacing = (doc_bottom - doc_top - fold_size) // (line_count + 2)
for i in range(line_count):
y = doc_top + fold_size + line_spacing * (i + 1)
draw.rectangle(
[line_margin, y, line_margin + line_width, y + max(1, size // 128)],
fill=(52, 73, 94)
)
# "M" für Mentor (nur bei großen Icons)
if size >= 64:
try:
# Versuche System-Font zu verwenden
font_size = size // 4
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", font_size)
except:
# Fallback auf Default-Font
font = ImageFont.load_default()
text = "M"
# Zentrieren (grobe Schätzung)
bbox = draw.textbbox((0, 0), text, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
text_x = (size - text_width) // 2
text_y = doc_bottom - text_height - margin // 2
draw.text((text_x, text_y), text, fill=bg_color, font=font)
images.append(img)
# Als ICO speichern
images[0].save(
output_path,
format='ICO',
sizes=[(img.width, img.height) for img in images],
append_images=images[1:]
)
print(f"✓ Standard-Icon erstellt: {output_path}")
def convert_png_to_ico(source_path: Path, output_path: Path):
"""Konvertiert ein PNG-Bild zu einem Multi-Size ICO."""
try:
img = Image.open(source_path)
# Zu RGBA konvertieren falls nötig
if img.mode != 'RGBA':
img = img.convert('RGBA')
# Verschiedene Größen erstellen
sizes = [256, 128, 64, 48, 32, 16]
images = []
for size in sizes:
resized = img.resize((size, size), Image.Resampling.LANCZOS)
images.append(resized)
# Als ICO speichern
images[0].save(
output_path,
format='ICO',
sizes=[(img.width, img.height) for img in images],
append_images=images[1:]
)
print(f"✓ Icon erstellt aus {source_path.name}: {output_path}")
except Exception as e:
print(f"✗ Fehler beim Konvertieren: {e}")
sys.exit(1)
def main():
project_root = Path(__file__).parent
resources_dir = project_root / "resources"
resources_dir.mkdir(exist_ok=True)
output_ico = resources_dir / "icon.ico"
if len(sys.argv) > 1:
# PNG zu ICO konvertieren
source_path = Path(sys.argv[1])
if not source_path.exists():
print(f"Fehler: Datei nicht gefunden: {source_path}")
sys.exit(1)
convert_png_to_ico(source_path, output_ico)
else:
# Standard-Icon generieren
print("Erstelle Standard-Icon...")
create_default_icon(output_ico)
print(f"\nIcon gespeichert: {output_ico}")
print("Das Icon wird automatisch von PyInstaller und Inno Setup verwendet.")
if __name__ == "__main__":
main()
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env python3
"""
Generiert Windows-Versionsinformationen für PyInstaller
Liest Version aus pyproject.toml und erstellt version_info.txt
"""
import tomllib
from pathlib import Path
from datetime import datetime
def parse_version(version_str: str) -> tuple[int, int, int, int]:
"""Parst Version-String (z.B. '0.1.0') zu Tuple (0, 1, 0, 0)."""
parts = version_str.split('.')
major = int(parts[0]) if len(parts) > 0 else 0
minor = int(parts[1]) if len(parts) > 1 else 0
patch = int(parts[2]) if len(parts) > 2 else 0
build = 0 # Könnte aus Git-Commit-Count generiert werden
return (major, minor, patch, build)
def create_version_info(project_root: Path):
"""Erstellt version_info.txt für PyInstaller."""
# pyproject.toml lesen
pyproject_path = project_root / "pyproject.toml"
with open(pyproject_path, 'rb') as f:
pyproject = tomllib.load(f)
project = pyproject['project']
version = project['version']
name = project['name']
description = project['description']
# Version parsen
file_version = parse_version(version)
product_version = file_version
# Jahr für Copyright
year = datetime.now().year
# version_info.txt Content
version_info_content = f"""# UTF-8
#
# Generiert automatisch von create_version_info.py
# NICHT manuell bearbeiten!
#
VSVersionInfo(
ffi=FixedFileInfo(
# filevers und prodvers als Tuple: (1, 0, 0, 0)
filevers={file_version},
prodvers={product_version},
# Maske für gültige Bits in filevers und prodvers
mask=0x3f,
# Flags - kann VS_FF_DEBUG, VS_FF_PRERELEASE, etc. enthalten
flags=0x0,
# Betriebssystem - VOS_NT_WINDOWS32
OS=0x40004,
# Dateityp - VFT_APP (Anwendung)
fileType=0x1,
# Subtyp (nicht verwendet für VFT_APP)
subtype=0x0,
# Datumsstempel
date=(0, 0)
),
kids=[
StringFileInfo(
[
StringTable(
'040904B0', # Deutsch (0x0409 = Englisch, 0x0407 = Deutsch), Unicode
[StringStruct('CompanyName', 'Vitali Graf / Software- und Datenbankentwicklung'),
StringStruct('FileDescription', '{description}'),
StringStruct('FileVersion', '{version}'),
StringStruct('InternalName', '{name}'),
StringStruct('LegalCopyright', '© {year} Vitali Graf. Alle Rechte vorbehalten.'),
StringStruct('OriginalFilename', '{name}.exe'),
StringStruct('ProductName', '{name}'),
StringStruct('ProductVersion', '{version}')])
]),
VarFileInfo([VarStruct('Translation', [1033, 1200])]) # Englisch, Unicode
]
)
"""
# version_info.txt schreiben
version_info_path = project_root / "version_info.txt"
version_info_path.write_text(version_info_content, encoding='utf-8')
print("✓ version_info.txt erstellt")
print(f" Version: {version}")
print(f" Datei: {version_info_path}")
def main():
project_root = Path(__file__).parent
create_version_info(project_root)
if __name__ == "__main__":
main()
@@ -0,0 +1,69 @@
# Transformation ohne Force — Entscheidungslogik
Die zentrale Methode ist `is_up_to_date()` in transform.py:155-180. Sie basiert auf Modifikationszeiten (mtime), nicht auf Hashes.
## Ablauf
Die Prüfung erfolgt an zwei Stellen in der Pipeline:
1. Vor Saxon-Transformation (`transform_saxon()`) — transform.py:192-194
2. Vor PDF-Build (`build_pdf()`) — transform.py:322-324
Beide rufen `is_up_to_date()` auf, die prüft:
- Existiert die New-PDF?
- Ist die XML-Datei neuer als die New-PDF?
- Ist die XSL-Datei neuer als die New-PDF?
Wenn die New-PDF existiert und älter als alle Inputs ist → Transformation wird übersprungen mit `(True, "Übersprungen (aktuell)")`.
## Mermaid-Diagramm
```mermaid
flowchart TD
A["Transformation gestartet<br/>(force=False)"] --> B{"force == True?"}
B -- Ja --> EXEC["Transformation ausführen"]
B -- Nein --> C{"New-PDF existiert?"}
C -- Nein --> EXEC
C -- Ja --> D["mtime der New-PDF ermitteln"]
D --> E{"XML-Datei neuer<br/>als New-PDF?"}
E -- Ja --> EXEC
E -- Nein --> F{"XSL-Datei neuer<br/>als New-PDF?"}
F -- Ja --> EXEC
F -- Nein --> SKIP["Übersprungen (aktuell)<br/>return (True, 'Übersprungen')"]
EXEC --> S1["Schritt 1: Saxon<br/>XML → FO"]
S1 --> S1OK{"Saxon erfolgreich?"}
S1OK -- Nein --> FAIL["Pipeline abgebrochen"]
S1OK -- Ja --> S2CHECK{"force == True?<br/>(erneute Prüfung<br/>für build_pdf)"}
S2CHECK -- Ja --> S2["Schritt 2: FOP<br/>FO → PDF"]
S2CHECK -- Nein --> S2UP{"is_up_to_date()?"}
S2UP -- Ja --> S2SKIP["PDF-Build übersprungen"]
S2UP -- Nein --> S2
S2 --> S3["Schritt 3: diff-pdf<br/>PDF-Vergleich"]
S3 --> DONE["Pipeline abgeschlossen"]
style SKIP fill:#4CAF50,color:#fff
style EXEC fill:#2196F3,color:#fff
style FAIL fill:#f44336,color:#fff
style DONE fill:#4CAF50,color:#fff
style S2SKIP fill:#4CAF50,color:#fff
```
## Wichtige Details
- Keine Hash-basierte Prüfung: Die Skip-Logik nutzt ausschließlich `mtime`-Vergleiche, nicht die blake2b-Hashes (die werden nur für Änderungsverfolgung in der UI verwendet).
- Doppelte Prüfung: `is_up_to_date()` wird sowohl vor Saxon als auch vor FOP aufgerufen — theoretisch könnte Saxon ausgeführt, aber der PDF-Build übersprungen werden.
- Skip = Erfolg: Ein übersprungener Schritt gilt als erfolgreich `(True, ...)`, die Pipeline läuft weiter.
- Force-Aufruf: Über das Kontextmenü gibt es explizite Force-Methoden wie `_transform_all_xml_files_force()` in transformation.py.
## Erweiterung
```mermaid
flowchart TD
LOAD["Projekt geladen"] --> BUILD["XSL-Abhängigkeitsgraph aufbauen<br/>dict[Path, set[Path]]"]
BUILD --> CACHE["Im Speicher halten"]
TRANSFORM["is_up_to_date() aufgerufen"] --> CHECK{"Graph-Eintrag<br/>vorhanden?"}
CHECK -- Nein --> PARSE["XSL parsen, Imports auflösen,<br/>Eintrag erstellen"]
PARSE --> MTIME
CHECK -- Ja --> STALE{"mtime der XSL<br/>geändert seit letztem Parse?"}
STALE -- Ja --> PARSE
STALE -- Nein --> MTIME["mtime aller Abhängigkeiten<br/>gegen New-PDF prüfen"]
MTIME --> RESULT["Ergebnis"]
```
+274
View File
@@ -0,0 +1,274 @@
# Icon und Versionsinformationen für Windows-Build
## Übersicht
DocuMentor unterstützt professionelle Windows-Builds mit:
- **Anwendungs-Icon** in allen benötigten Größen
- **Windows-Versionsinformationen** (Datei-Eigenschaften)
- Automatische Integration in Build-Prozess
## Icon-System
### Automatische Icon-Generierung
Das Build-Skript generiert automatisch ein Standard-Icon, falls keins vorhanden ist:
```bash
uv run python build_windows.py
```
Falls `resources/icon.ico` nicht existiert, wird automatisch ein Standard-Icon mit DocuMentor-Branding erstellt.
### Manuelles Icon erstellen
#### Option 1: Standard-Icon
```bash
uv run python create_icon.py
```
Erstellt ein einfaches Icon mit:
- Blauem Hintergrund (professionelles Blau: #2980B9)
- Weißem Dokument-Symbol
- "M" für Mentor (bei großen Icons)
- Umgeknickter Ecke
- Mehreren Größen (16×16 bis 256×256)
#### Option 2: Aus eigenem PNG
```bash
uv run python create_icon.py mein-logo.png
```
Konvertiert ein PNG-Bild zu einem Multi-Size Windows-Icon:
- Unterstützt Transparenz
- Erstellt alle benötigten Größen
- Optimiert für verschiedene Bildschirmauflösungen
**PNG-Anforderungen:**
- Idealerweise 256×256 Pixel oder größer
- Quadratisches Format
- PNG oder JPEG Format
- Transparenter Hintergrund empfohlen
### Icon-Größen
Das ICO-Format enthält folgende Auflösungen:
| Größe | Verwendung |
|---------|--------------------------------------|
| 256×256 | Windows 7+, Taskleiste, große Icons |
| 128×128 | Windows 7+, große Icons |
| 64×64 | Hohe DPI-Displays |
| 48×48 | Standard Desktop-Icon |
| 32×32 | Explorer Details-Ansicht |
| 16×16 | Kleinstes Icon, Titelleiste |
### Wo wird das Icon verwendet?
- **DocuMentor.exe** - Anwendungs-Icon
- **Setup.exe** - Installer-Icon (Inno Setup)
- **Desktop-Verknüpfung** - Erstellt beim Installieren
- **Start-Menü** - Windows-Programmgruppe
- **Taskleiste** - Beim Ausführen
- **Deinstallations-Programm** - System-Einstellungen
## Versionsinformationen
### Automatische Generierung
Versionsinformationen werden automatisch vom Build-Skript generiert:
```bash
uv run python build_windows.py
```
### Manuelle Generierung
```bash
uv run python create_version_info.py
```
### Inhalt der Versionsinformationen
Die `version_info.txt` enthält:
```
FileVersion: 0.1.0.0
ProductVersion: 0.1.0.0
CompanyName: Ihr Name/Organisation
FileDescription: Professionelle XSL-Transformations-Verwaltung und PDF-Generierung
InternalName: DocuMentor
LegalCopyright: © 2026 Ihr Name. Alle Rechte vorbehalten.
OriginalFilename: DocuMentor.exe
ProductName: DocuMentor
```
### Version aus pyproject.toml
Die Version wird automatisch aus `pyproject.toml` gelesen:
```toml
[project]
name = "DocuMentor"
version = "0.1.0"
description = "Professionelle XSL-Transformations-Verwaltung und PDF-Generierung"
```
### Version ändern
**Schritt 1:** Version in `pyproject.toml` ändern:
```toml
version = "0.2.0"
```
**Schritt 2:** Version in `installer.iss` ändern (Zeile 13):
```iss
#define MyAppVersion "0.2.0"
```
**Schritt 3:** Build neu ausführen:
```bash
uv run python build_windows.py
```
Die Versionsinformationen werden automatisch neu generiert.
### Versionsnummern-Schema
DocuMentor verwendet [Semantic Versioning](https://semver.org/lang/de/):
```
MAJOR.MINOR.PATCH
0.1.0 → Erste Beta-Version
1.0.0 → Erste stabile Version
1.1.0 → Neue Features
1.1.1 → Bugfixes
2.0.0 → Breaking Changes
```
### Windows-Eigenschaften anzeigen
Nach dem Build können Sie die Versionsinformationen in Windows anzeigen:
1. Rechtsklick auf `DocuMentor.exe`
2. **Eigenschaften** auswählen
3. Tab **Details** öffnen
Dort sehen Sie:
- Dateiversion
- Produktversion
- Beschreibung
- Copyright
- Produktname
- Original-Dateiname
## Integration in Build-Prozess
### DocuMentor.spec
```python
exe = EXE(
# ...
icon=str(project_root / 'resources' / 'icon.ico') if (project_root / 'resources' / 'icon.ico').exists() else None,
version=str(project_root / 'version_info.txt') if (project_root / 'version_info.txt').exists() else None,
)
```
**Automatische Erkennung:**
- Icon wird verwendet, falls vorhanden
- Versionsinformationen werden verwendet, falls vorhanden
- Build funktioniert auch ohne Icon/Version (mit Warnung)
### installer.iss
```iss
SetupIconFile=dist\DocuMentor\icon.ico
UninstallDisplayIcon={app}\DocuMentor.exe
```
Das Icon wird automatisch vom `build_windows.py` nach `dist/DocuMentor/` kopiert.
## Anpassungen
### Company Name / Copyright
In `create_version_info.py` (Zeile ~65):
```python
StringStruct('CompanyName', 'Ihr Name/Organisation'),
StringStruct('LegalCopyright', '© {year} Ihr Name. Alle Rechte vorbehalten.'),
```
Ändern Sie "Ihr Name/Organisation" auf Ihren tatsächlichen Namen oder Firmennamen.
### Icon-Design
Falls Sie das Standard-Icon anpassen möchten, bearbeiten Sie `create_icon.py`:
**Farben ändern** (Zeile ~31):
```python
bg_color = (41, 128, 185) # Blau - ändern Sie RGB-Werte
```
**Symbol ändern:**
- Bearbeiten Sie die `create_default_icon()` Funktion
- Oder erstellen Sie Ihr eigenes Icon in einem Grafikprogramm
## Best Practices
### Icon-Design
1. **Einfach und klar**: Funktioniert auch bei 16×16 Pixel
2. **Hoher Kontrast**: Gut lesbar auf hellem und dunklem Hintergrund
3. **Professionell**: Passend zum Business-Kontext
4. **Wiedererkennbar**: Symbolisiert die Anwendung
### Versionierung
1. **Semantische Versionierung**: MAJOR.MINOR.PATCH
2. **Vor jedem Release aktualisieren**
3. **Git-Tags verwenden**: `git tag v0.1.0`
4. **GUID beibehalten**: Nie die Inno Setup GUID ändern bei Updates!
## Troubleshooting
### Icon wird nicht angezeigt
**Problem**: DocuMentor.exe zeigt kein Icon
**Lösungen:**
1. Prüfen ob `resources/icon.ico` existiert
2. Build neu ausführen: `uv run python build_windows.py`
3. Windows Icon-Cache löschen und neu starten
### Versionsinformationen fehlen
**Problem**: Eigenschaften → Details zeigt keine Informationen
**Lösungen:**
1. Prüfen ob `version_info.txt` existiert
2. `uv run python create_version_info.py` ausführen
3. Build neu ausführen
### Pillow-Fehler beim Icon-Erstellen
**Problem**: `ImportError: No module named 'PIL'`
**Lösung:**
```bash
uv sync --all-groups
```
Dies installiert Pillow automatisch.
## Weiterführende Informationen
- **PyInstaller Icon-Dokumentation**: https://pyinstaller.org/en/stable/usage.html#icons
- **Windows ICO Format**: https://en.wikipedia.org/wiki/ICO_(file_format)
- **Semantic Versioning**: https://semver.org/lang/de/
- **Pillow Dokumentation**: https://pillow.readthedocs.io/
+308
View File
@@ -0,0 +1,308 @@
# XML-Hash-Duplikatserkennung und intelligente Dateinamen-Verwaltung
## Übersicht
Diese Dokumentation beschreibt die erweiterte Funktionalität zur Hash-basierten Duplikatserkennung von XML-Dateien und intelligenten Dateinamen-Verwaltung in der XSL-Validator-Anwendung.
## Neue Funktionalitäten
### 1. Hash-basierte Duplikatserkennung
Beim Hinzufügen neuer XML-Dateien wird automatisch geprüft, ob bereits eine Datei mit identischem Inhalt (basierend auf blake2b-Hash) im Projekt vorhanden ist.
**Vorteile:**
- Vermeidung von Datei-Duplikaten
- Automatische Zuordnung vorhandener Dateien
- Speicherplatz-Optimierung
- Konsistente Datenintegrität
### 2. Intelligente Dateinamen-Verwaltung
Bei Dateinamen-Konflikten werden automatisch alternative Namen im Format `datei_1.xml`, `datei_2.xml`, etc. generiert.
**Features:**
- Automatische Generierung alternativer Dateinamen
- Benutzerfreundlicher Auswahl-Dialog
- Vermeidung von Überschreibungen
- Konsistente Namenskonventionen
### 3. Nahtlose Integration
Die neuen Funktionalitäten sind vollständig in bestehende Workflows integriert:
- **Drag & Drop**: Automatische Hash-Prüfung beim Ziehen von XML-Dateien
- **Kontextmenü**: Hash-Prüfung beim manuellen Hinzufügen über "XML-Datei hinzufügen"
## Technische Implementierung
### Kern-Architektur
```python
def _assign_xml_to_xsl_nodes(self, xml_file_path: Path, selected_xsl_nodes: list):
# 1. Hash berechnen
file_hash = self._calculate_hash_for_file(xml_file_path)
# 2. Duplikatsprüfung
existing_xml = self._find_xml_file_by_hash(file_hash)
if existing_xml:
# 3. Automatische Zuordnung bei Hash-Match
self._assign_existing_xml_to_nodes(existing_xml, selected_xsl_nodes)
else:
# 4. Neue Datei verarbeiten
self._process_new_xml_file(xml_file_path, selected_xsl_nodes, file_hash)
```
### Neue Hilfsmethoden
#### Hash-Vergleich und Suche
```python
def _get_all_project_xml_files(self) -> List[XmlFile]:
"""Sammelt alle XML-Dateien aus dem gesamten Projekt."""
def _find_xml_file_by_hash(self, target_hash: str) -> XmlFile | None:
"""Sucht XML-Datei mit spezifischem Hash im Projekt."""
def _calculate_hash_for_file(self, file_path: Path) -> str | None:
"""Berechnet blake2b-Hash für eine Datei."""
```
#### Dateinamen-Verwaltung
```python
def _generate_alternative_filename(self, original_path: Path, xml_dir: Path) -> Path:
"""Generiert alternative Dateinamen im Format: datei_1.xml, datei_2.xml, ..."""
def _is_filename_used_in_project(self, relative_xml_path: Path) -> bool:
"""Prüft ob Dateiname bereits im Projekt verwendet wird."""
def _show_filename_selection_dialog(self, original_name: str, alternative_paths: List[Path]) -> Path | None:
"""Zeigt Dialog zur Auswahl alternativer Dateinamen."""
```
#### Verarbeitungslogik
```python
def _assign_existing_xml_to_nodes(self, existing_xml: XmlFile, selected_xsl_nodes: list):
"""Ordnet vorhandene XML-Datei (Hash-Match) den XSL-Knoten zu."""
def _process_new_xml_file(self, xml_file_path: Path, selected_xsl_nodes: list, file_hash: str | None):
"""Verarbeitet neue XML-Datei (kein Hash-Match)."""
```
## Benutzer-Workflows
### Workflow 1: Hash-Duplikat gefunden
1. Benutzer fügt XML-Datei hinzu (Drag&Drop oder Kontextmenü)
2. System berechnet Hash der neuen Datei
3. Hash-Match mit vorhandener Datei gefunden
4. **Automatische Zuordnung**: Vorhandene Datei wird den ausgewählten XSL-Knoten zugeordnet
5. Erfolgsmeldung: "XML-Datei mit gleichem Inhalt bereits vorhanden - automatisch zugeordnet"
### Workflow 2: Neue Datei, Dateiname-Konflikt
1. Benutzer fügt XML-Datei hinzu
2. Kein Hash-Match gefunden (neue Datei)
3. Dateiname bereits vorhanden
4. **Dialog angezeigt**: Auswahl alternativer Dateinamen
5. Benutzer wählt gewünschten Namen
6. Datei wird kopiert und zugeordnet
7. Erfolgsmeldung mit Hinweis auf Umbenennung
### Workflow 3: Neue Datei, kein Konflikt
1. Benutzer fügt XML-Datei hinzu
2. Kein Hash-Match gefunden
3. Dateiname verfügbar
4. **Direkte Verarbeitung**: Datei wird kopiert und zugeordnet
5. Standard-Erfolgsmeldung
## Benutzeroberfläche
### Hash-Duplikat Dialog
```
┌─────────────────────────────────────────┐
│ XML-Datei zugeordnet │
├─────────────────────────────────────────┤
│ Eine XML-Datei mit gleichem Inhalt war │
│ bereits im Projekt vorhanden. │
│ │
│ Die vorhandene Datei 'dokument.xml' │
│ wurde automatisch zu 2 XSL-Knoten │
│ zugeordnet. │
├─────────────────────────────────────────┤
│ [OK] │
└─────────────────────────────────────────┘
```
### Dateiname-Auswahl Dialog
```
┌─────────────────────────────────────────┐
│ Dateiname auswählen │
├─────────────────────────────────────────┤
│ Eine Datei mit dem Namen 'test.xml' │
│ existiert bereits. │
│ │
│ Bitte wählen Sie einen alternativen │
│ Dateinamen: │
│ │
│ ○ test_1.xml │
│ ● test_2.xml │
│ ○ test_3.xml │
│ ○ test_4.xml │
├─────────────────────────────────────────┤
│ [OK] [Abbrechen] │
└─────────────────────────────────────────┘
```
## Performance-Optimierungen
### Hash-Berechnung
- **Synchrone Berechnung** für neue Dateien (akzeptable Verzögerung)
- **Effiziente blake2b-Implementierung** aus Python's hashlib
- **Caching** von Hash-Werten in XmlFile-Objekten
### Projekt-weite Suche
- **Einmalige Sammlung** aller XML-Dateien pro Operation
- **Optimierte Rekursion** durch die Node-Struktur
- **Duplikat-Vermeidung** bei der Sammlung
### Dateinamen-Generierung
- **Sequenzielle Suche** mit Sicherheitsgrenze (max. 1000 Versuche)
- **Fallback-Mechanismus** mit Zeitstempel
- **Kombinierte Prüfung** von physischer Existenz und Projekt-Verwendung
## Fehlerbehandlung
### Hash-Berechnung Fehler
```python
try:
file_hash = self._calculate_hash_for_file(xml_file_path)
except Exception as e:
logger.error(f"Hash-Berechnung fehlgeschlagen: {e}")
# Fortsetzung ohne Hash (Fallback-Verhalten)
```
### Datei-Zugriff Fehler
```python
if not file_path.exists():
logger.warning(f"Datei nicht gefunden: {file_path}")
return None
```
### Dialog-Fehler
```python
try:
selected_path = self._show_filename_selection_dialog(...)
except Exception as e:
logger.error(f"Dialog-Fehler: {e}")
# Fallback: Ersten alternativen Namen verwenden
return alternative_paths[0] if alternative_paths else None
```
## Logging
Das System verwendet strukturiertes Logging für alle Operationen:
```python
logger.info(f"Hash-Duplikat gefunden: {existing_xml.xml} hat gleichen Hash wie {xml_file_path.name}")
logger.debug(f"Hash-Vergleich: {len(xml_files)} XML-Dateien im Projekt gefunden")
logger.warning(f"Hash-Berechnung für {xml_file_path} fehlgeschlagen")
logger.error(f"Fehler beim Zuordnen der XML-Datei: {str(e)}")
```
## Testing
### Umfassende Test-Suite
Die Implementierung wird durch eine umfassende Test-Suite validiert:
```bash
uv run python test_xml_hash_duplicate_detection.py
```
**Test-Abdeckung:**
- Hash-Berechnung und -Konsistenz
- XmlFile-Modell mit Hash-Unterstützung
- Duplikatserkennung-Logik
- Alternative Dateinamen-Generierung
- Integration Workflow
### Test-Ergebnisse
```
=== Test: Hash-Berechnung ===
[OK] Hash-Berechnung funktioniert korrekt
=== Test: XmlFile-Modell mit Hash ===
[OK] XmlFile-Modell mit Hash funktioniert korrekt
=== Test: Duplikatserkennung-Logik ===
[OK] Duplikatserkennung-Logik funktioniert korrekt
=== Test: Alternative Dateinamen-Generierung ===
[OK] Alternative Dateinamen-Generierung funktioniert korrekt
=== Test: Integration Workflow ===
[OK] Integration Workflow funktioniert korrekt
[SUCCESS] Alle Tests erfolgreich abgeschlossen!
```
## Kompatibilität
### Rückwärtskompatibilität
- **Bestehende Projekte** funktionieren unverändert
- **Vorhandene XML-Dateien** ohne Hash werden automatisch nachberechnet
- **Keine Breaking Changes** in der API
### Datenformat
- **XmlFile.hashsum** ist optional (kann None sein)
- **Automatische Migration** beim Projektladen
- **Graceful Degradation** bei Hash-Fehlern
## Wartung und Erweiterung
### Konfigurierbarkeit
Die Implementierung kann einfach erweitert werden:
```python
# Verschiedene Hash-Algorithmen
def _calculate_hash(self, file_path: Path, algorithm: str = "blake2b"):
if algorithm == "blake2b":
return self._calculate_blake2b_hash(file_path)
elif algorithm == "sha256":
return self._calculate_sha256_hash(file_path)
# Konfigurierbare Dateinamen-Formate
def _generate_alternative_filename(self, original_path: Path, format_pattern: str = "{name}_{counter}{ext}"):
# Implementierung mit konfigurierbaren Mustern
```
### Monitoring
```python
# Performance-Metriken
start_time = time.time()
# ... Operation ...
duration = time.time() - start_time
logger.info(f"Hash-Vergleich abgeschlossen in {duration:.3f}s")
```
## Changelog
### Version 1.0.0 (2025-01-20)
- ✅ Hash-basierte Duplikatserkennung im gesamten Projekt
- ✅ Automatische Zuordnung bei Hash-Match
- ✅ Intelligente Dateinamen-Generierung (datei_1.xml Format)
- ✅ Integration in Drag&Drop und Kontextmenü
- ✅ Benutzerfreundliche Dateiname-Auswahl-Dialoge
- ✅ Umfassende Test-Suite
- ✅ Strukturiertes Logging
- ✅ Fehlerbehandlung und Fallback-Mechanismen
## Fazit
Die erweiterte XML-Hash-Duplikatserkennung bietet eine robuste, benutzerfreundliche Lösung für die intelligente Verwaltung von XML-Dateien in XSL-Validator-Projekten. Die Implementierung ist vollständig getestet, performant und nahtlos in bestehende Workflows integriert.
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env python3
"""
GUID-Generator für Inno Setup
Generiert eine eindeutige GUID für die AppId in installer.iss
"""
import uuid
import sys
def main():
# GUID generieren
guid = uuid.uuid4()
guid_str = f"{{{{{str(guid).upper()}}}}}"
print("=" * 60)
print("GUID für Inno Setup AppId")
print("=" * 60)
print()
print("Generierte GUID:")
print(f" {guid_str}")
print()
print("Anleitung:")
print("1. Kopieren Sie die GUID oben")
print("2. Öffnen Sie installer.iss")
print("3. Suchen Sie nach 'AppId={{' (Zeile ~22)")
print("4. Ersetzen Sie die Beispiel-GUID mit Ihrer neuen GUID")
print()
print("Beispiel:")
print(f" AppId={guid_str}")
print()
print("WICHTIG:")
print("- Die GUID sollte nur EINMAL beim ersten Setup generiert werden")
print("- Ändern Sie die GUID NICHT bei Updates, sonst wird die App")
print(" als separate Anwendung installiert!")
print("=" * 60)
if __name__ == "__main__":
main()
+184
View File
@@ -0,0 +1,184 @@
"""
WiX ProductFiles.wxs Generator
Generiert automatisch eine WXS-Datei mit allen Dateien aus dist/DocuMentor.
Ersetzt die veraltete 'wix heat' Funktionalität für WiX v6.
"""
import uuid
from pathlib import Path
from xml.etree import ElementTree as ET
def generate_guid() -> str:
"""Generiert eine neue GUID."""
return str(uuid.uuid4()).upper()
def sanitize_id(name: str) -> str:
"""
Macht einen String WiX-konform für IDs.
WiX erlaubt nur: A-Z, a-z, 0-9, _, .
Darf nicht mit Zahl beginnen.
"""
# Ersetze illegale Zeichen
sanitized = name.replace("-", "_").replace(" ", "_").replace(".", "_")
# Entferne alle anderen nicht erlaubten Zeichen
allowed = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_"
sanitized = "".join(c if c in allowed else "_" for c in sanitized)
# Darf nicht mit Zahl beginnen
if sanitized and sanitized[0].isdigit():
sanitized = f"_{sanitized}"
return sanitized
def create_wix_fragment(dist_dir: Path) -> ET.Element:
"""
Erstellt ein WiX Fragment mit allen Dateien aus dem dist Verzeichnis.
Args:
dist_dir: Pfad zum dist/DocuMentor Verzeichnis
Returns:
ET.Element: Wix Root-Element mit Fragment
"""
# Namespace (WiX v4+)
ns = "http://wixtoolset.org/schemas/v4/wxs"
ET.register_namespace("", ns)
# Root Element
wix = ET.Element(f"{{{ns}}}Wix")
fragment = ET.SubElement(wix, f"{{{ns}}}Fragment")
component_group = ET.SubElement(
fragment, f"{{{ns}}}ComponentGroup", {"Id": "ProductComponents", "Directory": "INSTALLFOLDER"}
)
# Sammle alle Dateien
all_files = sorted(dist_dir.rglob("*"))
files_only = [f for f in all_files if f.is_file()]
print(f"Gefunden: {len(files_only)} Dateien")
# Gruppiere nach Verzeichnis
dirs_dict: dict[Path, list[Path]] = {}
for file in files_only:
rel_dir = file.parent.relative_to(dist_dir)
if rel_dir not in dirs_dict:
dirs_dict[rel_dir] = []
dirs_dict[rel_dir].append(file)
# Erstelle Directory Fragments
dir_fragment = ET.SubElement(wix, f"{{{ns}}}Fragment")
# INSTALLFOLDER ist bereits in DocuMentor.wxs definiert
directory_ref = ET.SubElement(dir_fragment, f"{{{ns}}}DirectoryRef", {"Id": "INSTALLFOLDER"})
# Erstelle Verzeichnisstruktur
created_dirs = {"INSTALLFOLDER": directory_ref}
for rel_dir in sorted(dirs_dict.keys()):
if rel_dir == Path("."):
continue
parts = rel_dir.parts
parent_id = "INSTALLFOLDER"
for i, part in enumerate(parts):
current_path = Path(*parts[: i + 1])
dir_id = f"Dir_{sanitize_id(current_path.as_posix().replace('/', '_'))}"
if dir_id not in created_dirs:
parent_elem = created_dirs[parent_id]
new_dir = ET.SubElement(parent_elem, f"{{{ns}}}Directory", {"Id": dir_id, "Name": part})
created_dirs[dir_id] = new_dir
parent_id = dir_id
else:
parent_id = dir_id
# Füge Komponenten hinzu
component_counter = 0
for rel_dir, files in sorted(dirs_dict.items()):
if rel_dir == Path("."):
dir_id = "INSTALLFOLDER"
else:
dir_id = f"Dir_{sanitize_id(rel_dir.as_posix().replace('/', '_'))}"
# Erstelle eine Komponente pro Verzeichnis
component_id = f"Component_{sanitize_id(dir_id)}"
component_counter += 1
component = ET.SubElement(
component_group, f"{{{ns}}}Component", {"Id": component_id, "Directory": dir_id, "Guid": generate_guid()}
)
# Füge alle Dateien des Verzeichnisses hinzu
for idx, file in enumerate(files):
file_id = f"File_{sanitize_id(component_id)}_{idx}"
# Absoluter Pfad für WiX
source_path = str(file).replace("/", "\\")
file_attribs = {"Id": file_id, "Source": source_path, "Name": file.name}
# Erste Datei ist KeyPath
if idx == 0:
file_attribs["KeyPath"] = "yes"
ET.SubElement(component, f"{{{ns}}}File", file_attribs)
print(f"Erstellt: {component_counter} Komponenten")
return wix
def format_xml(element: ET.Element, level: int = 0) -> None:
"""Formatiert XML mit Einrückungen (in-place)."""
indent = " "
i = f"\n{indent * level}"
if len(element):
if not element.text or not element.text.strip():
element.text = i + indent
if not element.tail or not element.tail.strip():
element.tail = i
last_child = None
for child in element:
format_xml(child, level + 1)
last_child = child
if last_child is not None and (not last_child.tail or not last_child.tail.strip()):
last_child.tail = i
else:
if level and (not element.tail or not element.tail.strip()):
element.tail = i
def main():
"""Hauptfunktion."""
dist_dir = Path("dist/DocuMentor")
output_file = Path("ProductFiles.wxs")
if not dist_dir.exists():
print(f"FEHLER: {dist_dir} existiert nicht!")
print("Führe zuerst 'uv run pyinstaller DocuMentor.spec' aus.")
return
print(f"Generiere ProductFiles.wxs aus {dist_dir}...")
wix_root = create_wix_fragment(dist_dir)
format_xml(wix_root)
# Schreibe XML
tree = ET.ElementTree(wix_root)
tree.write(output_file, encoding="utf-8", xml_declaration=True)
print(f"[OK] {output_file} erfolgreich erstellt!")
print(f"\nNaechste Schritte:")
print(f"1. wix build DocuMentor.wxs ProductFiles.wxs -o DocuMentor.msi")
if __name__ == "__main__":
main()
+73
View File
@@ -0,0 +1,73 @@
; Inno Setup Konfiguration für DocuMentor
; Erstellt eine professionelle Setup.exe für Windows
;
; Installation von Inno Setup: https://jrsoftware.org/isdl.php
;
; WICHTIG: Vor dem ersten Build GUID generieren!
; python -c "import uuid; print(f'{{{{' + str(uuid.uuid4()).upper() + '}}}}')"
; Ergebnis in AppId unten einfügen
;
; Build-Befehl: iscc installer.iss
#define MyAppName "DocuMentor"
#define MyAppVersion "1.7.3"
#define MyAppPublisher "Ihr Name/Organisation"
#define MyAppURL "https://github.com/yourusername/xsl-validator"
#define MyAppExeName "DocuMentor.exe"
[Setup]
; Basis-Informationen
; WICHTIG: Ersetzen Sie die GUID mit einer eigenen generierten GUID!
; AppId={{BEISPIEL-GUID-HIER-EINFÜGEN}}
AppId={{A1B2C3D4-E5F6-4789-ABCD-EF0123456789}}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
; Installation
DefaultDirName={autopf}\{#MyAppName}
DefaultGroupName={#MyAppName}
AllowNoIcons=yes
; Output
OutputDir=dist\installer
OutputBaseFilename=DocuMentor-Setup-{#MyAppVersion}
SetupIconFile=dist\DocuMentor\icon.ico
UninstallDisplayIcon={app}\{#MyAppExeName}
; Kompression
Compression=lzma
SolidCompression=yes
; Moderne UI
WizardStyle=modern
; Rechte (normal für User-Installation, admin für System-weite Installation)
PrivilegesRequired=lowest
PrivilegesRequiredOverridesAllowed=dialog
[Languages]
Name: "german"; MessagesFile: "compiler:Languages\German.isl"
Name: "english"; MessagesFile: "compiler:Default.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
[Files]
; Alle Dateien aus dem PyInstaller-Build
Source: "dist\DocuMentor\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
[Icons]
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
[Messages]
; Deutsche Anpassungen
german.WelcomeLabel2=Dies wird [name/ver] auf Ihrem Computer installieren.%n%nBitte stellen Sie sicher, dass folgende externe Tools installiert sind:%n• Java Runtime Environment (JRE)%n• Apache FOP%n• Saxon XSLT-Prozessor%n• diff-pdf
+19 -9
View File
@@ -1,15 +1,18 @@
[project] [project]
name = "DocuMentor" name = "DocuMentor"
version = "0.1.0" version = "1.7.3"
description = "Add your description here" description = "Professionelle XSL-Transformations-Verwaltung und PDF-Generierung"
readme = "README.md" readme = "README.md"
requires-python = ">=3.13" license = {text = "MIT"}
requires-python = ">=3.13,<3.15"
dependencies = [ dependencies = [
"pyqtdarktheme>=2.1.0", "pydantic-settings>=2.12.0",
"pydantic-settings>=2.9.1", "pyside6>=6.10.1",
"pyside6>=6.9.1", "polars[connectorx,pyarrow]>=1.37.0",
"polars[connectorx,pyarrow]>=1.31.0", "connectorx>=0.4.0",
"pydantic-yaml>=1.5.1", "pydantic-yaml>=1.6.0",
"psutil>=6.1.1",
"lxml>=6.0.2",
] ]
[tool.ruff] [tool.ruff]
@@ -19,5 +22,12 @@ dependencies = [
# ...but use a different line length. # ...but use a different line length.
line-length = 120 line-length = 120
# Ignoriere automatisch generierte UI-Dateien
extend-exclude = ["*_ui.py"]
[dependency-groups] [dependency-groups]
dev = [] dev = [
"ruff>=0.14.11",
"pyinstaller>=6.0.0",
"pillow>=10.0.0",
]
Binary file not shown.

After

Width:  |  Height:  |  Size: 678 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 819 KiB

+64
View File
@@ -0,0 +1,64 @@
# Resources für DocuMentor
Dieses Verzeichnis enthält Ressourcen für den Windows-Build.
## Icon (icon.ico)
Das Icon wird verwendet für:
- Windows-Executable (DocuMentor.exe)
- Inno Setup Installer
- Desktop-Verknüpfungen
- Start-Menü-Einträge
### Icon erstellen
#### Automatisch (Standard-Icon):
```bash
python create_icon.py
```
Dies erstellt ein einfaches Standard-Icon mit DocuMentor-Branding.
#### Aus eigenem PNG-Bild:
```bash
python create_icon.py mein-icon.png
```
Ihr PNG sollte idealerweise:
- Mindestens 256x256 Pixel groß sein
- Quadratisch sein
- Transparenten Hintergrund haben (optional)
### Icon-Anforderungen
Das `.ico`-Dateiformat enthält mehrere Auflösungen:
- 256x256 (Windows 7+, Taskleiste)
- 128x128
- 64x64
- 48x48 (Standard Desktop-Icon)
- 32x32 (Explorer Details)
- 16x16 (kleines Icon)
Das `create_icon.py` Skript erstellt automatisch alle diese Größen.
## Icon manuell ersetzen
1. Eigenes Icon als `resources/icon.ico` speichern
2. Oder mit einem Online-Tool PNG→ICO konvertieren
3. Build-Skript verwendet automatisch die vorhandene Datei
## Design-Richtlinien
Falls Sie ein eigenes Icon erstellen:
- **Einfach und klar**: Funktioniert auch in kleinen Größen (16x16)
- **Professionell**: Passend zum Business-Kontext
- **Wiedererkennbar**: DocuMentor steht für Dokumenten-Management
- **Kontrast**: Gut sichtbar auf hellem und dunklem Hintergrund
## Weitere Ressourcen
In diesem Verzeichnis können später weitere Ressourcen abgelegt werden:
- Splash-Screen-Bilder
- Toolbar-Icons
- Dokumentations-Bilder
- etc.
+45
View File
@@ -0,0 +1,45 @@
Erstelle ein professionelles Icon für eine Desktop-Anwendung namens "DocuMentor".
Die Anwendung wird verwendet für:
- Verwaltung von XSL-Transformationen
- Umwandlung von XML-Dokumenten zu PDF-Dateien
- Vergleich und Validierung von PDF-Dokumenten
Design-Anforderungen:
1. Stil: Minimalistisch, modern, professionell, business-orientiert
2. Farben:
- Hauptfarbe: Blau (#2980B9 oder ähnlich)
- Akzentfarbe: Weiß oder helles Grau
- Maximal 2-3 Farben insgesamt
3. Elemente (wähle eine Kombination):
- Dokument-Symbol (Papier/Seite)
- Transformation/Workflow-Element (Pfeil, Zahnrad)
- Optional: Stilisierter Buchstabe "M" oder "D"
4. Technische Anforderungen:
- Quadratisches Format
- Einfache, klare Linien
- Hoher Kontrast
- Muss auch bei 16x16 Pixel noch erkennbar sein
- Flat Design (keine 3D-Effekte)
- Keine Farbverläufe
5. Hintergrund:
- Transparent ODER
- Einfarbig (blau oder weiß)
6. Referenz-Stil:
- Ähnlich wie Microsoft Office Icons
- Ähnlich wie Visual Studio Code Icons
- Moderne SaaS-Anwendungs-Icons
Bitte erstelle ein Icon, das:
- Professionell und vertrauenswürdig wirkt
- Für technische Anwender geeignet ist
- Gut in einer Windows-Taskleiste aussieht
- Auch als Desktop-Verknüpfung funktioniert
Format: PNG oder SVG, mindestens 512x512 Pixel
Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.
+150
View File
@@ -0,0 +1,150 @@
# Icon-Prompt für Bild-Generierungs-KI
## DocuMentor Logo/Icon
### Deutsche Version (für deutsche KI-Tools)
```
Erstelle ein professionelles, minimalistisches SVG-Icon für eine Business-Software namens "DocuMentor".
Anwendungsbeschreibung:
DocuMentor ist eine Desktop-Anwendung zur Verwaltung und Validierung von XSL-Transformationen. Die Software wird von technischen Redakteuren und Entwicklern verwendet, um XML-Dokumente in PDF-Dateien zu transformieren und diese zu vergleichen.
Design-Anforderungen:
- Stil: Professionell, modern, technisch, business-orientiert
- Farben: Blaue Töne (z.B. #2980B9, #3498DB) kombiniert mit neutralen Grautönen oder Weiß
- Elemente: Kombination aus Dokumenten-Symbol und Transformations-/Workflow-Elementen
- Einfachheit: Muss auch in sehr kleinen Größen (16x16 Pixel) erkennbar sein
- Klare Linien und hoher Kontrast
Symbolik-Vorschläge:
- Ein Dokument mit Transformations-Pfeilen
- Gestapelte/verschachtelte Dokumente (XML → XSLT → PDF)
- Stilisiertes "D" oder "M" für DocuMentor
- Workflow-Diagramm mit Dokumenten-Symbolen
- Dokument mit Zahnrad (Verarbeitung/Transformation)
Format: Vektorgrafik (SVG), quadratisch (1:1 Verhältnis), 512x512 Pixel oder größer
Hintergrund: Transparent oder einfarbig (blau/weiß)
Stil-Referenzen: Microsoft Office Icons, Adobe Creative Cloud Icons, moderne SaaS-Anwendungen
```
### Englische Version (für internationale KI-Tools wie DALL-E, Midjourney, Stable Diffusion)
```
Create a professional, minimalist SVG icon for business software called "DocuMentor".
Application Description:
DocuMentor is a desktop application for managing and validating XSL transformations. The software is used by technical writers and developers to transform XML documents into PDF files and compare them.
Design Requirements:
- Style: Professional, modern, technical, business-oriented
- Colors: Blue tones (e.g., #2980B9, #3498DB) combined with neutral grays or white
- Elements: Combination of document symbol and transformation/workflow elements
- Simplicity: Must be recognizable even at very small sizes (16x16 pixels)
- Clean lines and high contrast
Symbolism Suggestions:
- A document with transformation arrows
- Stacked/nested documents (XML → XSLT → PDF)
- Stylized "D" or "M" for DocuMentor
- Workflow diagram with document symbols
- Document with gear icon (processing/transformation)
Format: Vector graphic (SVG), square (1:1 ratio), 512x512 pixels or larger
Background: Transparent or solid color (blue/white)
Style References: Microsoft Office icons, Adobe Creative Cloud icons, modern SaaS applications
Additional Instructions:
- Flat design, not 3D
- No gradients or complex shadows
- Maximum 3 colors
- Geometric shapes preferred
- Professional and trustworthy appearance
```
### Alternativer Prompt (detaillierter für KIs wie ChatGPT mit DALL-E)
```
Design a minimalist icon for "DocuMentor" - a professional XML/XSL transformation management software.
Concept: A clean, modern icon that combines:
1. A document/page symbol (representing XML/PDF files)
2. An element suggesting transformation or workflow (arrows, gears, or connecting lines)
3. Professional color scheme: Primary blue (#2980B9) with white/gray accents
Requirements:
- Vector style, flat design
- Must work well at 16x16, 48x48, and 256x256 pixels
- High contrast for visibility
- No text, icon only
- Square format (512x512px minimum)
- Transparent background preferred
Style inspiration: Think Microsoft Office 365 icons, VS Code icons, or modern productivity app icons - clean, professional, instantly recognizable.
Technical constraints:
- Simple enough to work as a favicon
- Clear silhouette when shown in monochrome
- Distinctive enough to stand out in a taskbar or dock
```
## Prompt für spezifische Konzepte
### Konzept 1: Dokument mit Transformation
```
A minimalist icon showing a document page with a curved arrow pointing to another document, symbolizing transformation. Blue (#2980B9) and white color scheme. Flat design, professional, suitable for business software. SVG style, 512x512px, transparent background.
```
### Konzept 2: Gestapelte Dokumente
```
An icon with three overlapping document sheets in a cascading arrangement, representing XML to XSL to PDF transformation workflow. Modern flat design, blue gradient (#3498DB to #2980B9), white accents. Professional business software icon. 512x512px SVG format.
```
### Konzept 3: Dokument + Zahnrad
```
A clean icon combining a document page with a small gear/cog symbol in the corner, representing document processing. Minimalist design, blue (#2980B9) on white background. Professional style like Microsoft Office icons. 512x512px, vector art, high contrast.
```
### Konzept 4: Stilisiertes "M"
```
A stylized letter "M" for "Mentor" integrated with document/page elements. Modern, geometric, professional. Blue (#2980B9) color scheme. Suitable for small sizes. Flat design, vector style, 512x512px, transparent background.
```
## Verwendung
1. Wähle einen der Prompts oben
2. Füge ihn in eine Bild-Generierungs-KI ein:
- **DALL-E 3** (ChatGPT Plus): Englischer Prompt empfohlen
- **Midjourney**: Englischer Prompt, evtl. kürzer
- **Adobe Firefly**: Deutscher oder englischer Prompt
- **Stable Diffusion**: Englischer Prompt mit detaillierten Tags
- **Leonardo.ai**: Englischer Prompt
3. Lade das generierte Bild herunter (idealerweise als PNG)
4. Konvertiere zu ICO:
```bash
uv run python create_icon.py generiertes-icon.png
```
## Tipps für beste Ergebnisse
- **Iteriere**: Generiere mehrere Varianten
- **Einfachheit**: Betone "minimalist", "simple", "clean"
- **Größe**: Teste das Icon in verschiedenen Größen
- **Kontrast**: Achte auf gute Sichtbarkeit auf hellem und dunklem Hintergrund
- **Professionalität**: Vermeide zu verspielte oder kindliche Designs
## Nachbearbeitung
Falls die KI kein perfektes SVG erstellt:
1. PNG exportieren (hohe Auflösung, mind. 512x512px)
2. Mit Inkscape oder Adobe Illustrator zu SVG konvertieren
3. Oder direkt als PNG verwenden und mit `create_icon.py` konvertieren
Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 605 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

+77
View File
@@ -0,0 +1,77 @@
"""
Lädt Feather Icons (MIT-Lizenz) von GitHub herunter und speichert sie in src/res/icons/.
Stroke-Farbe wird auf #444444 gesetzt für bessere Sichtbarkeit in hellen und dunklen Themes.
Ausführung: uv run python scripts/download_icons.py
"""
import urllib.request
from pathlib import Path
ICONS = [
"folder-plus",
"log-out",
"settings",
"folder",
"refresh-cw",
"plus-circle",
"minus-circle",
"play-circle",
"file",
"check-circle",
"info",
"git-branch",
"file-text",
"code",
"chevron-down",
"chevron-up",
"trash-2",
"file-plus",
"columns",
"sliders",
"database",
"activity",
]
BASE_URL = "https://raw.githubusercontent.com/feathericons/feather/master/icons/{name}.svg"
OUTPUT_DIR = Path(__file__).parent.parent / "src" / "res" / "icons"
def download_icons():
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
success = 0
failed = []
for name in ICONS:
url = BASE_URL.format(name=name)
dest = OUTPUT_DIR / f"{name}.svg"
if dest.exists():
print(f" [OK] {name}.svg (bereits vorhanden)")
success += 1
continue
try:
with urllib.request.urlopen(url, timeout=10) as response:
content = response.read().decode("utf-8")
content = content.replace('stroke="#000000"', 'stroke="#444444"')
content = content.replace("stroke='#000000'", "stroke='#444444'")
dest.write_text(content, encoding="utf-8")
print(f" [OK] {name}.svg")
success += 1
except Exception as e:
print(f" [FEHLER] {name}.svg: {e}")
failed.append(name)
print(f"\n{success}/{len(ICONS)} Icons heruntergeladen nach {OUTPUT_DIR}")
if failed:
print(f"Fehlgeschlagen: {', '.join(failed)}")
if __name__ == "__main__":
print(f"Lade {len(ICONS)} Feather Icons herunter ...")
download_icons()
+110 -37
View File
@@ -1,4 +1,5 @@
from os import path import os
import sys
from pathlib import Path from pathlib import Path
from sys import platform from sys import platform
from typing import Tuple, Type from typing import Tuple, Type
@@ -18,15 +19,15 @@ app_name = "DocuMentor"
if platform == "win32": if platform == "win32":
config_path = f"%APPDATA%\\{app_name}\\config.json" tmp_config_path = f"%APPDATA%\\{app_name}\\config.json"
elif platform in ("linux", "linux2"): elif platform in ("linux", "linux2"):
config_path = f"~/.config/{app_name}/config.json" tmp_config_path = f"~/.config/{app_name}/config.json"
elif platform == "darwin": elif platform == "darwin":
config_path = f"~/Library/Application Support/{app_name}/͏͏͏͏config.json" tmp_config_path = f"~/Library/Application Support/{app_name}/͏͏͏͏config.json"
else: else:
config_path = f"~/.config/{app_name}/config.json" tmp_config_path = f"~/.config/{app_name}/config.json"
config_path = Path(path.expandvars(config_path)) config_path = Path(os.path.expandvars(tmp_config_path)).expanduser()
class JavaVm(BaseModel): class JavaVm(BaseModel):
@@ -61,8 +62,8 @@ class XslDir(BaseModel):
id: int id: int
name: str name: str
path_to_root_dir: Path path_to_root_dir: Path
class SSLMode(str, Enum): class SSLMode(str, Enum):
DISABLE = "disable" DISABLE = "disable"
ALLOW = "allow" ALLOW = "allow"
@@ -71,6 +72,73 @@ class SSLMode(str, Enum):
VERIFY_CA = "verify-ca" VERIFY_CA = "verify-ca"
VERIFY_FULL = "verify-full" VERIFY_FULL = "verify-full"
class XsltVersion(str, Enum):
"""XSLT-Version für Saxon-Transformationen."""
XSLT_1_0 = "1.0" # JAXP API (nur XSLT 1.0)
XSLT_2_0_3_0 = "2.0/3.0" # s9api (XSLT 2.0 und 3.0)
class GraphLayout(str, Enum):
"""vis.js Physics-Solver / Layout-Modus."""
BARNES_HUT = "barnesHut"
FORCE_ATLAS2 = "forceAtlas2Based"
REPULSION = "repulsion"
HIERARCHICAL = "hierarchical"
class HierarchicalDirection(str, Enum):
"""Richtung für hierarchisches Layout."""
UD = "UD"
DU = "DU"
LR = "LR"
RL = "RL"
class HierarchicalSortMethod(str, Enum):
"""Sortiermethode für hierarchisches Layout."""
HUBSIZE = "hubsize"
DIRECTED = "directed"
class GraphLayoutSettings(BaseModel):
"""Persistierte vis.js Layout-Einstellungen für den XSL-Abhängigkeitsgraph."""
layout: GraphLayout = GraphLayout.BARNES_HUT
# barnesHut
bh_gravitational_constant: int = -3000
bh_central_gravity: float = 0.3
bh_spring_length: int = 150
bh_spring_constant: float = 0.04
bh_damping: float = 0.09
# forceAtlas2Based
fa_gravitational_constant: int = -50
fa_central_gravity: float = 0.01
fa_spring_length: int = 100
fa_spring_constant: float = 0.08
fa_damping: float = 0.4
# repulsion
re_node_distance: int = 120
re_central_gravity: float = 0.0
re_spring_length: int = 200
re_spring_constant: float = 0.05
re_damping: float = 0.09
# hierarchical
hi_direction: HierarchicalDirection = HierarchicalDirection.UD
hi_sort_method: HierarchicalSortMethod = HierarchicalSortMethod.HUBSIZE
hi_level_separation: int = 150
hi_node_spacing: int = 100
hi_tree_spacing: int = 200
class PostgreSqlDb(BaseModel): class PostgreSqlDb(BaseModel):
id: int id: int
name: str name: str
@@ -80,6 +148,7 @@ class PostgreSqlDb(BaseModel):
username: str username: str
password: str password: str
ssl_mode: SSLMode = SSLMode.PREFER ssl_mode: SSLMode = SSLMode.PREFER
timeout: int = 10
class Project(BaseModel): class Project(BaseModel):
@@ -92,42 +161,32 @@ class Project(BaseModel):
apache_fop_id: int = Field(..., description="ID der Apache FOP Konfiguration", gt=0) 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) 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) 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")
xslt_params: dict[str, str] = Field(default_factory=dict, description="Projektweite XSLT-Parameter")
@staticmethod
def _lookup(collection, item_id: int, attr: str) -> str:
"""Sucht einen Wert in einer Konfigurationsliste anhand der ID."""
value = [getattr(x, attr) for x in collection if x.id == item_id]
return value[0] if value else ""
def getXsl(self) -> str: def getXsl(self) -> str:
global app_settings return self._lookup(app_settings.xsl_dirs, self.xsl_dir_id, "name")
value = [x.name for x in app_settings.xsl_dirs if x.id == self.xsl_dir_id]
return value[0] if len(value) else ""
def getJavaVm(self) -> str: def getJavaVm(self) -> str:
global app_settings return self._lookup(app_settings.java_vms, self.java_vm_id, "version")
value = [x.version for x in app_settings.java_vms if x.id == self.java_vm_id]
return value[0] if len(value) else ""
def getSaxon(self) -> str: def getSaxon(self) -> str:
global app_settings return self._lookup(app_settings.saxon_jars, self.saxon_jar_id, "version")
value = [x.version for x in app_settings.saxon_jars if x.id == self.saxon_jar_id]
return value[0] if len(value) else ""
def getApacheFop(self) -> str: def getApacheFop(self) -> str:
global app_settings return self._lookup(app_settings.apache_fops, self.apache_fop_id, "version")
value = [x.version for x in app_settings.apache_fops if x.id == self.apache_fop_id]
return value[0] if len(value) else ""
def getDiffPdf(self) -> str: def getDiffPdf(self) -> str:
global app_settings return self._lookup(app_settings.diff_pdfs, self.diff_pdf_id, "version")
value = [x.version for x in app_settings.diff_pdfs if x.id == self.diff_pdf_id]
return value[0] if len(value) else ""
def getPostgreSqlDb(self) -> str: def getPostgreSqlDb(self) -> str:
global app_settings return self._lookup(app_settings.postgresql_dbs, self.postgre_sql_db_id, "name")
value = [x.name for x in app_settings.postgresql_dbs if x.id == self.postgre_sql_db_id]
return value[0] if len(value) else ""
class AppSettings(BaseSettings): class AppSettings(BaseSettings):
@@ -139,6 +198,16 @@ class AppSettings(BaseSettings):
pdf_projects: list[Project] = [] pdf_projects: list[Project] = []
postgresql_dbs: list[PostgreSqlDb] = [] postgresql_dbs: list[PostgreSqlDb] = []
theme: str | None = None theme: str | None = None
max_workers: int = 8 # Anzahl paralleler Worker für Transformationen (Standard: 8)
use_saxon_worker_pool: bool = True # SaxonWorkerPool aktivieren (schneller, benötigt JDK)
saxon_xslt_version: XsltVersion = XsltVersion.XSLT_2_0_3_0 # XSLT-Version für Saxon (Standard: 2.0/3.0 mit s9api)
use_fop_worker_pool: bool = True # FopWorkerPool aktivieren (schneller, benötigt JDK)
# 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
graph_layout_settings: GraphLayoutSettings = Field(default_factory=GraphLayoutSettings)
model_config = SettingsConfigDict(json_file=config_path) model_config = SettingsConfigDict(json_file=config_path)
@@ -154,11 +223,14 @@ class AppSettings(BaseSettings):
return (JsonConfigSettingsSource(settings_cls),) return (JsonConfigSettingsSource(settings_cls),)
def save(self): def save(self):
global config_path
# Ordner existert nicht # Ordner existert nicht
if not config_path.parent.exists(): if not config_path.parent.exists():
config_path.parent.mkdir(parents=True, exist_ok=True) 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 # Konfiguration speichern
with open(config_path, "wb") as c: with open(config_path, "wb") as c:
c.write(app_settings.model_dump_json(indent=4).encode()) c.write(app_settings.model_dump_json(indent=4).encode())
@@ -194,16 +266,17 @@ class ProjectData(BaseModel):
""" """
nodes: list[TreeNode] = [] nodes: list[TreeNode] = []
expanded_nodes: list[tuple] | None = None # Optional: IDs der aufgeklappten Knoten (TreeNode und XslFile)
@classmethod @classmethod
def readSettings(cls, project_dir: Path): def readSettings(cls, project_dir: Path):
# Explizit UTF-8 Encoding verwenden # Explizit UTF-8 Encoding verwenden
project_yaml_path = project_dir / "project.yaml" project_yaml_path = project_dir / "project.yaml"
with open(project_yaml_path, 'r', encoding='utf-8') as f: with open(project_yaml_path, "r", encoding="utf-8") as f:
yaml = YAML(typ='safe') yaml = YAML(typ="safe")
yaml_data = yaml.load(f) yaml_data = yaml.load(f)
return cls.model_validate(yaml_data) return cls.model_validate(yaml_data)
def writeSettings(self, project_dir: Path): def writeSettings(self, project_dir: Path):
with open(project_dir / "project.yaml", "w", encoding="utf8") as f: with open(project_dir / "project.yaml", "w", encoding="utf8") as f:
f.write(to_yaml_str(self)) f.write(to_yaml_str(self))
+282
View File
@@ -0,0 +1,282 @@
"""
FOP Worker Pool - Persistente JVM-Prozesse für schnelle PDF-Generierung.
Eliminiert JVM-Startup-Overhead durch Vorinitialisierung von N Worker-Prozessen.
Jeder Worker läuft als Daemon und verarbeitet mehrere FO→PDF Transformationen nacheinander.
"""
import glob
import logging
from pathlib import Path
from typing import Optional
from worker_pool_base import BaseWorkerPool, _CLASSPATH_SEP
logger = logging.getLogger(__name__)
# Java-Worker-Code (wird zur Laufzeit kompiliert)
FOP_WORKER_JAVA = """
import org.apache.fop.apps.*;
import org.xml.sax.SAXException;
import javax.xml.transform.*;
import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.stream.StreamSource;
import java.io.*;
import java.net.URI;
public class FopWorker {
public static void main(String[] args) {
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
String line;
System.err.println("FopWorker starting...");
System.err.flush();
// Create FopFactory once and reuse (major performance boost!)
FopFactory fopFactory = null;
try {
// Check if config file is provided as first argument
if (args.length > 0 && !args[0].isEmpty()) {
File configFile = new File(args[0]);
if (configFile.exists()) {
System.err.println("Loading FOP config: " + configFile.getAbsolutePath());
fopFactory = FopFactory.newInstance(configFile);
} else {
System.err.println("Config file not found, using default configuration");
fopFactory = FopFactory.newInstance(new File(".").toURI());
}
} else {
System.err.println("No config file specified, using default FOP configuration");
fopFactory = FopFactory.newInstance(new File(".").toURI());
}
System.err.println("FopWorker started and ready");
System.err.flush();
} catch (Exception e) {
System.err.println("FATAL: Failed to initialize FopFactory: " + e.getMessage());
e.printStackTrace(System.err);
System.exit(1);
}
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("FopWorker 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 < 2) {
System.out.println("ERROR: Invalid job format");
System.out.flush();
continue;
}
String inputFo = parts[0];
String outputPdf = parts[1];
System.err.println("DEBUG: Input FO: " + inputFo);
System.err.println("DEBUG: Output PDF: " + outputPdf);
System.err.flush();
// Create FOUserAgent for this transformation
FOUserAgent foUserAgent = fopFactory.newFOUserAgent();
// Note: Event Listener für detailliertes Error-Logging könnte hier hinzugefügt werden,
// aber ist nicht kritisch - Fehler werden durch Exceptions gefangen
// Create output stream
File outputFile = new File(outputPdf);
outputFile.getParentFile().mkdirs();
OutputStream out = new BufferedOutputStream(new FileOutputStream(outputFile));
try {
System.err.println("DEBUG: Creating Fop instance...");
System.err.flush();
// Create Fop instance
Fop fop = fopFactory.newFop(MimeConstants.MIME_PDF, foUserAgent, out);
System.err.println("DEBUG: Setting up transformer...");
System.err.flush();
// Setup Transformer
TransformerFactory transformerFactory = TransformerFactory.newInstance();
Transformer transformer = transformerFactory.newTransformer();
// Setup input and output
Source src = new StreamSource(new File(inputFo));
Result res = new SAXResult(fop.getDefaultHandler());
System.err.println("DEBUG: Running FOP transformation...");
System.err.flush();
// Run transformation
transformer.transform(src, res);
System.err.println("DEBUG: FOP transformation completed");
System.err.flush();
} finally {
out.close();
}
// Transformation erfolgreich
System.out.println("OK");
System.out.flush();
} catch (Exception e) {
System.err.println("DEBUG: Job processing 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 (IOException e) {
System.err.println("FopWorker I/O error: " + e.getMessage());
e.printStackTrace(System.err);
}
}
}
"""
class FopWorkerPool(BaseWorkerPool):
"""
Pool von lang-laufenden JVM-Prozessen für Apache FOP PDF-Generierung.
Eliminiert JVM-Startup-Overhead durch Wiederverwendung von N Worker-Prozessen.
"""
def __init__(
self,
num_workers: int,
java_vm_path: Path,
apache_fop_dir: Path,
fop_config_file: Optional[Path] = None,
log_dir: Optional[Path] = None,
):
super().__init__(num_workers, java_vm_path, log_dir)
self.apache_fop_dir = apache_fop_dir
self.fop_config_file = fop_config_file
self.fop_classpath: Optional[str] = None
self._build_fop_classpath()
self._compile_worker_class()
self._start_workers()
logger.info(f"FopWorkerPool initialisiert mit {num_workers} Workern")
def _build_fop_classpath(self):
"""Erstellt den Classpath für Apache FOP."""
all_jars = glob.glob(str(self.apache_fop_dir / "build" / "*.jar"))
lib_dir = self.apache_fop_dir / "lib"
if lib_dir.exists() and lib_dir.is_dir():
all_jars.extend(glob.glob(str(lib_dir / "*.jar")))
if not all_jars:
raise RuntimeError(f"Keine FOP JAR-Dateien gefunden in {self.apache_fop_dir}")
self.fop_classpath = _CLASSPATH_SEP.join(all_jars)
logger.debug(f"FOP Classpath: {len(all_jars)} JARs")
# --- Abstrakte Properties ---
@property
def _pool_name(self) -> str:
return "FOP"
@property
def _java_source_code(self) -> str:
return FOP_WORKER_JAVA
@property
def _java_class_name(self) -> str:
return "FopWorker"
@property
def _temp_dir_prefix(self) -> str:
return "fop_worker_"
@property
def _worker_init_sleep(self) -> float:
return 0.2 # FOP braucht etwas länger zum Initialisieren
# --- Abstrakte Methoden ---
def _get_classpath(self) -> str:
return self.fop_classpath
def _build_worker_cmd(self, full_classpath: str) -> list[str]:
cmd = [str(self.java_vm_path), "-cp", full_classpath, "FopWorker"]
if self.fop_config_file and self.fop_config_file.exists():
cmd.append(str(self.fop_config_file))
return cmd
def _stderr_log_name(self, i: int) -> str:
return f"fop_worker_{i}_stderr.log"
# --- FOP-spezifische Job-Methode ---
def build_pdf(self, input_fo: Path, output_pdf: Path) -> tuple[bool, str]:
"""
Generiert PDF aus FO-Datei mit einem Worker aus dem Pool.
Args:
input_fo: Pfad zur FO-Eingabedatei
output_pdf: Pfad zur PDF-Ausgabedatei
Returns:
tuple[bool, str]: (Erfolg, Fehlermeldung/Info)
"""
worker_idx = self._acquire_worker()
try:
worker = self.workers[worker_idx]
if worker.poll() is not None:
stderr_content = self._read_stderr_log(worker_idx)
error_msg = f"FOP Worker {worker_idx} ist beendet (Exit: {worker.returncode})\nstderr:\n{stderr_content}"
logger.error(error_msg)
return False, error_msg
job = f"{input_fo}\t{output_pdf}\n"
logger.debug(f"Sende FOP-Job an Worker {worker_idx}: {input_fo.name}{output_pdf.name}")
worker.stdin.write(job)
worker.stdin.flush()
response = worker.stdout.readline().strip()
logger.debug(f"FOP Worker {worker_idx} Antwort: '{response}'")
if response == "OK":
return True, "Erfolgreich"
elif response.startswith("ERROR:"):
return False, f"FOP-Fehler: {response[6:].strip()}"
elif not response:
stderr_content = self._read_stderr_log(worker_idx, tail=500)
return False, f"FOP Worker {worker_idx} crashed (keine Antwort)\nstderr:\n{stderr_content}"
else:
return False, f"Unerwartete Antwort: {response}"
except Exception as e:
logger.error(f"Fehler bei FOP Worker {worker_idx}: {e}")
return False, f"Worker-Fehler: {str(e)}"
finally:
self.worker_locks[worker_idx].release()
+105
View File
@@ -0,0 +1,105 @@
import logging
from PySide6.QtCore import QByteArray, QEvent, QFile, Qt
from PySide6.QtGui import QIcon, QPainter, QPixmap, QPalette
from PySide6.QtSvg import QSvgRenderer
from PySide6.QtWidgets import QApplication
import res.resources_rc # noqa: F401 # registriert die Qt-Ressourcen (Icons) beim Import
logger = logging.getLogger(__name__)
# Basisgröße für das einmalige Rendern; Qt skaliert daraus die benötigten Icon-Größen
# (16/24/32 px in Menüs, Bäumen, Buttons). 64 px bietet genug Reserve auch für HiDPI.
_RENDER_SIZE = 64
# Cache: (Icon-Name, Theme-Textfarbe) → fertig gerendertes QIcon.
# Der Farb-Anteil im Schlüssel sorgt dafür, dass ein Theme-Wechsel automatisch
# neue Einträge erzeugt, ohne dass der Cache explizit geleert werden muss.
_ICON_CACHE: dict[tuple[str, bytes], QIcon] = {}
def icon(name: str) -> QIcon:
"""
Lädt ein Feather-Icon aus dem Qt-Ressource-System und färbt es mit der aktuellen Palette-Farbe.
Args:
name: Feather-Icon-Name (z.B. "folder-plus", "settings")
Returns:
QIcon in der Textfarbe des aktiven Themes, oder leerer QIcon bei unbekanntem Namen
"""
path = f":/icons/{name}.svg"
app = QApplication.instance()
if app is None:
return QIcon(path)
color = app.palette().color(QPalette.ColorRole.WindowText).name().encode()
cache_key = (name, color)
if (cached := _ICON_CACHE.get(cache_key)) is not None:
return cached
f = QFile(path)
if not f.open(QFile.OpenModeFlag.ReadOnly):
logger.warning(f"Icon konnte nicht geöffnet werden: {path}")
return QIcon(path)
svg_data = QByteArray(bytes(f.readAll()).replace(b"currentColor", color))
f.close()
renderer = QSvgRenderer(svg_data)
pixmap = QPixmap(_RENDER_SIZE, _RENDER_SIZE)
pixmap.fill(Qt.GlobalColor.transparent)
painter = QPainter(pixmap)
renderer.render(painter)
painter.end()
result = QIcon(pixmap)
_ICON_CACHE[cache_key] = result
return result
class IconRefreshMixin:
"""
Mischklasse für Top-Level-Widgets, deren Icons sich beim Theme-Wechsel automatisch neu einfärben.
Voraussetzungen für die nutzende Klasse:
- Sie stellt eine Methode ``_setup_icons()`` bereit, die alle ihre Icons neu setzt.
- Nach abgeschlossener Initialisierung setzt sie ``self._icons_ready = True``
(idealerweise zusammen mit ``self._last_icon_color``, siehe ``mark_icons_ready()``).
Bei einem Style-/Palette-Wechsel wird nur wenn sich die Theme-Textfarbe tatsächlich
geändert hat ``_on_icon_theme_changed()`` aufgerufen (Standard: ``_setup_icons()``).
Unterklassen können ``_on_icon_theme_changed()`` überschreiben, um zusätzlich z.B.
Baum-Icons neu zu zeichnen.
Hinweis: ``IconRefreshMixin`` muss in der Klassenbasis VOR der Qt-Basisklasse
(``QMainWindow``/``QDialog``) stehen, damit das überschriebene ``changeEvent`` greift.
"""
_icons_ready: bool = False
_last_icon_color: str | None = None
@staticmethod
def _current_text_color() -> str | None:
app = QApplication.instance()
if app is None:
return None
return app.palette().color(QPalette.ColorRole.WindowText).name()
def mark_icons_ready(self):
"""Schaltet die Theme-Reaktivität scharf (nach dem ersten ``_setup_icons()`` aufrufen)."""
self._last_icon_color = self._current_text_color()
self._icons_ready = True
def changeEvent(self, event):
if event.type() in (QEvent.Type.PaletteChange, QEvent.Type.StyleChange) and self._icons_ready:
color = self._current_text_color()
if color is not None and color != self._last_icon_color:
self._last_icon_color = color
self._on_icon_theme_changed()
super().changeEvent(event)
def _on_icon_theme_changed(self):
"""Reaktion auf eine geänderte Theme-Textfarbe. Standard: alle Icons neu setzen."""
self._setup_icons()
+218
View File
@@ -0,0 +1,218 @@
"""
Parser für THIRD_PARTY_LICENSES.txt.
Extrahiert strukturierte Lizenzinformationen und ergänzt sie
mit den tatsächlich installierten Paketversionen via importlib.metadata
oder (im PyInstaller-Bundle) aus der mitgebündelten versions.json.
"""
import json
import logging
import re
import sys
from dataclasses import dataclass, field
from importlib.metadata import PackageNotFoundError, version
from pathlib import Path
logger = logging.getLogger(__name__)
# Pfad zur Lizenzdatei: im PyInstaller-Bundle aus sys._MEIPASS,
# im Entwicklungsmodus relativ zum Projektroot
if hasattr(sys, "_MEIPASS"):
LICENSE_FILE = Path(sys._MEIPASS) / "THIRD_PARTY_LICENSES.txt" # type: ignore[attr-defined]
else:
LICENSE_FILE = Path(__file__).parent.parent / "THIRD_PARTY_LICENSES.txt"
# Mapping von Anzeigenamen zu PyPI-Paketnamen für Sonderfälle
_PACKAGE_NAME_MAP: dict[str, str] = {
"PySide6": "PySide6",
"Pydantic": "pydantic",
"Pydantic-Settings": "pydantic-settings",
"Pydantic-YAML": "pydantic-yaml",
"Polars": "polars",
"ConnectorX (via Polars)": "connectorx",
"PyArrow (via Polars)": "pyarrow",
"psutil": "psutil",
"lxml": "lxml",
"Ruff (Development)": "ruff",
"PyInstaller (Development)": "pyinstaller",
"Pillow (Development)": "pillow",
}
# Regex für den Beginn eines nummerierten Eintrags (z.B. "1. PySide6" oder "10. PyInstaller (Development)")
_ENTRY_PATTERN = re.compile(r"^\d+\.\s+(.+)$")
# Bekannte Feldschlüssel in der Lizenzdatei
_KNOWN_KEYS = {"Version", "Lizenz", "Webseite", "GitHub", "Beschreibung", "Copyright", "Datei", "Hinweis"}
@dataclass
class LicenseEntry:
"""Ein einzelner Eintrag aus THIRD_PARTY_LICENSES.txt."""
name: str
license: str = ""
installed_version: str = ""
website: str = ""
description: str = ""
copyright: str = ""
category: str = ""
@dataclass
class ParsedLicenses:
"""Ergebnis des Parsens der Lizenzdatei."""
entries: list[LicenseEntry] = field(default_factory=list)
def _normalize_package_name(display_name: str) -> str:
"""Normalisiert einen Anzeigenamen zu einem PyPI-Paketnamen."""
if display_name in _PACKAGE_NAME_MAP:
return _PACKAGE_NAME_MAP[display_name]
# Fallback: lowercase, Leerzeichen/Klammern entfernen
return display_name.lower().split("(")[0].strip()
_bundled_versions: dict[str, str] | None = None
def _load_bundled_versions() -> dict[str, str]:
"""Lädt den Versions-Snapshot aus der mitgebündelten versions.json (nur im PyInstaller-Bundle)."""
global _bundled_versions
if _bundled_versions is None:
if hasattr(sys, "_MEIPASS"):
versions_file = Path(sys._MEIPASS) / "versions.json" # type: ignore[attr-defined]
if versions_file.exists():
_bundled_versions = json.loads(versions_file.read_text(encoding="utf-8"))
else:
_bundled_versions = {}
else:
_bundled_versions = {}
return _bundled_versions
def _get_installed_version(package_name: str) -> str:
"""Ermittelt die installierte Version eines Pakets."""
bundled = _load_bundled_versions()
if bundled:
return bundled.get(package_name.lower(), "")
try:
return version(package_name)
except PackageNotFoundError:
return ""
def parse_license_file(license_file: Path | None = None) -> ParsedLicenses:
"""
Parst THIRD_PARTY_LICENSES.txt und gibt strukturierte Einträge zurück.
Args:
license_file: Pfad zur Lizenzdatei. Standard: LICENSE_FILE
Returns:
ParsedLicenses mit allen gefundenen Einträgen inkl. installierter Versionen
"""
if license_file is None:
license_file = LICENSE_FILE
if not license_file.exists():
logger.warning(f"Lizenzdatei nicht gefunden: {license_file}")
return ParsedLicenses()
text = license_file.read_text(encoding="utf-8")
lines = text.splitlines()
result = ParsedLicenses()
current_category = ""
current_entry: LicenseEntry | None = None
last_key: str | None = None # Letzter erkannter Schlüssel (für Fortsetzungszeilen)
separator_line = "=" * 20 # Mindestlänge für Sektions-Trennlinien
i = 0
while i < len(lines):
line = lines[i]
# Sektions-Trennlinie erkannt → nächste Zeile ist Sektionsname
if line.startswith(separator_line):
i += 1
if i < len(lines):
section_name = lines[i].strip()
# Bei "Lizenztexte" aufhören — ab hier kommen nur noch Volltexte
if section_name == "Lizenztexte":
# Letzten Eintrag noch sichern
if current_entry:
result.entries.append(current_entry)
break
# Neue Kategorie setzen (ignoriere den Datei-Header "THIRD PARTY LICENSES")
if section_name != "THIRD PARTY LICENSES":
current_category = section_name
# Schließende Trennlinie der Sektion überspringen
i += 1
if i < len(lines) and lines[i].startswith(separator_line):
i += 1
continue
# Nummerierter Eintrag (z.B. "1. PySide6")
match = _ENTRY_PATTERN.match(line.strip())
if match:
# Vorherigen Eintrag speichern
if current_entry:
result.entries.append(current_entry)
entry_name = match.group(1).strip()
current_entry = LicenseEntry(name=entry_name, category=current_category)
# Installierte Version ermitteln
pypi_name = _normalize_package_name(entry_name)
current_entry.installed_version = _get_installed_version(pypi_name)
i += 1
continue
# Schlüssel-Wert-Zeile innerhalb eines Eintrags (z.B. " Version: >=6.10.1")
stripped = line.strip()
if current_entry and ":" in stripped:
key, _, value = stripped.partition(":")
key = key.strip()
value = value.strip()
if key in _KNOWN_KEYS:
last_key = key
if key == "Lizenz":
current_entry.license = value
elif key == "Webseite":
current_entry.website = value
elif key == "GitHub" and not current_entry.website:
# GitHub nur als Fallback wenn keine Webseite vorhanden
current_entry.website = value
elif key == "Beschreibung":
current_entry.description = value
elif key == "Copyright":
current_entry.copyright = value
else:
last_key = None
elif current_entry and stripped and not stripped.startswith("="):
# Fortsetzungszeile (z.B. mehrzeiliger Copyright-Eintrag)
if last_key == "Copyright":
current_entry.copyright += "\n" + stripped
i += 1
# Letzten Eintrag sichern (falls Datei nicht mit "Lizenztexte" endet)
if current_entry and current_entry not in result.entries:
result.entries.append(current_entry)
logger.info(f"{len(result.entries)} Lizenzeinträge aus {license_file.name} geladen")
return result
if __name__ == "__main__":
# Standalone-Test
logging.basicConfig(level=logging.DEBUG)
parsed = parse_license_file()
for entry in parsed.entries:
ver = entry.installed_version or ""
logger.debug(f"[{entry.category}] {entry.name}: {ver} ({entry.license})")
+84 -3
View File
@@ -1,21 +1,102 @@
import sys import sys
import logging
from pathlib import Path
from PySide6.QtGui import QIcon
from PySide6.QtWidgets import QApplication from PySide6.QtWidgets import QApplication
from ui.MainWindow import MainWindow from ui.MainWindow import MainWindow
from ui.AppSettings import AppSettingsDlg from ui.AppSettings import AppSettingsDlg
from conf import app_settings from conf import app_settings
# import qdarktheme
def cleanup_old_logs(log_dir, max_age_hours=24):
"""
Löscht Log-Dateien, die älter als die angegebene Anzahl von Stunden sind.
Args:
log_dir: Pfad zum Log-Verzeichnis
max_age_hours: Maximales Alter der Log-Dateien in Stunden (Standard: 24)
"""
import time
if not log_dir.exists():
return
cutoff_time = time.time() - (max_age_hours * 3600)
deleted_count = 0
# Alle .log Dateien im Verzeichnis durchsuchen
for log_file in log_dir.glob("*.log"):
try:
# Änderungszeit der Datei abrufen
file_mtime = log_file.stat().st_mtime
# Wenn die Datei älter als die Grenzzeit ist, löschen
if file_mtime < cutoff_time:
log_file.unlink()
deleted_count += 1
except Exception as e:
logging.warning(f"Fehler beim Löschen von {log_file}: {e}")
if deleted_count > 0:
logging.info(f"{deleted_count} alte Log-Datei(en) gelöscht (älter als {max_age_hours} Stunden)")
def main(): def main():
"""Haupteinstiegspunkt der Anwendung.""" """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(parents=True, 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}")
# Alte Log-Dateien aufräumen (erst nach Logger-Init)
cleanup_old_logs(log_dir, max_age_hours=24)
# Unter Windows: AppUserModelID setzen, damit die Taskleiste das richtige Symbol zeigt
if sys.platform == "win32":
import ctypes
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID("de.vitaligraf.documentor")
# QApplication-Instanz erstellen # QApplication-Instanz erstellen
app = QApplication(sys.argv) app = QApplication(sys.argv)
# Dark Theme aktivieren # App-Symbol setzen (PyInstaller: sys._MEIPASS, sonst Quellpfad)
# qdarktheme.setup_theme("auto") if getattr(sys, "frozen", False):
icon_path = Path(sys._MEIPASS) / "resources" / "icon.ico"
else:
icon_path = Path(__file__).parent.parent / "resources" / "icon.ico"
if icon_path.exists():
app.setWindowIcon(QIcon(str(icon_path)))
# Hauptfenster erstellen # Hauptfenster erstellen
window = MainWindow() window = MainWindow()
+166
View File
@@ -0,0 +1,166 @@
"""
Obsolete-Detector — Erkennung veralteter Projekteinträge nach DB-Import.
Reine Analyselogik ohne Qt-Abhängigkeit. Findet XslFile-Einträge im Projekt,
die nicht mehr in der Datenbank vorhanden sind.
"""
from dataclasses import dataclass, field
from pathlib import Path
from conf import TreeNode, XslFile
@dataclass
class ObsoleteXslEntry:
"""Ein XslFile-Eintrag, der nicht mehr in der Datenbank vorhanden ist."""
xsl_file: XslFile
parent_node: TreeNode
@dataclass
class ObsoleteGroup:
"""Gruppe veralteter XslFile-Einträge unter einem gemeinsamen Eltern-Pfad."""
node_path: list[str]
parent_node: TreeNode
xsl_entries: list[ObsoleteXslEntry] = field(default_factory=list)
def extract_db_xsl_ids(new_nodes: list[TreeNode]) -> set[tuple]:
"""
Extrahiert alle XslFile-IDs aus den frisch aus der DB geladenen Nodes.
Args:
new_nodes: Nodes aus _process_sql_data()
Returns:
Set aller XslFile-IDs (Tupel)
"""
ids: set[tuple] = set()
_collect_ids_recursive(new_nodes, ids)
return ids
def _collect_ids_recursive(nodes: list, ids: set[tuple]) -> None:
for node in nodes:
if isinstance(node, XslFile):
ids.add(node.id)
elif isinstance(node, TreeNode) and node.children:
_collect_ids_recursive(node.children, ids)
def find_obsolete_xsl_entries(
project_nodes: list[TreeNode],
db_xsl_ids: set[tuple],
) -> list[ObsoleteGroup]:
"""
Findet alle XslFile-Einträge im Projekt, die nicht mehr in der DB vorhanden sind.
Args:
project_nodes: Aktuelle Nodes aus pdf_project.nodes
db_xsl_ids: Set aller XslFile-IDs aus der DB (von extract_db_xsl_ids)
Returns:
Liste von ObsoleteGroup, sortiert nach Hierarchiepfad
"""
groups: dict[int, ObsoleteGroup] = {}
_find_obsolete_recursive(project_nodes, db_xsl_ids, path=[], groups=groups)
# Sortieren nach Pfad für stabile Darstellung
return sorted(groups.values(), key=lambda g: g.node_path)
def _find_obsolete_recursive(
nodes: list,
db_xsl_ids: set[tuple],
path: list[str],
groups: dict[int, ObsoleteGroup],
) -> None:
for node in nodes:
if isinstance(node, TreeNode):
_find_obsolete_in_tree_node(node, db_xsl_ids, path, groups)
def _find_obsolete_in_tree_node(
node: TreeNode,
db_xsl_ids: set[tuple],
parent_path: list[str],
groups: dict[int, ObsoleteGroup],
) -> None:
current_path = parent_path + [node.bez]
for child in node.children:
if isinstance(child, XslFile):
if child.id not in db_xsl_ids:
node_id = id(node)
if node_id not in groups:
groups[node_id] = ObsoleteGroup(
node_path=current_path,
parent_node=node,
)
groups[node_id].xsl_entries.append(ObsoleteXslEntry(xsl_file=child, parent_node=node))
elif isinstance(child, TreeNode):
_find_obsolete_in_tree_node(child, db_xsl_ids, current_path, groups)
def remove_empty_tree_nodes(nodes: list) -> list:
"""
Entfernt rekursiv alle TreeNodes, deren children-Liste nach der Bereinigung leer ist.
XslFile-Einträge werden immer behalten (sie wurden bereits entfernt oder sind noch gültig).
Args:
nodes: Liste von TreeNode|XslFile
Returns:
Bereinigte Liste ohne leere TreeNodes
"""
result = []
for node in nodes:
if isinstance(node, TreeNode):
node.children = remove_empty_tree_nodes(node.children)
if node.children:
result.append(node)
# leere TreeNodes stillschweigend verwerfen
else:
result.append(node)
return result
def collect_unused_xml_files(
obsolete_groups: list[ObsoleteGroup],
project_dir: Path,
is_xml_used_elsewhere_fn,
) -> list[tuple[Path, Path]]:
"""
Sammelt XML-Dateien der veralteten XslFiles, die nirgends anders mehr verwendet werden.
WICHTIG: Muss nach dem Entfernen der XslFiles aus dem Modell aufgerufen werden,
damit is_xml_used_elsewhere_fn korrekte Ergebnisse liefert.
Args:
obsolete_groups: Die veralteten Gruppen
project_dir: Absoluter Pfad zum Projektverzeichnis
is_xml_used_elsewhere_fn: Callable(xml_path, exclude_xsl_file) -> bool
Returns:
Liste von (xml_path_relativ, xml_path_absolut) für nicht mehr verwendete XML-Dateien
"""
unused: list[tuple[Path, Path]] = []
seen: set[str] = set()
for group in obsolete_groups:
for entry in group.xsl_entries:
for xml_file_obj in entry.xsl_file.xmls:
xml_path_str = str(xml_file_obj.xml)
if xml_path_str in seen:
continue
seen.add(xml_path_str)
xml_abs = project_dir / xml_file_obj.xml
if xml_abs.exists():
if not is_xml_used_elsewhere_fn(xml_file_obj.xml, entry.xsl_file):
unused.append((xml_file_obj.xml, xml_abs))
return unused
+1 -1
View File
@@ -8,4 +8,4 @@ select
r3.xsl_datei r3.xsl_datei
from reporttyp r from reporttyp r
inner join report r2 on r.reporttyp = r2.reporttyp and r2.aktiv = 1 inner join report r2 on r.reporttyp = r2.reporttyp and r2.aktiv = 1
inner join repfile r3 on r2.reporttyp = r3.reporttyp and r2.report = r3.report and r3.xsl_datei is not null and r3.aktiv = 1 inner join repfile r3 on r2.reporttyp = r3.reporttyp and r2.report = r3.report and r3.xsl_datei is not null and r3.aktiv = 1 and r3.export = 0
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>

After

Width:  |  Height:  |  Size: 239 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>

After

Width:  |  Height:  |  Size: 275 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>

After

Width:  |  Height:  |  Size: 222 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"/></svg>

After

Width:  |  Height:  |  Size: 223 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>

After

Width:  |  Height:  |  Size: 258 B

+1
View File
@@ -0,0 +1 @@
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3h7a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-7m0-18H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h7m0-18v18"/></svg>

After

Width:  |  Height:  |  Size: 288 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg>

After

Width:  |  Height:  |  Size: 318 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="12" y1="18" x2="12" y2="12"/><line x1="9" y1="15" x2="15" y2="15"/></svg>

After

Width:  |  Height:  |  Size: 369 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>

After

Width:  |  Height:  |  Size: 401 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><polyline points="13 2 13 9 20 9"/></svg>

After

Width:  |  Height:  |  Size: 292 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/><line x1="12" y1="11" x2="12" y2="17"/><line x1="9" y1="14" x2="15" y2="14"/></svg>

After

Width:  |  Height:  |  Size: 351 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>

After

Width:  |  Height:  |  Size: 274 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg>

After

Width:  |  Height:  |  Size: 314 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>

After

Width:  |  Height:  |  Size: 298 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>

After

Width:  |  Height:  |  Size: 313 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="8" y1="12" x2="16" y2="12"/></svg>

After

Width:  |  Height:  |  Size: 257 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="10 8 16 12 10 16 10 8"/></svg>

After

Width:  |  Height:  |  Size: 260 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg>

After

Width:  |  Height:  |  Size: 295 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>

After

Width:  |  Height:  |  Size: 339 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>

After

Width:  |  Height:  |  Size: 964 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/></svg>

After

Width:  |  Height:  |  Size: 525 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>

After

Width:  |  Height:  |  Size: 388 B

+27
View File
@@ -0,0 +1,27 @@
<!DOCTYPE RCC>
<RCC version="1.0">
<qresource prefix="/">
<file>icons/folder-plus.svg</file>
<file>icons/log-out.svg</file>
<file>icons/settings.svg</file>
<file>icons/folder.svg</file>
<file>icons/refresh-cw.svg</file>
<file>icons/plus-circle.svg</file>
<file>icons/minus-circle.svg</file>
<file>icons/play-circle.svg</file>
<file>icons/file.svg</file>
<file>icons/check-circle.svg</file>
<file>icons/info.svg</file>
<file>icons/git-branch.svg</file>
<file>icons/file-text.svg</file>
<file>icons/code.svg</file>
<file>icons/chevron-down.svg</file>
<file>icons/chevron-up.svg</file>
<file>icons/trash-2.svg</file>
<file>icons/file-plus.svg</file>
<file>icons/columns.svg</file>
<file>icons/sliders.svg</file>
<file>icons/database.svg</file>
<file>icons/activity.svg</file>
</qresource>
</RCC>
+685
View File
@@ -0,0 +1,685 @@
# Resource object code (Python 3)
# Created by: object code
# Created by: The Resource Compiler for Qt version 6.11.1
# WARNING! All changes made in this file will be lost!
from PySide6 import QtCore
qt_resource_data = b"\
\x00\x00\x01\x01\
<\
svg xmlns=\x22http:\
//www.w3.org/200\
0/svg\x22 width=\x2224\
\x22 height=\x2224\x22 vi\
ewBox=\x220 0 24 24\
\x22 fill=\x22none\x22 st\
roke=\x22currentCol\
or\x22 stroke-width\
=\x222\x22 stroke-line\
cap=\x22round\x22 stro\
ke-linejoin=\x22rou\
nd\x22><circle cx=\x22\
12\x22 cy=\x2212\x22 r=\x221\
0\x22/><line x1=\x228\x22\
y1=\x2212\x22 x2=\x2216\x22\
y2=\x2212\x22/></svg>\
\
\x00\x00\x02\x0d\
<\
svg xmlns=\x22http:\
//www.w3.org/200\
0/svg\x22 width=\x2224\
\x22 height=\x2224\x22 vi\
ewBox=\x220 0 24 24\
\x22 fill=\x22none\x22 st\
roke=\x22currentCol\
or\x22 stroke-width\
=\x222\x22 stroke-line\
cap=\x22round\x22 stro\
ke-linejoin=\x22rou\
nd\x22><line x1=\x224\x22\
y1=\x2221\x22 x2=\x224\x22 \
y2=\x2214\x22/><line x\
1=\x224\x22 y1=\x2210\x22 x2\
=\x224\x22 y2=\x223\x22/><li\
ne x1=\x2212\x22 y1=\x222\
1\x22 x2=\x2212\x22 y2=\x221\
2\x22/><line x1=\x2212\
\x22 y1=\x228\x22 x2=\x2212\x22\
y2=\x223\x22/><line x\
1=\x2220\x22 y1=\x2221\x22 x\
2=\x2220\x22 y2=\x2216\x22/>\
<line x1=\x2220\x22 y1\
=\x2212\x22 x2=\x2220\x22 y2\
=\x223\x22/><line x1=\x22\
1\x22 y1=\x2214\x22 x2=\x227\
\x22 y2=\x2214\x22/><line\
x1=\x229\x22 y1=\x228\x22 x\
2=\x2215\x22 y2=\x228\x22/><\
line x1=\x2217\x22 y1=\
\x2216\x22 x2=\x2223\x22 y2=\
\x2216\x22/></svg>\
\x00\x00\x01\x04\
<\
svg xmlns=\x22http:\
//www.w3.org/200\
0/svg\x22 width=\x2224\
\x22 height=\x2224\x22 vi\
ewBox=\x220 0 24 24\
\x22 fill=\x22none\x22 st\
roke=\x22currentCol\
or\x22 stroke-width\
=\x222\x22 stroke-line\
cap=\x22round\x22 stro\
ke-linejoin=\x22rou\
nd\x22><circle cx=\x22\
12\x22 cy=\x2212\x22 r=\x221\
0\x22/><polygon poi\
nts=\x2210 8 16 12 \
10 16 10 8\x22/></s\
vg>\
\x00\x00\x01 \
<\
svg width=\x2224\x22 h\
eight=\x2224\x22 viewB\
ox=\x220 0 24 24\x22 x\
mlns=\x22http://www\
.w3.org/2000/svg\
\x22 fill=\x22none\x22 st\
roke=\x22currentCol\
or\x22 stroke-width\
=\x222\x22 stroke-line\
cap=\x22round\x22 stro\
ke-linejoin=\x22rou\
nd\x22><path d=\x22M12\
3h7a2 2 0 0 1 2\
2v14a2 2 0 0 1-\
2 2h-7m0-18H5a2 \
2 0 0 0-2 2v14a2\
2 0 0 0 2 2h7m0\
-18v18\x22/></svg>\
\x00\x00\x01_\
<\
svg xmlns=\x22http:\
//www.w3.org/200\
0/svg\x22 width=\x2224\
\x22 height=\x2224\x22 vi\
ewBox=\x220 0 24 24\
\x22 fill=\x22none\x22 st\
roke=\x22currentCol\
or\x22 stroke-width\
=\x222\x22 stroke-line\
cap=\x22round\x22 stro\
ke-linejoin=\x22rou\
nd\x22><path d=\x22M22\
19a2 2 0 0 1-2 \
2H4a2 2 0 0 1-2-\
2V5a2 2 0 0 1 2-\
2h5l2 3h9a2 2 0 \
0 1 2 2z\x22/><line\
x1=\x2212\x22 y1=\x2211\x22\
x2=\x2212\x22 y2=\x2217\x22\
/><line x1=\x229\x22 y\
1=\x2214\x22 x2=\x2215\x22 y\
2=\x2214\x22/></svg>\
\x00\x00\x01$\
<\
svg xmlns=\x22http:\
//www.w3.org/200\
0/svg\x22 width=\x2224\
\x22 height=\x2224\x22 vi\
ewBox=\x220 0 24 24\
\x22 fill=\x22none\x22 st\
roke=\x22currentCol\
or\x22 stroke-width\
=\x222\x22 stroke-line\
cap=\x22round\x22 stro\
ke-linejoin=\x22rou\
nd\x22><path d=\x22M13\
2H6a2 2 0 0 0-2\
2v16a2 2 0 0 0 \
2 2h12a2 2 0 0 0\
2-2V9z\x22/><polyl\
ine points=\x2213 2\
13 9 20 9\x22/></s\
vg>\
\x00\x00\x01S\
<\
svg xmlns=\x22http:\
//www.w3.org/200\
0/svg\x22 width=\x2224\
\x22 height=\x2224\x22 vi\
ewBox=\x220 0 24 24\
\x22 fill=\x22none\x22 st\
roke=\x22currentCol\
or\x22 stroke-width\
=\x222\x22 stroke-line\
cap=\x22round\x22 stro\
ke-linejoin=\x22rou\
nd\x22><polyline po\
ints=\x2223 4 23 10\
17 10\x22/><polyli\
ne points=\x221 20 \
1 14 7 14\x22/><pat\
h d=\x22M3.51 9a9 9\
0 0 1 14.85-3.3\
6L23 10M1 14l4.6\
4 4.36A9 9 0 0 0\
20.49 15\x22/></sv\
g>\
\x00\x00\x01>\
<\
svg xmlns=\x22http:\
//www.w3.org/200\
0/svg\x22 width=\x2224\
\x22 height=\x2224\x22 vi\
ewBox=\x220 0 24 24\
\x22 fill=\x22none\x22 st\
roke=\x22currentCol\
or\x22 stroke-width\
=\x222\x22 stroke-line\
cap=\x22round\x22 stro\
ke-linejoin=\x22rou\
nd\x22><ellipse cx=\
\x2212\x22 cy=\x225\x22 rx=\x22\
9\x22 ry=\x223\x22/><path\
d=\x22M21 12c0 1.6\
6-4 3-9 3s-9-1.3\
4-9-3\x22/><path d=\
\x22M3 5v14c0 1.66 \
4 3 9 3s9-1.34 9\
-3V5\x22/></svg>\
\x00\x00\x01\x12\
<\
svg xmlns=\x22http:\
//www.w3.org/200\
0/svg\x22 width=\x2224\
\x22 height=\x2224\x22 vi\
ewBox=\x220 0 24 24\
\x22 fill=\x22none\x22 st\
roke=\x22currentCol\
or\x22 stroke-width\
=\x222\x22 stroke-line\
cap=\x22round\x22 stro\
ke-linejoin=\x22rou\
nd\x22><path d=\x22M22\
19a2 2 0 0 1-2 \
2H4a2 2 0 0 1-2-\
2V5a2 2 0 0 1 2-\
2h5l2 3h9a2 2 0 \
0 1 2 2z\x22/></svg\
>\
\x00\x00\x00\xde\
<\
svg xmlns=\x22http:\
//www.w3.org/200\
0/svg\x22 width=\x2224\
\x22 height=\x2224\x22 vi\
ewBox=\x220 0 24 24\
\x22 fill=\x22none\x22 st\
roke=\x22currentCol\
or\x22 stroke-width\
=\x222\x22 stroke-line\
cap=\x22round\x22 stro\
ke-linejoin=\x22rou\
nd\x22><polyline po\
ints=\x226 9 12 15 \
18 9\x22/></svg>\
\x00\x00\x03\xc4\
<\
svg xmlns=\x22http:\
//www.w3.org/200\
0/svg\x22 width=\x2224\
\x22 height=\x2224\x22 vi\
ewBox=\x220 0 24 24\
\x22 fill=\x22none\x22 st\
roke=\x22currentCol\
or\x22 stroke-width\
=\x222\x22 stroke-line\
cap=\x22round\x22 stro\
ke-linejoin=\x22rou\
nd\x22><circle cx=\x22\
12\x22 cy=\x2212\x22 r=\x223\
\x22/><path d=\x22M19.\
4 15a1.65 1.65 0\
0 0 .33 1.82l.0\
6.06a2 2 0 0 1 0\
2.83 2 2 0 0 1-\
2.83 0l-.06-.06a\
1.65 1.65 0 0 0-\
1.82-.33 1.65 1.\
65 0 0 0-1 1.51V\
21a2 2 0 0 1-2 2\
2 2 0 0 1-2-2v-\
.09A1.65 1.65 0 \
0 0 9 19.4a1.65 \
1.65 0 0 0-1.82.\
33l-.06.06a2 2 0\
0 1-2.83 0 2 2 \
0 0 1 0-2.83l.06\
-.06a1.65 1.65 0\
0 0 .33-1.82 1.\
65 1.65 0 0 0-1.\
51-1H3a2 2 0 0 1\
-2-2 2 2 0 0 1 2\
-2h.09A1.65 1.65\
0 0 0 4.6 9a1.6\
5 1.65 0 0 0-.33\
-1.82l-.06-.06a2\
2 0 0 1 0-2.83 \
2 2 0 0 1 2.83 0\
l.06.06a1.65 1.6\
5 0 0 0 1.82.33H\
9a1.65 1.65 0 0 \
0 1-1.51V3a2 2 0\
0 1 2-2 2 2 0 0\
1 2 2v.09a1.65 \
1.65 0 0 0 1 1.5\
1 1.65 1.65 0 0 \
0 1.82-.33l.06-.\
06a2 2 0 0 1 2.8\
3 0 2 2 0 0 1 0 \
2.83l-.06.06a1.6\
5 1.65 0 0 0-.33\
1.82V9a1.65 1.6\
5 0 0 0 1.51 1H2\
1a2 2 0 0 1 2 2 \
2 2 0 0 1-2 2h-.\
09a1.65 1.65 0 0\
0-1.51 1z\x22/></s\
vg>\
\x00\x00\x01'\
<\
svg xmlns=\x22http:\
//www.w3.org/200\
0/svg\x22 width=\x2224\
\x22 height=\x2224\x22 vi\
ewBox=\x220 0 24 24\
\x22 fill=\x22none\x22 st\
roke=\x22currentCol\
or\x22 stroke-width\
=\x222\x22 stroke-line\
cap=\x22round\x22 stro\
ke-linejoin=\x22rou\
nd\x22><circle cx=\x22\
12\x22 cy=\x2212\x22 r=\x221\
0\x22/><line x1=\x2212\
\x22 y1=\x228\x22 x2=\x2212\x22\
y2=\x2216\x22/><line \
x1=\x228\x22 y1=\x2212\x22 x\
2=\x2216\x22 y2=\x2212\x22/>\
</svg>\
\x00\x00\x019\
<\
svg xmlns=\x22http:\
//www.w3.org/200\
0/svg\x22 width=\x2224\
\x22 height=\x2224\x22 vi\
ewBox=\x220 0 24 24\
\x22 fill=\x22none\x22 st\
roke=\x22currentCol\
or\x22 stroke-width\
=\x222\x22 stroke-line\
cap=\x22round\x22 stro\
ke-linejoin=\x22rou\
nd\x22><path d=\x22M9 \
21H5a2 2 0 0 1-2\
-2V5a2 2 0 0 1 2\
-2h4\x22/><polyline\
points=\x2216 17 2\
1 12 16 7\x22/><lin\
e x1=\x2221\x22 y1=\x2212\
\x22 x2=\x229\x22 y2=\x2212\x22\
/></svg>\
\x00\x00\x01\x13\
<\
svg xmlns=\x22http:\
//www.w3.org/200\
0/svg\x22 width=\x2224\
\x22 height=\x2224\x22 vi\
ewBox=\x220 0 24 24\
\x22 fill=\x22none\x22 st\
roke=\x22currentCol\
or\x22 stroke-width\
=\x222\x22 stroke-line\
cap=\x22round\x22 stro\
ke-linejoin=\x22rou\
nd\x22><path d=\x22M22\
11.08V12a10 10 \
0 1 1-5.93-9.14\x22\
/><polyline poin\
ts=\x2222 4 12 14.0\
1 9 11.01\x22/></sv\
g>\
\x00\x00\x01*\
<\
svg xmlns=\x22http:\
//www.w3.org/200\
0/svg\x22 width=\x2224\
\x22 height=\x2224\x22 vi\
ewBox=\x220 0 24 24\
\x22 fill=\x22none\x22 st\
roke=\x22currentCol\
or\x22 stroke-width\
=\x222\x22 stroke-line\
cap=\x22round\x22 stro\
ke-linejoin=\x22rou\
nd\x22><circle cx=\x22\
12\x22 cy=\x2212\x22 r=\x221\
0\x22/><line x1=\x2212\
\x22 y1=\x2216\x22 x2=\x2212\
\x22 y2=\x2212\x22/><line\
x1=\x2212\x22 y1=\x228\x22 \
x2=\x2212.01\x22 y2=\x228\
\x22/></svg>\
\x00\x00\x00\xdf\
<\
svg xmlns=\x22http:\
//www.w3.org/200\
0/svg\x22 width=\x2224\
\x22 height=\x2224\x22 vi\
ewBox=\x220 0 24 24\
\x22 fill=\x22none\x22 st\
roke=\x22currentCol\
or\x22 stroke-width\
=\x222\x22 stroke-line\
cap=\x22round\x22 stro\
ke-linejoin=\x22rou\
nd\x22><polyline po\
ints=\x2218 15 12 9\
6 15\x22/></svg>\
\x00\x00\x01\x91\
<\
svg xmlns=\x22http:\
//www.w3.org/200\
0/svg\x22 width=\x2224\
\x22 height=\x2224\x22 vi\
ewBox=\x220 0 24 24\
\x22 fill=\x22none\x22 st\
roke=\x22currentCol\
or\x22 stroke-width\
=\x222\x22 stroke-line\
cap=\x22round\x22 stro\
ke-linejoin=\x22rou\
nd\x22><path d=\x22M14\
2H6a2 2 0 0 0-2\
2v16a2 2 0 0 0 \
2 2h12a2 2 0 0 0\
2-2V8z\x22/><polyl\
ine points=\x2214 2\
14 8 20 8\x22/><li\
ne x1=\x2216\x22 y1=\x221\
3\x22 x2=\x228\x22 y2=\x2213\
\x22/><line x1=\x2216\x22\
y1=\x2217\x22 x2=\x228\x22 \
y2=\x2217\x22/><polyli\
ne points=\x2210 9 \
9 9 8 9\x22/></svg>\
\
\x00\x00\x00\xef\
<\
svg xmlns=\x22http:\
//www.w3.org/200\
0/svg\x22 width=\x2224\
\x22 height=\x2224\x22 vi\
ewBox=\x220 0 24 24\
\x22 fill=\x22none\x22 st\
roke=\x22currentCol\
or\x22 stroke-width\
=\x222\x22 stroke-line\
cap=\x22round\x22 stro\
ke-linejoin=\x22rou\
nd\x22><polyline po\
ints=\x2222 12 18 1\
2 15 21 9 3 6 12\
2 12\x22/></svg>\
\x00\x00\x01q\
<\
svg xmlns=\x22http:\
//www.w3.org/200\
0/svg\x22 width=\x2224\
\x22 height=\x2224\x22 vi\
ewBox=\x220 0 24 24\
\x22 fill=\x22none\x22 st\
roke=\x22currentCol\
or\x22 stroke-width\
=\x222\x22 stroke-line\
cap=\x22round\x22 stro\
ke-linejoin=\x22rou\
nd\x22><path d=\x22M14\
2H6a2 2 0 0 0-2\
2v16a2 2 0 0 0 \
2 2h12a2 2 0 0 0\
2-2V8z\x22/><polyl\
ine points=\x2214 2\
14 8 20 8\x22/><li\
ne x1=\x2212\x22 y1=\x221\
8\x22 x2=\x2212\x22 y2=\x221\
2\x22/><line x1=\x229\x22\
y1=\x2215\x22 x2=\x2215\x22\
y2=\x2215\x22/></svg>\
\
\x00\x00\x01\x02\
<\
svg xmlns=\x22http:\
//www.w3.org/200\
0/svg\x22 width=\x2224\
\x22 height=\x2224\x22 vi\
ewBox=\x220 0 24 24\
\x22 fill=\x22none\x22 st\
roke=\x22currentCol\
or\x22 stroke-width\
=\x222\x22 stroke-line\
cap=\x22round\x22 stro\
ke-linejoin=\x22rou\
nd\x22><polyline po\
ints=\x2216 18 22 1\
2 16 6\x22/><polyli\
ne points=\x228 6 2\
12 8 18\x22/></svg\
>\
\x00\x00\x01\x84\
<\
svg xmlns=\x22http:\
//www.w3.org/200\
0/svg\x22 width=\x2224\
\x22 height=\x2224\x22 vi\
ewBox=\x220 0 24 24\
\x22 fill=\x22none\x22 st\
roke=\x22currentCol\
or\x22 stroke-width\
=\x222\x22 stroke-line\
cap=\x22round\x22 stro\
ke-linejoin=\x22rou\
nd\x22><polyline po\
ints=\x223 6 5 6 21\
6\x22/><path d=\x22M1\
9 6v14a2 2 0 0 1\
-2 2H7a2 2 0 0 1\
-2-2V6m3 0V4a2 2\
0 0 1 2-2h4a2 2\
0 0 1 2 2v2\x22/><\
line x1=\x2210\x22 y1=\
\x2211\x22 x2=\x2210\x22 y2=\
\x2217\x22/><line x1=\x22\
14\x22 y1=\x2211\x22 x2=\x22\
14\x22 y2=\x2217\x22/></s\
vg>\
\x00\x00\x01:\
<\
svg xmlns=\x22http:\
//www.w3.org/200\
0/svg\x22 width=\x2224\
\x22 height=\x2224\x22 vi\
ewBox=\x220 0 24 24\
\x22 fill=\x22none\x22 st\
roke=\x22currentCol\
or\x22 stroke-width\
=\x222\x22 stroke-line\
cap=\x22round\x22 stro\
ke-linejoin=\x22rou\
nd\x22><line x1=\x226\x22\
y1=\x223\x22 x2=\x226\x22 y\
2=\x2215\x22/><circle \
cx=\x2218\x22 cy=\x226\x22 r\
=\x223\x22/><circle cx\
=\x226\x22 cy=\x2218\x22 r=\x22\
3\x22/><path d=\x22M18\
9a9 9 0 0 1-9 9\
\x22/></svg>\
"
qt_resource_name = b"\
\x00\x05\
\x00o\xa6S\
\x00i\
\x00c\x00o\x00n\x00s\
\x00\x10\
\x02\xfe\x1a\xa7\
\x00m\
\x00i\x00n\x00u\x00s\x00-\x00c\x00i\x00r\x00c\x00l\x00e\x00.\x00s\x00v\x00g\
\x00\x0b\
\x0cb\x05\x87\
\x00s\
\x00l\x00i\x00d\x00e\x00r\x00s\x00.\x00s\x00v\x00g\
\x00\x0f\
\x0b\xa3Gg\
\x00p\
\x00l\x00a\x00y\x00-\x00c\x00i\x00r\x00c\x00l\x00e\x00.\x00s\x00v\x00g\
\x00\x0b\
\x04\x82\x9dG\
\x00c\
\x00o\x00l\x00u\x00m\x00n\x00s\x00.\x00s\x00v\x00g\
\x00\x0f\
\x06\x9fG\xa7\
\x00f\
\x00o\x00l\x00d\x00e\x00r\x00-\x00p\x00l\x00u\x00s\x00.\x00s\x00v\x00g\
\x00\x08\
\x00(Wg\
\x00f\
\x00i\x00l\x00e\x00.\x00s\x00v\x00g\
\x00\x0e\
\x04\x1b\xd7\x87\
\x00r\
\x00e\x00f\x00r\x00e\x00s\x00h\x00-\x00c\x00w\x00.\x00s\x00v\x00g\
\x00\x0c\
\x05\xc9\x15\xc7\
\x00d\
\x00a\x00t\x00a\x00b\x00a\x00s\x00e\x00.\x00s\x00v\x00g\
\x00\x0a\
\x0a\xc8\xf6\x87\
\x00f\
\x00o\x00l\x00d\x00e\x00r\x00.\x00s\x00v\x00g\
\x00\x10\
\x0e\x17\x06\x87\
\x00c\
\x00h\x00e\x00v\x00r\x00o\x00n\x00-\x00d\x00o\x00w\x00n\x00.\x00s\x00v\x00g\
\x00\x0c\
\x0b\xdf,\xc7\
\x00s\
\x00e\x00t\x00t\x00i\x00n\x00g\x00s\x00.\x00s\x00v\x00g\
\x00\x0f\
\x02\xe3G'\
\x00p\
\x00l\x00u\x00s\x00-\x00c\x00i\x00r\x00c\x00l\x00e\x00.\x00s\x00v\x00g\
\x00\x0b\
\x06!\xeeG\
\x00l\
\x00o\x00g\x00-\x00o\x00u\x00t\x00.\x00s\x00v\x00g\
\x00\x10\
\x0d\xfd\xe1'\
\x00c\
\x00h\x00e\x00c\x00k\x00-\x00c\x00i\x00r\x00c\x00l\x00e\x00.\x00s\x00v\x00g\
\x00\x08\
\x04\xd2T\xc7\
\x00i\
\x00n\x00f\x00o\x00.\x00s\x00v\x00g\
\x00\x0e\
\x09Xl\x87\
\x00c\
\x00h\x00e\x00v\x00r\x00o\x00n\x00-\x00u\x00p\x00.\x00s\x00v\x00g\
\x00\x0d\
\x06\xf2R'\
\x00f\
\x00i\x00l\x00e\x00-\x00t\x00e\x00x\x00t\x00.\x00s\x00v\x00g\
\x00\x0c\
\x0cQ;g\
\x00a\
\x00c\x00t\x00i\x00v\x00i\x00t\x00y\x00.\x00s\x00v\x00g\
\x00\x0d\
\x09\xc3S\x87\
\x00f\
\x00i\x00l\x00e\x00-\x00p\x00l\x00u\x00s\x00.\x00s\x00v\x00g\
\x00\x08\
\x05\xa8W\x87\
\x00c\
\x00o\x00d\x00e\x00.\x00s\x00v\x00g\
\x00\x0b\
\x0b\xf2K\xe7\
\x00t\
\x00r\x00a\x00s\x00h\x00-\x002\x00.\x00s\x00v\x00g\
\x00\x0e\
\x04|qG\
\x00g\
\x00i\x00t\x00-\x00b\x00r\x00a\x00n\x00c\x00h\x00.\x00s\x00v\x00g\
"
qt_resource_struct = b"\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x16\x00\x00\x00\x02\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\xb6\x00\x00\x00\x00\x00\x01\x00\x00\x06\xa5\
\x00\x00\x01\x9ez2\xf1\x12\
\x00\x00\x01j\x00\x00\x00\x00\x00\x01\x00\x00\x10&\
\x00\x00\x01\x9ez2\xf1\x12\
\x00\x00\x00\x10\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
\x00\x00\x01\x9ez2\xf1\x12\
\x00\x00\x00\xcc\x00\x00\x00\x00\x00\x01\x00\x00\x07\xcd\
\x00\x00\x01\x9ez2\xf1\x12\
\x00\x00\x02\x98\x00\x00\x00\x00\x00\x01\x00\x00\x1cA\
\x00\x00\x01\x9ez2\xf1\x12\
\x00\x00\x00v\x00\x00\x00\x00\x00\x01\x00\x00\x04\x1e\
\x00\x00\x01\x9ez2\xf1\x11\
\x00\x00\x01\xd0\x00\x00\x00\x00\x00\x01\x00\x00\x13\xa5\
\x00\x00\x01\x9ez2\xf1\x12\
\x00\x00\x02f\x00\x00\x00\x00\x00\x01\x00\x00\x19\xb3\
\x00\x00\x01\x9ez2\xf1\x11\
\x00\x00\x00\xee\x00\x00\x00\x00\x00\x01\x00\x00\x09$\
\x00\x00\x01\x9e}\xa1\x91F\
\x00\x00\x01\x8e\x00\x00\x00\x00\x00\x01\x00\x00\x11Q\
\x00\x00\x01\x9ez2\xf1\x12\
\x00\x00\x00\x92\x00\x00\x00\x00\x00\x01\x00\x00\x05B\
\x00\x00\x01\x9ez2\xf1\x12\
\x00\x00\x02\x08\x00\x00\x00\x00\x00\x01\x00\x00\x15\xb6\
\x00\x00\x01\x9ez2\xf1\x11\
\x00\x00\x01\xe6\x00\x00\x00\x00\x00\x01\x00\x00\x14\xd3\
\x00\x00\x01\x9ez2\xf1\x11\
\x00\x00\x02F\x00\x00\x00\x00\x00\x01\x00\x00\x18>\
\x00\x00\x01\x9ez2\xf1\x11\
\x00\x00\x01\x0c\x00\x00\x00\x00\x00\x01\x00\x00\x0af\
\x00\x00\x01\x9ez2\xf1\x12\
\x00\x00\x00R\x00\x00\x00\x00\x00\x01\x00\x00\x03\x16\
\x00\x00\x01\x9ez2\xf1\x12\
\x00\x00\x01L\x00\x00\x00\x00\x00\x01\x00\x00\x0c^\
\x00\x00\x01\x9ez2\xf1\x12\
\x00\x00\x02|\x00\x00\x00\x00\x00\x01\x00\x00\x1a\xb9\
\x00\x00\x01\x9ez2\xf1\x12\
\x00\x00\x02(\x00\x00\x00\x00\x00\x01\x00\x00\x17K\
\x00\x00\x01\x9e}\xa1\x92!\
\x00\x00\x006\x00\x00\x00\x00\x00\x01\x00\x00\x01\x05\
\x00\x00\x01\x9ez2\xf1\x12\
\x00\x00\x01\xaa\x00\x00\x00\x00\x00\x01\x00\x00\x12\x8e\
\x00\x00\x01\x9ez2\xf1\x11\
\x00\x00\x01&\x00\x00\x00\x00\x00\x01\x00\x00\x0b|\
\x00\x00\x01\x9ez2\xf1\x11\
"
def qInitResources():
QtCore.qRegisterResourceData(0x03, qt_resource_struct, qt_resource_name, qt_resource_data)
def qCleanupResources():
QtCore.qUnregisterResourceData(0x03, qt_resource_struct, qt_resource_name, qt_resource_data)
qInitResources()
+34
View File
File diff suppressed because one or more lines are too long
+283
View File
@@ -0,0 +1,283 @@
"""
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
from pathlib import Path
from typing import Optional
from worker_pool_base import BaseWorkerPool, build_jar_classpath
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();
// Cache für kompilierte Stylesheets (Performance-Optimierung)
Map<String, Templates> templatesCache = new HashMap<>();
System.err.println("SaxonWorker started and ready (using JAXP Transformer API with stylesheet caching)");
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];
// Prüfe ob Stylesheet bereits im Cache ist
Templates templates;
if (templatesCache.containsKey(xslStylesheet)) {
templates = templatesCache.get(xslStylesheet);
System.err.println("DEBUG: Using cached stylesheet: " + xslStylesheet);
System.err.flush();
} else {
System.err.println("DEBUG: Compiling and caching stylesheet: " + xslStylesheet);
System.err.flush();
StreamSource xslSource = new StreamSource(new File(xslStylesheet));
templates = factory.newTemplates(xslSource);
templatesCache.put(xslStylesheet, templates);
System.err.println("DEBUG: Stylesheet compiled and cached (cache size: " + templatesCache.size() + ")");
System.err.flush();
}
System.err.println("DEBUG: Creating transformer from cached template...");
System.err.flush();
// Create Source and Result objects
StreamSource xmlSource = new StreamSource(new File(sourceXml));
StreamResult result = new StreamResult(new File(outputFo));
// Create transformer from templates
Transformer transformer = templates.newTransformer();
// 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(BaseWorkerPool):
"""
Pool von lang-laufenden JVM-Prozessen für Saxon-Transformationen (JAXP/XSLT 1.0).
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,
):
super().__init__(num_workers, java_vm_path, log_dir)
self.saxon_jar_path = saxon_jar_path
self.classpath_cache = classpath_cache
self._compile_worker_class()
self._start_workers()
logger.info(f"SaxonWorkerPool initialisiert mit {num_workers} Workern")
# --- Abstrakte Properties ---
@property
def _pool_name(self) -> str:
return "Saxon"
@property
def _java_source_code(self) -> str:
return SAXON_WORKER_JAVA
@property
def _java_class_name(self) -> str:
return "SaxonWorker"
@property
def _temp_dir_prefix(self) -> str:
return "saxon_worker_"
@property
def _worker_init_sleep(self) -> float:
return 0.1
# --- Abstrakte Methoden ---
def _get_classpath(self) -> str:
saxon_dir = self.saxon_jar_path.parent
if saxon_dir not in self.classpath_cache:
self.classpath_cache[saxon_dir] = build_jar_classpath(saxon_dir)
return self.classpath_cache[saxon_dir]
def _build_worker_cmd(self, full_classpath: str) -> list[str]:
return [str(self.java_vm_path), "-cp", full_classpath, "SaxonWorker"]
def _stderr_log_name(self, i: int) -> str:
return f"worker_{i}_stderr.log"
# --- Saxon-spezifische Job-Methode ---
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)
"""
worker_idx = self._acquire_worker()
try:
worker = self.workers[worker_idx]
if worker.poll() is not None:
stderr_content = self._read_stderr_log(worker_idx)
error_msg = f"Worker {worker_idx} ist beendet (Exit: {worker.returncode})\nstderr:\n{stderr_content}"
logger.error(error_msg)
return False, error_msg
params_str = "|||".join([f"{k}={v}" for k, v in xslt_params.items()])
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}")
worker.stdin.write(job)
worker.stdin.flush()
response = worker.stdout.readline().strip()
logger.debug(f"Worker {worker_idx} Antwort: '{response}'")
if response == "OK":
return True, "Erfolgreich"
elif response.startswith("ERROR:"):
return False, f"Saxon-Fehler: {response[6:].strip()}"
elif not response:
stderr_content = self._read_stderr_log(worker_idx, tail=500)
return False, f"Worker {worker_idx} crashed (keine Antwort)\nstderr:\n{stderr_content}"
else:
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:
self.worker_locks[worker_idx].release()
+267
View File
@@ -0,0 +1,267 @@
"""
Saxon Worker Pool (s9api) - Persistente JVM-Prozesse für XSLT 2.0/3.0 Transformationen.
Diese Variante verwendet die Saxon s9api API anstatt JAXP und ist für XSLT 2.0 und 3.0 geeignet.
Eliminiert JVM-Startup-Overhead durch Vorinitialisierung von N Worker-Prozessen.
Jeder Worker läuft als Daemon und verarbeitet mehrere Transformationen nacheinander.
"""
import logging
from pathlib import Path
from typing import Optional
from worker_pool_base import BaseWorkerPool, build_jar_classpath
logger = logging.getLogger(__name__)
# Java-Worker-Code für s9api (wird zur Laufzeit kompiliert)
SAXON_S9API_WORKER_JAVA = """
import net.sf.saxon.s9api.*;
import javax.xml.transform.stream.StreamSource;
import java.io.*;
import java.util.HashMap;
import java.util.Map;
public class SaxonS9ApiWorker {
public static void main(String[] args) {
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
String line;
// Create Processor once and reuse (equivalent to TransformerFactory)
Processor processor = new Processor(false);
// Cache für kompilierte Stylesheets (Performance-Optimierung)
Map<String, XsltExecutable> stylesheetCache = new HashMap<>();
System.err.println("SaxonS9ApiWorker started and ready (using s9api for XSLT 2.0/3.0 with stylesheet caching)");
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("SaxonS9ApiWorker 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];
// Prüfe ob Stylesheet bereits im Cache ist
XsltExecutable executable;
if (stylesheetCache.containsKey(xslStylesheet)) {
executable = stylesheetCache.get(xslStylesheet);
System.err.println("DEBUG: Using cached stylesheet: " + xslStylesheet);
System.err.flush();
} else {
System.err.println("DEBUG: Compiling and caching stylesheet: " + xslStylesheet);
System.err.flush();
XsltCompiler compiler = processor.newXsltCompiler();
executable = compiler.compile(new StreamSource(new File(xslStylesheet)));
stylesheetCache.put(xslStylesheet, executable);
System.err.println("DEBUG: Stylesheet compiled and cached (cache size: " + stylesheetCache.size() + ")");
System.err.flush();
}
System.err.println("DEBUG: Creating transformer...");
System.err.flush();
// Create transformer
XsltTransformer transformer = executable.load();
// Set source
transformer.setSource(new StreamSource(new File(sourceXml)));
// Set destination
Serializer serializer = processor.newSerializer(new File(outputFo));
transformer.setDestination(serializer);
// 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(new QName(kv[0]), new XdmAtomicValue(kv[1]));
System.err.println("DEBUG: Set parameter: " + kv[0] + " = " + kv[1]);
}
}
System.err.flush();
}
System.err.println("DEBUG: Running transformation...");
System.err.flush();
// Transform
transformer.transform();
System.err.println("DEBUG: Transformation completed");
System.err.flush();
System.out.println("OK");
System.out.flush();
} catch (SaxonApiException e) {
System.err.println("DEBUG: SaxonApiException: " + 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("SaxonS9ApiWorker I/O error: " + e.getMessage());
e.printStackTrace(System.err);
}
}
}
"""
class SaxonWorkerPoolS9Api(BaseWorkerPool):
"""
Pool von lang-laufenden JVM-Prozessen für Saxon-Transformationen mit s9api.
Diese Variante verwendet die Saxon s9api API anstatt JAXP und unterstützt
vollständig XSLT 2.0 und 3.0 Transformationen.
"""
def __init__(
self,
num_workers: int,
java_vm_path: Path,
saxon_jar_path: Path,
classpath_cache: dict[Path, str],
log_dir: Optional[Path] = None,
):
super().__init__(num_workers, java_vm_path, log_dir)
self.saxon_jar_path = saxon_jar_path
self.classpath_cache = classpath_cache
self._compile_worker_class()
self._start_workers()
logger.info(f"SaxonWorkerPoolS9Api initialisiert mit {num_workers} Workern (XSLT 2.0/3.0)")
# --- Abstrakte Properties ---
@property
def _pool_name(self) -> str:
return "Saxon-S9Api"
@property
def _java_source_code(self) -> str:
return SAXON_S9API_WORKER_JAVA
@property
def _java_class_name(self) -> str:
return "SaxonS9ApiWorker"
@property
def _temp_dir_prefix(self) -> str:
return "saxon_s9api_worker_"
@property
def _worker_init_sleep(self) -> float:
return 0.1
# --- Abstrakte Methoden ---
def _get_classpath(self) -> str:
saxon_dir = self.saxon_jar_path.parent
if saxon_dir not in self.classpath_cache:
self.classpath_cache[saxon_dir] = build_jar_classpath(saxon_dir)
logger.debug(f"Classpath für {saxon_dir} neu erstellt und gecacht")
return self.classpath_cache[saxon_dir]
def _build_worker_cmd(self, full_classpath: str) -> list[str]:
return [str(self.java_vm_path), "-cp", full_classpath, "SaxonS9ApiWorker"]
def _stderr_log_name(self, i: int) -> str:
return f"s9api_worker_{i}_stderr.log"
# --- Saxon-s9api-spezifische Job-Methode ---
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)
"""
worker_idx = self._acquire_worker()
try:
worker = self.workers[worker_idx]
if worker.poll() is not None:
stderr_content = self._read_stderr_log(worker_idx)
error_msg = (
f"S9Api Worker {worker_idx} ist beendet (Exit: {worker.returncode})\nstderr:\n{stderr_content}"
)
logger.error(error_msg)
return False, error_msg
params_str = "|||".join([f"{k}={v}" for k, v in xslt_params.items()])
job = f"{source_xml}\t{xsl_stylesheet}\t{output_fo}\t{params_str}\n"
logger.debug(f"Sende Job an S9Api Worker {worker_idx}: {source_xml.name}")
worker.stdin.write(job)
worker.stdin.flush()
response = worker.stdout.readline().strip()
logger.debug(f"S9Api Worker {worker_idx} Antwort: '{response}'")
if response == "OK":
return True, "Erfolgreich"
elif response.startswith("ERROR:"):
return False, f"Saxon-Fehler (s9api): {response[6:].strip()}"
elif not response:
stderr_content = self._read_stderr_log(worker_idx, tail=500)
return False, f"S9Api Worker {worker_idx} crashed (keine Antwort)\nstderr:\n{stderr_content}"
else:
return False, f"Unerwartete Antwort: {response}"
except Exception as e:
logger.error(f"Fehler bei S9Api Worker {worker_idx}: {e}")
return False, f"Worker-Fehler: {str(e)}"
finally:
self.worker_locks[worker_idx].release()
+608
View File
@@ -0,0 +1,608 @@
"""
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
import sys
from pathlib import Path
from datetime import datetime
from typing import Any, Optional, TYPE_CHECKING
# Verhindert Konsolenfenster bei Subprozessen in PyInstaller-EXE (Windows)
_SUBPROCESS_FLAGS = subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
if TYPE_CHECKING:
from saxon_pool import SaxonWorkerPool
from saxon_pool_s9api import SaxonWorkerPoolS9Api
from fop_pool import FopWorkerPool
from xsl_dependencies import XslDependencyGraph
logger = logging.getLogger(__name__)
# Globaler Saxon-Worker-Pool (wird von MainWindow initialisiert)
# Kann entweder JAXP oder s9api Variante sein
_saxon_worker_pool: Optional["SaxonWorkerPool | SaxonWorkerPoolS9Api"] = None
# Globaler FOP-Worker-Pool (wird von MainWindow initialisiert)
_fop_worker_pool: Optional["FopWorkerPool"] = None
def set_saxon_worker_pool(pool: Optional["SaxonWorkerPool | SaxonWorkerPoolS9Api"]):
"""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)")
def get_saxon_worker_pool() -> Optional["SaxonWorkerPool | SaxonWorkerPoolS9Api"]:
"""Gibt den aktuellen globalen Saxon-Worker-Pool zurück."""
return _saxon_worker_pool
def set_fop_worker_pool(pool: Optional["FopWorkerPool"]):
"""Setzt den globalen FOP-Worker-Pool."""
global _fop_worker_pool
_fop_worker_pool = pool
if pool:
logger.info(f"FOP-Worker-Pool aktiviert mit {pool.num_workers} Workern")
else:
logger.info("FOP-Worker-Pool deaktiviert (Fallback auf subprocess)")
def get_fop_worker_pool() -> Optional["FopWorkerPool"]:
"""Gibt den aktuellen globalen FOP-Worker-Pool zurück."""
return _fop_worker_pool
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,
dependency_graph: Optional["XslDependencyGraph"] = 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)
dependency_graph: Optionaler XSL-Abhängigkeitsgraph für Import/Include-Prüfung
"""
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
self.dependency_graph = dependency_graph
# 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
"""
try:
output_mtime = self.new_pdf.stat().st_mtime
except FileNotFoundError:
logger.debug(f"New-PDF existiert nicht: {self.new_pdf}")
return False
# 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
# Prüfe importierte/inkludierte XSL-Dateien (transitiv)
if self.dependency_graph and self.xsl_file.exists():
for dep_xsl in self.dependency_graph.get_dependencies(self.xsl_file):
if dep_xsl.exists() and dep_xsl.stat().st_mtime > output_mtime:
logger.debug(f"Importierte XSL-Datei ist neuer: {dep_xsl}")
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
creationflags=_SUBPROCESS_FLAGS,
)
# 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
logger.info(f"Starte Apache FOP PDF-Generierung: {self.xml_file.name}")
# Versuche zuerst den Worker-Pool zu nutzen (schneller!)
global _fop_worker_pool
if _fop_worker_pool:
try:
success, message = _fop_worker_pool.build_pdf(
input_fo=self.temp_fo,
output_pdf=self.new_pdf,
)
if success:
logger.info(f"FOP PDF-Generierung erfolgreich (Worker-Pool): {self.xml_file.name}")
# 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}")
# 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}")
return True, "Erfolgreich"
else:
logger.error(f"FOP PDF-Generierung fehlgeschlagen (Worker-Pool): {message}")
return False, message
except Exception as e:
logger.warning(f"FOP Worker-Pool-Fehler, Fallback auf subprocess: {e}")
# Fallback auf subprocess unten
# Fallback: Traditionelle subprocess-Methode (langsamer, aber robuster)
# Apache FOP Kommandozeile
fop_conf_exists = self.fop_conf.exists()
cmd_line = [
str(self.fop_cmd),
"-c",
str(self.fop_conf) if 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 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
creationflags=_SUBPROCESS_FLAGS,
)
# 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
creationflags=_SUBPROCESS_FLAGS,
)
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
creationflags=_SUBPROCESS_FLAGS,
)
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
+157
View File
@@ -0,0 +1,157 @@
"""
Info-Dialog für DocuMentor.
Zeigt Programmversion, Python-Version und alle Drittanbieter-Bibliotheken
mit installierten Versionen und Lizenzinformationen an.
"""
import logging
import sys
from importlib.metadata import PackageNotFoundError, version
from pathlib import Path
from PySide6.QtCore import Qt
from PySide6.QtGui import QFont
from PySide6.QtWidgets import (
QDialog,
QDialogButtonBox,
QFrame,
QHBoxLayout,
QHeaderView,
QLabel,
QTableWidget,
QTableWidgetItem,
QVBoxLayout,
)
from license_parser import parse_license_file
logger = logging.getLogger(__name__)
class AboutDialog(QDialog):
"""Info-Dialog mit Versionsinformationen und Drittanbieter-Lizenzen."""
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Über DocuMentor")
self.resize(950, 620)
self.setSizeGripEnabled(True)
self.setModal(True)
self._setup_ui()
self._populate_data()
def _get_app_version(self) -> str:
"""Ermittelt die Programmversion."""
try:
return version("DocuMentor")
except PackageNotFoundError:
# Fallback: pyproject.toml parsen (PyInstaller-Bundle oder Entwicklungsmodus)
try:
import tomllib
if hasattr(sys, "_MEIPASS"):
pyproject = Path(sys._MEIPASS) / "pyproject.toml" # type: ignore[attr-defined]
else:
pyproject = Path(__file__).parent.parent.parent / "pyproject.toml"
with open(pyproject, "rb") as f:
return tomllib.load(f)["project"]["version"]
except Exception:
return "unbekannt"
def _setup_ui(self):
"""Erstellt das Dialog-Layout programmatisch."""
layout = QVBoxLayout(self)
layout.setSpacing(8)
# App-Name
self.app_name_label = QLabel("DocuMentor")
font = QFont()
font.setPointSize(18)
font.setBold(True)
self.app_name_label.setFont(font)
self.app_name_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(self.app_name_label)
# Beschreibung
self.description_label = QLabel("Professionelle XSL-Transformations-Verwaltung und PDF-Generierung")
self.description_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.description_label.setWordWrap(True)
layout.addWidget(self.description_label)
# Version + Python-Version
self.version_label = QLabel()
self.version_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(self.version_label)
# Lizenz
self.license_label = QLabel("Lizenz: MIT")
self.license_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(self.license_label)
# Separator
separator = QFrame()
separator.setFrameShape(QFrame.Shape.HLine)
separator.setFrameShadow(QFrame.Shadow.Sunken)
layout.addWidget(separator)
# Überschrift für Tabelle
header_layout = QHBoxLayout()
table_header = QLabel("Drittanbieter-Bibliotheken:")
header_font = QFont()
header_font.setBold(True)
table_header.setFont(header_font)
header_layout.addWidget(table_header)
header_layout.addStretch()
layout.addLayout(header_layout)
# Dependency-Tabelle
self.table = QTableWidget()
self.table.setColumnCount(6)
self.table.setHorizontalHeaderLabels(["Name", "Lizenz", "Installiert", "Webseite", "Copyright", "Kategorie"])
self.table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
self.table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
self.table.setAlternatingRowColors(True)
self.table.setSortingEnabled(True)
self.table.verticalHeader().setVisible(False)
# Spaltenbreiten
header = self.table.horizontalHeader()
header.resizeSection(0, 170) # Name
header.resizeSection(1, 180) # Lizenz
header.resizeSection(2, 80) # Installiert
header.resizeSection(5, 200) # Kategorie
header.setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch) # Webseite
header.setSectionResizeMode(4, QHeaderView.ResizeMode.Stretch) # Copyright
layout.addWidget(self.table)
# Schließen-Button
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
button_box.rejected.connect(self.reject)
button_box.setCenterButtons(True)
layout.addWidget(button_box)
def _populate_data(self):
"""Befüllt den Dialog mit Versions- und Lizenzinformationen."""
# Version setzen
app_version = self._get_app_version()
python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
self.version_label.setText(f"Version: {app_version} | Python: {python_version}")
# Lizenzdaten laden und Tabelle befüllen
parsed = parse_license_file()
self.table.setRowCount(len(parsed.entries))
for row, entry in enumerate(parsed.entries):
self.table.setItem(row, 0, QTableWidgetItem(entry.name))
self.table.setItem(row, 1, QTableWidgetItem(entry.license))
self.table.setItem(row, 2, QTableWidgetItem(entry.installed_version or ""))
if entry.website:
website_label = QLabel(f'<a href="{entry.website}">{entry.website}</a>')
website_label.setOpenExternalLinks(True)
website_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
self.table.setCellWidget(row, 3, website_label)
else:
self.table.setItem(row, 3, QTableWidgetItem(""))
self.table.setItem(row, 4, QTableWidgetItem(entry.copyright))
self.table.setItem(row, 5, QTableWidgetItem(entry.category))
+296 -497
View File
File diff suppressed because it is too large Load Diff
+203 -1
View File
@@ -7,7 +7,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>833</width> <width>833</width>
<height>387</height> <height>526</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@@ -520,6 +520,208 @@
</item> </item>
</layout> </layout>
</widget> </widget>
<widget class="QWidget" name="tabPerformance">
<attribute name="title">
<string>Performance</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_9">
<item>
<widget class="QGroupBox" name="groupBoxWorker">
<property name="title">
<string>ThreadPoolExecutor Einstellungen</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_10">
<item>
<widget class="QLabel" name="labelWorkerCount">
<property name="text">
<string>Anzahl paralleler Worker für Transformationen:</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBoxWorkerCount">
<property name="toolTip">
<string>Anzahl der parallelen Worker-Threads für Transformationen (Standard: 8)</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>32</number>
</property>
<property name="value">
<number>8</number>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBoxSaxonPool">
<property name="title">
<string>SaxonWorkerPool Einstellungen</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_11">
<item>
<widget class="QCheckBox" name="checkBoxUseSaxonPool">
<property name="toolTip">
<string>Aktiviert persistente JVM-Prozesse für Saxon-Transformationen.
Vorteile: Bis zu 10x schneller durch Eliminierung von JVM-Startup-Overhead
Nachteile: Benötigt JDK (javac) - funktioniert nicht mit JRE allein
Deaktivieren Sie diese Option, wenn:
• Sie nur ein JRE (keine JDK) installiert haben
• Sie Probleme mit dem Worker-Pool haben
• Sie die Funktion testen möchten</string>
</property>
<property name="text">
<string>SaxonWorkerPool verwenden (empfohlen)</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayoutXsltVersion">
<item>
<widget class="QLabel" name="labelXsltVersion">
<property name="text">
<string>XSLT-Version:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBoxXsltVersion">
<property name="toolTip">
<string>Wählen Sie die XSLT-Version für Saxon-Transformationen:
XSLT 1.0 (JAXP): Verwendet die JAXP Transformer API
• Nur für XSLT 1.0 vollständig spezifiziert
• Kann bei XSLT 2.0/3.0 zu fehlerhaften Ausgaben führen
XSLT 2.0/3.0 (s9api): Verwendet die Saxon s9api
• Vollständige Unterstützung für XSLT 2.0 und 3.0
• Empfohlen für moderne XSLT-Stylesheets</string>
</property>
<item>
<property name="text">
<string>XSLT 1.0 (JAXP)</string>
</property>
</item>
<item>
<property name="text">
<string>XSLT 2.0/3.0 (s9api) - Empfohlen</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="horizontalSpacerXsltVersion">
<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>
<item>
<widget class="QLabel" name="labelSaxonPoolInfo">
<property name="text">
<string>&lt;i&gt;Hinweis: SaxonWorkerPool benötigt ein JDK (Java Development Kit).&lt;br&gt;Mit JRE allein werden Transformationen im Fallback-Modus ausgeführt.&lt;/i&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBoxFopPool">
<property name="title">
<string>FopWorkerPool Einstellungen</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_12">
<item>
<widget class="QCheckBox" name="checkBoxUseFopPool">
<property name="toolTip">
<string>Aktiviert persistente JVM-Prozesse für Apache FOP PDF-Generierung.
Vorteile: Bis zu 10x schneller durch Eliminierung von JVM-Startup-Overhead
Nachteile: Benötigt JDK (javac) - funktioniert nicht mit JRE allein
Deaktivieren Sie diese Option, wenn:
• Sie nur ein JRE (keine JDK) installiert haben
• Sie Probleme mit dem Worker-Pool haben
• Sie die Funktion testen möchten</string>
</property>
<property name="text">
<string>FopWorkerPool verwenden (empfohlen)</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="labelFopPoolInfo">
<property name="text">
<string>&lt;i&gt;Hinweis: FopWorkerPool benötigt ein JDK (Java Development Kit).&lt;br&gt;Mit JRE allein werden PDFs im Fallback-Modus generiert.&lt;/i&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label">
<property name="mouseTracking">
<bool>true</bool>
</property>
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:700; font-style:italic;&quot;&gt;Hinweis: Änderungen in diesem Dialog sind unter Umständen erst nach neu start der Anwendung wirksam.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacerPerformance">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget> </widget>
</item> </item>
<item> <item>
+498 -351
View File
@@ -1,351 +1,498 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
################################################################################ ################################################################################
## Form generated from reading UI file 'AppSettings.ui' ## Form generated from reading UI file 'AppSettings.ui'
## ##
## Created by: Qt User Interface Compiler version 6.9.1 ## Created by: Qt User Interface Compiler version 6.9.2
## ##
## WARNING! All changes made in this file will be lost when recompiling UI file! ## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################ ################################################################################
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
QMetaObject, QObject, QPoint, QRect, QMetaObject, QObject, QPoint, QRect,
QSize, QTime, QUrl, Qt) QSize, QTime, QUrl, Qt)
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon, QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter, QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform) QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QAbstractButton, QApplication, QDialog, QDialogButtonBox, from PySide6.QtWidgets import (QAbstractButton, QApplication, QCheckBox, QComboBox,
QFrame, QHBoxLayout, QHeaderView, QPushButton, QDialog, QDialogButtonBox, QFrame, QGroupBox,
QSizePolicy, QTabWidget, QTableWidget, QTableWidgetItem, QHBoxLayout, QHeaderView, QLabel, QPushButton,
QVBoxLayout, QWidget) QSizePolicy, QSpacerItem, QSpinBox, QTabWidget,
QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget)
class Ui_Dialog(object):
def setupUi(self, Dialog): class Ui_Dialog(object):
if not Dialog.objectName(): def setupUi(self, Dialog):
Dialog.setObjectName(u"Dialog") if not Dialog.objectName():
Dialog.resize(833, 387) Dialog.setObjectName(u"Dialog")
self.verticalLayout = QVBoxLayout(Dialog) Dialog.resize(833, 526)
self.verticalLayout.setObjectName(u"verticalLayout") self.verticalLayout = QVBoxLayout(Dialog)
self.tabSettings = QTabWidget(Dialog) self.verticalLayout.setObjectName(u"verticalLayout")
self.tabSettings.setObjectName(u"tabSettings") self.tabSettings = QTabWidget(Dialog)
self.tabSettings.setEnabled(True) self.tabSettings.setObjectName(u"tabSettings")
self.tabSettings.setElideMode(Qt.TextElideMode.ElideRight) self.tabSettings.setEnabled(True)
self.tabXsls = QWidget() self.tabSettings.setElideMode(Qt.TextElideMode.ElideRight)
self.tabXsls.setObjectName(u"tabXsls") self.tabXsls = QWidget()
self.verticalLayout_5 = QVBoxLayout(self.tabXsls) self.tabXsls.setObjectName(u"tabXsls")
self.verticalLayout_5.setObjectName(u"verticalLayout_5") self.verticalLayout_5 = QVBoxLayout(self.tabXsls)
self.tableXsls = QTableWidget(self.tabXsls) self.verticalLayout_5.setObjectName(u"verticalLayout_5")
if (self.tableXsls.columnCount() < 2): self.tableXsls = QTableWidget(self.tabXsls)
self.tableXsls.setColumnCount(2) if (self.tableXsls.columnCount() < 2):
self.tableXsls.setObjectName(u"tableXsls") self.tableXsls.setColumnCount(2)
self.tableXsls.setColumnCount(2) self.tableXsls.setObjectName(u"tableXsls")
self.tableXsls.setColumnCount(2)
self.verticalLayout_5.addWidget(self.tableXsls)
self.verticalLayout_5.addWidget(self.tableXsls)
self.frame_2 = QFrame(self.tabXsls)
self.frame_2.setObjectName(u"frame_2") self.frame_2 = QFrame(self.tabXsls)
sizePolicy = QSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Preferred) self.frame_2.setObjectName(u"frame_2")
sizePolicy.setHorizontalStretch(0) sizePolicy = QSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Preferred)
sizePolicy.setVerticalStretch(0) sizePolicy.setHorizontalStretch(0)
sizePolicy.setHeightForWidth(self.frame_2.sizePolicy().hasHeightForWidth()) sizePolicy.setVerticalStretch(0)
self.frame_2.setSizePolicy(sizePolicy) sizePolicy.setHeightForWidth(self.frame_2.sizePolicy().hasHeightForWidth())
self.frame_2.setFrameShape(QFrame.Shape.NoFrame) self.frame_2.setSizePolicy(sizePolicy)
self.frame_2.setFrameShadow(QFrame.Shadow.Raised) self.frame_2.setFrameShape(QFrame.Shape.NoFrame)
self.horizontalLayout_2 = QHBoxLayout(self.frame_2) self.frame_2.setFrameShadow(QFrame.Shadow.Raised)
self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") self.horizontalLayout_2 = QHBoxLayout(self.frame_2)
self.horizontalLayout_2.setContentsMargins(0, 1, 0, 0) self.horizontalLayout_2.setObjectName(u"horizontalLayout_2")
self.addXsl = QPushButton(self.frame_2) self.horizontalLayout_2.setContentsMargins(0, 1, 0, 0)
self.addXsl.setObjectName(u"addXsl") self.addXsl = QPushButton(self.frame_2)
icon = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.ListAdd)) self.addXsl.setObjectName(u"addXsl")
self.addXsl.setIcon(icon) icon = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.ListAdd))
self.addXsl.setIcon(icon)
self.horizontalLayout_2.addWidget(self.addXsl)
self.horizontalLayout_2.addWidget(self.addXsl)
self.removeXsl = QPushButton(self.frame_2)
self.removeXsl.setObjectName(u"removeXsl") self.removeXsl = QPushButton(self.frame_2)
self.removeXsl.setEnabled(False) self.removeXsl.setObjectName(u"removeXsl")
icon1 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.ListRemove)) self.removeXsl.setEnabled(False)
self.removeXsl.setIcon(icon1) icon1 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.ListRemove))
self.removeXsl.setIcon(icon1)
self.horizontalLayout_2.addWidget(self.removeXsl)
self.horizontalLayout_2.addWidget(self.removeXsl)
self.verticalLayout_5.addWidget(self.frame_2)
self.verticalLayout_5.addWidget(self.frame_2)
self.tabSettings.addTab(self.tabXsls, "")
self.tabJavaVm = QWidget() self.tabSettings.addTab(self.tabXsls, "")
self.tabJavaVm.setObjectName(u"tabJavaVm") self.tabJavaVm = QWidget()
self.verticalLayout_2 = QVBoxLayout(self.tabJavaVm) self.tabJavaVm.setObjectName(u"tabJavaVm")
self.verticalLayout_2.setObjectName(u"verticalLayout_2") self.verticalLayout_2 = QVBoxLayout(self.tabJavaVm)
self.tableJavaVms = QTableWidget(self.tabJavaVm) self.verticalLayout_2.setObjectName(u"verticalLayout_2")
if (self.tableJavaVms.columnCount() < 2): self.tableJavaVms = QTableWidget(self.tabJavaVm)
self.tableJavaVms.setColumnCount(2) if (self.tableJavaVms.columnCount() < 2):
self.tableJavaVms.setObjectName(u"tableJavaVms") self.tableJavaVms.setColumnCount(2)
self.tableJavaVms.setColumnCount(2) self.tableJavaVms.setObjectName(u"tableJavaVms")
self.tableJavaVms.setColumnCount(2)
self.verticalLayout_2.addWidget(self.tableJavaVms)
self.verticalLayout_2.addWidget(self.tableJavaVms)
self.frame_3 = QFrame(self.tabJavaVm)
self.frame_3.setObjectName(u"frame_3") self.frame_3 = QFrame(self.tabJavaVm)
sizePolicy.setHeightForWidth(self.frame_3.sizePolicy().hasHeightForWidth()) self.frame_3.setObjectName(u"frame_3")
self.frame_3.setSizePolicy(sizePolicy) sizePolicy.setHeightForWidth(self.frame_3.sizePolicy().hasHeightForWidth())
self.frame_3.setFrameShape(QFrame.Shape.NoFrame) self.frame_3.setSizePolicy(sizePolicy)
self.frame_3.setFrameShadow(QFrame.Shadow.Raised) self.frame_3.setFrameShape(QFrame.Shape.NoFrame)
self.horizontalLayout_3 = QHBoxLayout(self.frame_3) self.frame_3.setFrameShadow(QFrame.Shadow.Raised)
self.horizontalLayout_3.setObjectName(u"horizontalLayout_3") self.horizontalLayout_3 = QHBoxLayout(self.frame_3)
self.horizontalLayout_3.setContentsMargins(0, 0, 0, 0) self.horizontalLayout_3.setObjectName(u"horizontalLayout_3")
self.addJavaVm = QPushButton(self.frame_3) self.horizontalLayout_3.setContentsMargins(0, 0, 0, 0)
self.addJavaVm.setObjectName(u"addJavaVm") self.addJavaVm = QPushButton(self.frame_3)
self.addJavaVm.setIcon(icon) self.addJavaVm.setObjectName(u"addJavaVm")
self.addJavaVm.setIcon(icon)
self.horizontalLayout_3.addWidget(self.addJavaVm)
self.horizontalLayout_3.addWidget(self.addJavaVm)
self.removeJavaVm = QPushButton(self.frame_3)
self.removeJavaVm.setObjectName(u"removeJavaVm") self.removeJavaVm = QPushButton(self.frame_3)
self.removeJavaVm.setEnabled(False) self.removeJavaVm.setObjectName(u"removeJavaVm")
self.removeJavaVm.setIcon(icon1) self.removeJavaVm.setEnabled(False)
self.removeJavaVm.setIcon(icon1)
self.horizontalLayout_3.addWidget(self.removeJavaVm)
self.horizontalLayout_3.addWidget(self.removeJavaVm)
self.verticalLayout_2.addWidget(self.frame_3)
self.verticalLayout_2.addWidget(self.frame_3)
self.tabSettings.addTab(self.tabJavaVm, "")
self.tabSaxon = QWidget() self.tabSettings.addTab(self.tabJavaVm, "")
self.tabSaxon.setObjectName(u"tabSaxon") self.tabSaxon = QWidget()
self.verticalLayout_4 = QVBoxLayout(self.tabSaxon) self.tabSaxon.setObjectName(u"tabSaxon")
self.verticalLayout_4.setObjectName(u"verticalLayout_4") self.verticalLayout_4 = QVBoxLayout(self.tabSaxon)
self.tableSaxons = QTableWidget(self.tabSaxon) self.verticalLayout_4.setObjectName(u"verticalLayout_4")
if (self.tableSaxons.columnCount() < 3): self.tableSaxons = QTableWidget(self.tabSaxon)
self.tableSaxons.setColumnCount(3) if (self.tableSaxons.columnCount() < 3):
self.tableSaxons.setObjectName(u"tableSaxons") self.tableSaxons.setColumnCount(3)
self.tableSaxons.setColumnCount(3) self.tableSaxons.setObjectName(u"tableSaxons")
self.tableSaxons.setColumnCount(3)
self.verticalLayout_4.addWidget(self.tableSaxons)
self.verticalLayout_4.addWidget(self.tableSaxons)
self.frame_4 = QFrame(self.tabSaxon)
self.frame_4.setObjectName(u"frame_4") self.frame_4 = QFrame(self.tabSaxon)
sizePolicy.setHeightForWidth(self.frame_4.sizePolicy().hasHeightForWidth()) self.frame_4.setObjectName(u"frame_4")
self.frame_4.setSizePolicy(sizePolicy) sizePolicy.setHeightForWidth(self.frame_4.sizePolicy().hasHeightForWidth())
self.frame_4.setFrameShape(QFrame.Shape.NoFrame) self.frame_4.setSizePolicy(sizePolicy)
self.frame_4.setFrameShadow(QFrame.Shadow.Raised) self.frame_4.setFrameShape(QFrame.Shape.NoFrame)
self.horizontalLayout_4 = QHBoxLayout(self.frame_4) self.frame_4.setFrameShadow(QFrame.Shadow.Raised)
self.horizontalLayout_4.setObjectName(u"horizontalLayout_4") self.horizontalLayout_4 = QHBoxLayout(self.frame_4)
self.horizontalLayout_4.setContentsMargins(0, 0, 0, 0) self.horizontalLayout_4.setObjectName(u"horizontalLayout_4")
self.addSaxon = QPushButton(self.frame_4) self.horizontalLayout_4.setContentsMargins(0, 0, 0, 0)
self.addSaxon.setObjectName(u"addSaxon") self.addSaxon = QPushButton(self.frame_4)
self.addSaxon.setIcon(icon) self.addSaxon.setObjectName(u"addSaxon")
self.addSaxon.setIcon(icon)
self.horizontalLayout_4.addWidget(self.addSaxon)
self.horizontalLayout_4.addWidget(self.addSaxon)
self.removeSaxon = QPushButton(self.frame_4)
self.removeSaxon.setObjectName(u"removeSaxon") self.removeSaxon = QPushButton(self.frame_4)
self.removeSaxon.setEnabled(False) self.removeSaxon.setObjectName(u"removeSaxon")
self.removeSaxon.setIcon(icon1) self.removeSaxon.setEnabled(False)
self.removeSaxon.setIcon(icon1)
self.horizontalLayout_4.addWidget(self.removeSaxon)
self.horizontalLayout_4.addWidget(self.removeSaxon)
self.verticalLayout_4.addWidget(self.frame_4)
self.verticalLayout_4.addWidget(self.frame_4)
self.tabSettings.addTab(self.tabSaxon, "")
self.tabApacheFop = QWidget() self.tabSettings.addTab(self.tabSaxon, "")
self.tabApacheFop.setObjectName(u"tabApacheFop") self.tabApacheFop = QWidget()
self.verticalLayout_3 = QVBoxLayout(self.tabApacheFop) self.tabApacheFop.setObjectName(u"tabApacheFop")
self.verticalLayout_3.setObjectName(u"verticalLayout_3") self.verticalLayout_3 = QVBoxLayout(self.tabApacheFop)
self.tableApacheFops = QTableWidget(self.tabApacheFop) self.verticalLayout_3.setObjectName(u"verticalLayout_3")
if (self.tableApacheFops.columnCount() < 3): self.tableApacheFops = QTableWidget(self.tabApacheFop)
self.tableApacheFops.setColumnCount(3) if (self.tableApacheFops.columnCount() < 3):
self.tableApacheFops.setObjectName(u"tableApacheFops") self.tableApacheFops.setColumnCount(3)
self.tableApacheFops.setColumnCount(3) self.tableApacheFops.setObjectName(u"tableApacheFops")
self.tableApacheFops.setColumnCount(3)
self.verticalLayout_3.addWidget(self.tableApacheFops)
self.verticalLayout_3.addWidget(self.tableApacheFops)
self.frame = QFrame(self.tabApacheFop)
self.frame.setObjectName(u"frame") self.frame = QFrame(self.tabApacheFop)
sizePolicy.setHeightForWidth(self.frame.sizePolicy().hasHeightForWidth()) self.frame.setObjectName(u"frame")
self.frame.setSizePolicy(sizePolicy) sizePolicy.setHeightForWidth(self.frame.sizePolicy().hasHeightForWidth())
self.frame.setFrameShape(QFrame.Shape.NoFrame) self.frame.setSizePolicy(sizePolicy)
self.frame.setFrameShadow(QFrame.Shadow.Raised) self.frame.setFrameShape(QFrame.Shape.NoFrame)
self.horizontalLayout = QHBoxLayout(self.frame) self.frame.setFrameShadow(QFrame.Shadow.Raised)
self.horizontalLayout.setObjectName(u"horizontalLayout") self.horizontalLayout = QHBoxLayout(self.frame)
self.horizontalLayout.setContentsMargins(0, 0, 0, 0) self.horizontalLayout.setObjectName(u"horizontalLayout")
self.addApacheFop = QPushButton(self.frame) self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
self.addApacheFop.setObjectName(u"addApacheFop") self.addApacheFop = QPushButton(self.frame)
self.addApacheFop.setIcon(icon) self.addApacheFop.setObjectName(u"addApacheFop")
self.addApacheFop.setIcon(icon)
self.horizontalLayout.addWidget(self.addApacheFop)
self.horizontalLayout.addWidget(self.addApacheFop)
self.removeApacheFop = QPushButton(self.frame)
self.removeApacheFop.setObjectName(u"removeApacheFop") self.removeApacheFop = QPushButton(self.frame)
self.removeApacheFop.setEnabled(False) self.removeApacheFop.setObjectName(u"removeApacheFop")
self.removeApacheFop.setIcon(icon1) self.removeApacheFop.setEnabled(False)
self.removeApacheFop.setIcon(icon1)
self.horizontalLayout.addWidget(self.removeApacheFop)
self.horizontalLayout.addWidget(self.removeApacheFop)
self.verticalLayout_3.addWidget(self.frame)
self.verticalLayout_3.addWidget(self.frame)
self.tabSettings.addTab(self.tabApacheFop, "")
self.tabDiffPdf = QWidget() self.tabSettings.addTab(self.tabApacheFop, "")
self.tabDiffPdf.setObjectName(u"tabDiffPdf") self.tabDiffPdf = QWidget()
self.verticalLayout_6 = QVBoxLayout(self.tabDiffPdf) self.tabDiffPdf.setObjectName(u"tabDiffPdf")
self.verticalLayout_6.setObjectName(u"verticalLayout_6") self.verticalLayout_6 = QVBoxLayout(self.tabDiffPdf)
self.tableDiffPdfs = QTableWidget(self.tabDiffPdf) self.verticalLayout_6.setObjectName(u"verticalLayout_6")
if (self.tableDiffPdfs.columnCount() < 4): self.tableDiffPdfs = QTableWidget(self.tabDiffPdf)
self.tableDiffPdfs.setColumnCount(4) if (self.tableDiffPdfs.columnCount() < 4):
self.tableDiffPdfs.setObjectName(u"tableDiffPdfs") self.tableDiffPdfs.setColumnCount(4)
self.tableDiffPdfs.setColumnCount(4) self.tableDiffPdfs.setObjectName(u"tableDiffPdfs")
self.tableDiffPdfs.setColumnCount(4)
self.verticalLayout_6.addWidget(self.tableDiffPdfs)
self.verticalLayout_6.addWidget(self.tableDiffPdfs)
self.frame_5 = QFrame(self.tabDiffPdf)
self.frame_5.setObjectName(u"frame_5") self.frame_5 = QFrame(self.tabDiffPdf)
sizePolicy.setHeightForWidth(self.frame_5.sizePolicy().hasHeightForWidth()) self.frame_5.setObjectName(u"frame_5")
self.frame_5.setSizePolicy(sizePolicy) sizePolicy.setHeightForWidth(self.frame_5.sizePolicy().hasHeightForWidth())
self.frame_5.setFrameShape(QFrame.Shape.NoFrame) self.frame_5.setSizePolicy(sizePolicy)
self.frame_5.setFrameShadow(QFrame.Shadow.Raised) self.frame_5.setFrameShape(QFrame.Shape.NoFrame)
self.horizontalLayout_5 = QHBoxLayout(self.frame_5) self.frame_5.setFrameShadow(QFrame.Shadow.Raised)
self.horizontalLayout_5.setObjectName(u"horizontalLayout_5") self.horizontalLayout_5 = QHBoxLayout(self.frame_5)
self.horizontalLayout_5.setContentsMargins(0, 0, 0, 0) self.horizontalLayout_5.setObjectName(u"horizontalLayout_5")
self.addDiffPdf = QPushButton(self.frame_5) self.horizontalLayout_5.setContentsMargins(0, 0, 0, 0)
self.addDiffPdf.setObjectName(u"addDiffPdf") self.addDiffPdf = QPushButton(self.frame_5)
self.addDiffPdf.setIcon(icon) self.addDiffPdf.setObjectName(u"addDiffPdf")
self.addDiffPdf.setIcon(icon)
self.horizontalLayout_5.addWidget(self.addDiffPdf)
self.horizontalLayout_5.addWidget(self.addDiffPdf)
self.removeDiffPdf = QPushButton(self.frame_5)
self.removeDiffPdf.setObjectName(u"removeDiffPdf") self.removeDiffPdf = QPushButton(self.frame_5)
self.removeDiffPdf.setEnabled(False) self.removeDiffPdf.setObjectName(u"removeDiffPdf")
self.removeDiffPdf.setIcon(icon1) self.removeDiffPdf.setEnabled(False)
self.removeDiffPdf.setIcon(icon1)
self.horizontalLayout_5.addWidget(self.removeDiffPdf)
self.horizontalLayout_5.addWidget(self.removeDiffPdf)
self.verticalLayout_6.addWidget(self.frame_5)
self.verticalLayout_6.addWidget(self.frame_5)
self.tabSettings.addTab(self.tabDiffPdf, "")
self.tabPostgreSql = QWidget() self.tabSettings.addTab(self.tabDiffPdf, "")
self.tabPostgreSql.setObjectName(u"tabPostgreSql") self.tabPostgreSql = QWidget()
self.verticalLayout_8 = QVBoxLayout(self.tabPostgreSql) self.tabPostgreSql.setObjectName(u"tabPostgreSql")
self.verticalLayout_8.setObjectName(u"verticalLayout_8") self.verticalLayout_8 = QVBoxLayout(self.tabPostgreSql)
self.tablePostgreSqlDbs = QTableWidget(self.tabPostgreSql) self.verticalLayout_8.setObjectName(u"verticalLayout_8")
if (self.tablePostgreSqlDbs.columnCount() < 5): self.tablePostgreSqlDbs = QTableWidget(self.tabPostgreSql)
self.tablePostgreSqlDbs.setColumnCount(5) if (self.tablePostgreSqlDbs.columnCount() < 5):
self.tablePostgreSqlDbs.setObjectName(u"tablePostgreSqlDbs") self.tablePostgreSqlDbs.setColumnCount(5)
self.tablePostgreSqlDbs.setColumnCount(5) self.tablePostgreSqlDbs.setObjectName(u"tablePostgreSqlDbs")
self.tablePostgreSqlDbs.setColumnCount(5)
self.verticalLayout_8.addWidget(self.tablePostgreSqlDbs)
self.verticalLayout_8.addWidget(self.tablePostgreSqlDbs)
self.frame_7 = QFrame(self.tabPostgreSql)
self.frame_7.setObjectName(u"frame_7") self.frame_7 = QFrame(self.tabPostgreSql)
sizePolicy.setHeightForWidth(self.frame_7.sizePolicy().hasHeightForWidth()) self.frame_7.setObjectName(u"frame_7")
self.frame_7.setSizePolicy(sizePolicy) sizePolicy.setHeightForWidth(self.frame_7.sizePolicy().hasHeightForWidth())
self.frame_7.setFrameShape(QFrame.Shape.NoFrame) self.frame_7.setSizePolicy(sizePolicy)
self.frame_7.setFrameShadow(QFrame.Shadow.Raised) self.frame_7.setFrameShape(QFrame.Shape.NoFrame)
self.horizontalLayout_7 = QHBoxLayout(self.frame_7) self.frame_7.setFrameShadow(QFrame.Shadow.Raised)
self.horizontalLayout_7.setObjectName(u"horizontalLayout_7") self.horizontalLayout_7 = QHBoxLayout(self.frame_7)
self.horizontalLayout_7.setContentsMargins(0, 0, 0, 0) self.horizontalLayout_7.setObjectName(u"horizontalLayout_7")
self.addPostgreSql = QPushButton(self.frame_7) self.horizontalLayout_7.setContentsMargins(0, 0, 0, 0)
self.addPostgreSql.setObjectName(u"addPostgreSql") self.addPostgreSql = QPushButton(self.frame_7)
self.addPostgreSql.setIcon(icon) self.addPostgreSql.setObjectName(u"addPostgreSql")
self.addPostgreSql.setIcon(icon)
self.horizontalLayout_7.addWidget(self.addPostgreSql)
self.horizontalLayout_7.addWidget(self.addPostgreSql)
self.removePostgreSql = QPushButton(self.frame_7)
self.removePostgreSql.setObjectName(u"removePostgreSql") self.removePostgreSql = QPushButton(self.frame_7)
self.removePostgreSql.setEnabled(False) self.removePostgreSql.setObjectName(u"removePostgreSql")
self.removePostgreSql.setIcon(icon1) self.removePostgreSql.setEnabled(False)
self.removePostgreSql.setIcon(icon1)
self.horizontalLayout_7.addWidget(self.removePostgreSql)
self.horizontalLayout_7.addWidget(self.removePostgreSql)
self.verticalLayout_8.addWidget(self.frame_7)
self.verticalLayout_8.addWidget(self.frame_7)
self.tabSettings.addTab(self.tabPostgreSql, "")
self.tabPdfProject = QWidget() self.tabSettings.addTab(self.tabPostgreSql, "")
self.tabPdfProject.setObjectName(u"tabPdfProject") self.tabPdfProject = QWidget()
self.verticalLayout_7 = QVBoxLayout(self.tabPdfProject) self.tabPdfProject.setObjectName(u"tabPdfProject")
self.verticalLayout_7.setObjectName(u"verticalLayout_7") self.verticalLayout_7 = QVBoxLayout(self.tabPdfProject)
self.tablePdfProjects = QTableWidget(self.tabPdfProject) self.verticalLayout_7.setObjectName(u"verticalLayout_7")
if (self.tablePdfProjects.columnCount() < 7): self.tablePdfProjects = QTableWidget(self.tabPdfProject)
self.tablePdfProjects.setColumnCount(7) if (self.tablePdfProjects.columnCount() < 7):
self.tablePdfProjects.setObjectName(u"tablePdfProjects") self.tablePdfProjects.setColumnCount(7)
self.tablePdfProjects.setColumnCount(7) self.tablePdfProjects.setObjectName(u"tablePdfProjects")
self.tablePdfProjects.setColumnCount(7)
self.verticalLayout_7.addWidget(self.tablePdfProjects)
self.verticalLayout_7.addWidget(self.tablePdfProjects)
self.frame_6 = QFrame(self.tabPdfProject)
self.frame_6.setObjectName(u"frame_6") self.frame_6 = QFrame(self.tabPdfProject)
sizePolicy.setHeightForWidth(self.frame_6.sizePolicy().hasHeightForWidth()) self.frame_6.setObjectName(u"frame_6")
self.frame_6.setSizePolicy(sizePolicy) sizePolicy.setHeightForWidth(self.frame_6.sizePolicy().hasHeightForWidth())
self.frame_6.setFrameShape(QFrame.Shape.NoFrame) self.frame_6.setSizePolicy(sizePolicy)
self.frame_6.setFrameShadow(QFrame.Shadow.Raised) self.frame_6.setFrameShape(QFrame.Shape.NoFrame)
self.horizontalLayout_6 = QHBoxLayout(self.frame_6) self.frame_6.setFrameShadow(QFrame.Shadow.Raised)
self.horizontalLayout_6.setObjectName(u"horizontalLayout_6") self.horizontalLayout_6 = QHBoxLayout(self.frame_6)
self.horizontalLayout_6.setContentsMargins(0, 0, 0, 0) self.horizontalLayout_6.setObjectName(u"horizontalLayout_6")
self.addProject = QPushButton(self.frame_6) self.horizontalLayout_6.setContentsMargins(0, 0, 0, 0)
self.addProject.setObjectName(u"addProject") self.addProject = QPushButton(self.frame_6)
self.addProject.setIcon(icon) self.addProject.setObjectName(u"addProject")
self.addProject.setIcon(icon)
self.horizontalLayout_6.addWidget(self.addProject)
self.horizontalLayout_6.addWidget(self.addProject)
self.removeProject = QPushButton(self.frame_6)
self.removeProject.setObjectName(u"removeProject") self.removeProject = QPushButton(self.frame_6)
self.removeProject.setEnabled(False) self.removeProject.setObjectName(u"removeProject")
self.removeProject.setIcon(icon1) self.removeProject.setEnabled(False)
self.removeProject.setIcon(icon1)
self.horizontalLayout_6.addWidget(self.removeProject)
self.horizontalLayout_6.addWidget(self.removeProject)
self.verticalLayout_7.addWidget(self.frame_6)
self.verticalLayout_7.addWidget(self.frame_6)
self.tabSettings.addTab(self.tabPdfProject, "")
self.tabSettings.addTab(self.tabPdfProject, "")
self.verticalLayout.addWidget(self.tabSettings) self.tabPerformance = QWidget()
self.tabPerformance.setObjectName(u"tabPerformance")
self.buttonBox = QDialogButtonBox(Dialog) self.verticalLayout_9 = QVBoxLayout(self.tabPerformance)
self.buttonBox.setObjectName(u"buttonBox") self.verticalLayout_9.setObjectName(u"verticalLayout_9")
self.buttonBox.setOrientation(Qt.Orientation.Horizontal) self.groupBoxWorker = QGroupBox(self.tabPerformance)
self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.Ok) self.groupBoxWorker.setObjectName(u"groupBoxWorker")
self.buttonBox.setCenterButtons(True) self.verticalLayout_10 = QVBoxLayout(self.groupBoxWorker)
self.verticalLayout_10.setObjectName(u"verticalLayout_10")
self.verticalLayout.addWidget(self.buttonBox) self.labelWorkerCount = QLabel(self.groupBoxWorker)
self.labelWorkerCount.setObjectName(u"labelWorkerCount")
self.retranslateUi(Dialog) self.verticalLayout_10.addWidget(self.labelWorkerCount)
self.buttonBox.accepted.connect(Dialog.accept)
self.buttonBox.rejected.connect(Dialog.reject) self.spinBoxWorkerCount = QSpinBox(self.groupBoxWorker)
self.spinBoxWorkerCount.setObjectName(u"spinBoxWorkerCount")
self.tabSettings.setCurrentIndex(0) self.spinBoxWorkerCount.setMinimum(1)
self.spinBoxWorkerCount.setMaximum(32)
self.spinBoxWorkerCount.setValue(8)
QMetaObject.connectSlotsByName(Dialog)
# setupUi self.verticalLayout_10.addWidget(self.spinBoxWorkerCount)
def retranslateUi(self, Dialog):
Dialog.setWindowTitle(QCoreApplication.translate("Dialog", u"Programm Einstellungen", None)) self.verticalLayout_9.addWidget(self.groupBoxWorker)
self.addXsl.setText(QCoreApplication.translate("Dialog", u"Hinzuf\u00fcgen", None))
self.removeXsl.setText(QCoreApplication.translate("Dialog", u"Entfernen", None)) self.groupBoxSaxonPool = QGroupBox(self.tabPerformance)
self.tabSettings.setTabText(self.tabSettings.indexOf(self.tabXsls), QCoreApplication.translate("Dialog", u"XSL-Ordner", None)) self.groupBoxSaxonPool.setObjectName(u"groupBoxSaxonPool")
self.addJavaVm.setText(QCoreApplication.translate("Dialog", u"Hinzuf\u00fcgen", None)) self.verticalLayout_11 = QVBoxLayout(self.groupBoxSaxonPool)
self.removeJavaVm.setText(QCoreApplication.translate("Dialog", u"Entfernen", None)) self.verticalLayout_11.setObjectName(u"verticalLayout_11")
self.tabSettings.setTabText(self.tabSettings.indexOf(self.tabJavaVm), QCoreApplication.translate("Dialog", u"Java VM", None)) self.checkBoxUseSaxonPool = QCheckBox(self.groupBoxSaxonPool)
self.addSaxon.setText(QCoreApplication.translate("Dialog", u"Hinzuf\u00fcgen", None)) self.checkBoxUseSaxonPool.setObjectName(u"checkBoxUseSaxonPool")
self.removeSaxon.setText(QCoreApplication.translate("Dialog", u"Entfernen", None))
self.tabSettings.setTabText(self.tabSettings.indexOf(self.tabSaxon), QCoreApplication.translate("Dialog", u"Saxon", None)) self.verticalLayout_11.addWidget(self.checkBoxUseSaxonPool)
self.addApacheFop.setText(QCoreApplication.translate("Dialog", u"Hinzuf\u00fcgen", None))
self.removeApacheFop.setText(QCoreApplication.translate("Dialog", u"Entfernen", None)) self.horizontalLayoutXsltVersion = QHBoxLayout()
self.tabSettings.setTabText(self.tabSettings.indexOf(self.tabApacheFop), QCoreApplication.translate("Dialog", u"Apache FOP", None)) self.horizontalLayoutXsltVersion.setObjectName(u"horizontalLayoutXsltVersion")
self.addDiffPdf.setText(QCoreApplication.translate("Dialog", u"Hinzuf\u00fcgen", None)) self.labelXsltVersion = QLabel(self.groupBoxSaxonPool)
self.removeDiffPdf.setText(QCoreApplication.translate("Dialog", u"Entfernen", None)) self.labelXsltVersion.setObjectName(u"labelXsltVersion")
self.tabSettings.setTabText(self.tabSettings.indexOf(self.tabDiffPdf), QCoreApplication.translate("Dialog", u"Diff-PDF", None))
self.addPostgreSql.setText(QCoreApplication.translate("Dialog", u"Hinzuf\u00fcgen", None)) self.horizontalLayoutXsltVersion.addWidget(self.labelXsltVersion)
self.removePostgreSql.setText(QCoreApplication.translate("Dialog", u"Entfernen", None))
self.tabSettings.setTabText(self.tabSettings.indexOf(self.tabPostgreSql), QCoreApplication.translate("Dialog", u"PostgreSQL", None)) self.comboBoxXsltVersion = QComboBox(self.groupBoxSaxonPool)
self.addProject.setText(QCoreApplication.translate("Dialog", u"Hinzuf\u00fcgen", None)) self.comboBoxXsltVersion.addItem("")
self.removeProject.setText(QCoreApplication.translate("Dialog", u"Entfernen", None)) self.comboBoxXsltVersion.addItem("")
self.tabSettings.setTabText(self.tabSettings.indexOf(self.tabPdfProject), QCoreApplication.translate("Dialog", u"PDF-Projekte", None)) self.comboBoxXsltVersion.setObjectName(u"comboBoxXsltVersion")
# retranslateUi
self.horizontalLayoutXsltVersion.addWidget(self.comboBoxXsltVersion)
self.horizontalSpacerXsltVersion = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
self.horizontalLayoutXsltVersion.addItem(self.horizontalSpacerXsltVersion)
self.verticalLayout_11.addLayout(self.horizontalLayoutXsltVersion)
self.labelSaxonPoolInfo = QLabel(self.groupBoxSaxonPool)
self.labelSaxonPoolInfo.setObjectName(u"labelSaxonPoolInfo")
self.labelSaxonPoolInfo.setWordWrap(True)
self.verticalLayout_11.addWidget(self.labelSaxonPoolInfo)
self.verticalLayout_9.addWidget(self.groupBoxSaxonPool)
self.groupBoxFopPool = QGroupBox(self.tabPerformance)
self.groupBoxFopPool.setObjectName(u"groupBoxFopPool")
self.verticalLayout_12 = QVBoxLayout(self.groupBoxFopPool)
self.verticalLayout_12.setObjectName(u"verticalLayout_12")
self.checkBoxUseFopPool = QCheckBox(self.groupBoxFopPool)
self.checkBoxUseFopPool.setObjectName(u"checkBoxUseFopPool")
self.verticalLayout_12.addWidget(self.checkBoxUseFopPool)
self.labelFopPoolInfo = QLabel(self.groupBoxFopPool)
self.labelFopPoolInfo.setObjectName(u"labelFopPoolInfo")
self.labelFopPoolInfo.setWordWrap(True)
self.verticalLayout_12.addWidget(self.labelFopPoolInfo)
self.verticalLayout_9.addWidget(self.groupBoxFopPool)
self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
self.verticalLayout_9.addItem(self.verticalSpacer)
self.label = QLabel(self.tabPerformance)
self.label.setObjectName(u"label")
self.label.setMouseTracking(True)
self.label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.label.setWordWrap(True)
self.verticalLayout_9.addWidget(self.label)
self.verticalSpacerPerformance = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
self.verticalLayout_9.addItem(self.verticalSpacerPerformance)
self.tabSettings.addTab(self.tabPerformance, "")
self.verticalLayout.addWidget(self.tabSettings)
self.buttonBox = QDialogButtonBox(Dialog)
self.buttonBox.setObjectName(u"buttonBox")
self.buttonBox.setOrientation(Qt.Orientation.Horizontal)
self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.Ok)
self.buttonBox.setCenterButtons(True)
self.verticalLayout.addWidget(self.buttonBox)
self.retranslateUi(Dialog)
self.buttonBox.accepted.connect(Dialog.accept)
self.buttonBox.rejected.connect(Dialog.reject)
self.tabSettings.setCurrentIndex(0)
QMetaObject.connectSlotsByName(Dialog)
# setupUi
def retranslateUi(self, Dialog):
Dialog.setWindowTitle(QCoreApplication.translate("Dialog", u"Programm Einstellungen", None))
self.addXsl.setText(QCoreApplication.translate("Dialog", u"Hinzuf\u00fcgen", None))
self.removeXsl.setText(QCoreApplication.translate("Dialog", u"Entfernen", None))
self.tabSettings.setTabText(self.tabSettings.indexOf(self.tabXsls), QCoreApplication.translate("Dialog", u"XSL-Ordner", None))
self.addJavaVm.setText(QCoreApplication.translate("Dialog", u"Hinzuf\u00fcgen", None))
self.removeJavaVm.setText(QCoreApplication.translate("Dialog", u"Entfernen", None))
self.tabSettings.setTabText(self.tabSettings.indexOf(self.tabJavaVm), QCoreApplication.translate("Dialog", u"Java VM", None))
self.addSaxon.setText(QCoreApplication.translate("Dialog", u"Hinzuf\u00fcgen", None))
self.removeSaxon.setText(QCoreApplication.translate("Dialog", u"Entfernen", None))
self.tabSettings.setTabText(self.tabSettings.indexOf(self.tabSaxon), QCoreApplication.translate("Dialog", u"Saxon", None))
self.addApacheFop.setText(QCoreApplication.translate("Dialog", u"Hinzuf\u00fcgen", None))
self.removeApacheFop.setText(QCoreApplication.translate("Dialog", u"Entfernen", None))
self.tabSettings.setTabText(self.tabSettings.indexOf(self.tabApacheFop), QCoreApplication.translate("Dialog", u"Apache FOP", None))
self.addDiffPdf.setText(QCoreApplication.translate("Dialog", u"Hinzuf\u00fcgen", None))
self.removeDiffPdf.setText(QCoreApplication.translate("Dialog", u"Entfernen", None))
self.tabSettings.setTabText(self.tabSettings.indexOf(self.tabDiffPdf), QCoreApplication.translate("Dialog", u"Diff-PDF", None))
self.addPostgreSql.setText(QCoreApplication.translate("Dialog", u"Hinzuf\u00fcgen", None))
self.removePostgreSql.setText(QCoreApplication.translate("Dialog", u"Entfernen", None))
self.tabSettings.setTabText(self.tabSettings.indexOf(self.tabPostgreSql), QCoreApplication.translate("Dialog", u"PostgreSQL", None))
self.addProject.setText(QCoreApplication.translate("Dialog", u"Hinzuf\u00fcgen", None))
self.removeProject.setText(QCoreApplication.translate("Dialog", u"Entfernen", None))
self.tabSettings.setTabText(self.tabSettings.indexOf(self.tabPdfProject), QCoreApplication.translate("Dialog", u"PDF-Projekte", None))
self.groupBoxWorker.setTitle(QCoreApplication.translate("Dialog", u"ThreadPoolExecutor Einstellungen", None))
self.labelWorkerCount.setText(QCoreApplication.translate("Dialog", u"Anzahl paralleler Worker f\u00fcr Transformationen:", None))
#if QT_CONFIG(tooltip)
self.spinBoxWorkerCount.setToolTip(QCoreApplication.translate("Dialog", u"Anzahl der parallelen Worker-Threads f\u00fcr Transformationen (Standard: 8)", None))
#endif // QT_CONFIG(tooltip)
self.groupBoxSaxonPool.setTitle(QCoreApplication.translate("Dialog", u"SaxonWorkerPool Einstellungen", None))
#if QT_CONFIG(tooltip)
self.checkBoxUseSaxonPool.setToolTip(QCoreApplication.translate("Dialog", u"Aktiviert persistente JVM-Prozesse f\u00fcr Saxon-Transformationen.\n"
"Vorteile: Bis zu 10x schneller durch Eliminierung von JVM-Startup-Overhead\n"
"Nachteile: Ben\u00f6tigt JDK (javac) - funktioniert nicht mit JRE allein\n"
"\n"
"Deaktivieren Sie diese Option, wenn:\n"
"\u2022 Sie nur ein JRE (keine JDK) installiert haben\n"
"\u2022 Sie Probleme mit dem Worker-Pool haben\n"
"\u2022 Sie die Funktion testen m\u00f6chten", None))
#endif // QT_CONFIG(tooltip)
self.checkBoxUseSaxonPool.setText(QCoreApplication.translate("Dialog", u"SaxonWorkerPool verwenden (empfohlen)", None))
self.labelXsltVersion.setText(QCoreApplication.translate("Dialog", u"XSLT-Version:", None))
self.comboBoxXsltVersion.setItemText(0, QCoreApplication.translate("Dialog", u"XSLT 1.0 (JAXP)", None))
self.comboBoxXsltVersion.setItemText(1, QCoreApplication.translate("Dialog", u"XSLT 2.0/3.0 (s9api) - Empfohlen", None))
#if QT_CONFIG(tooltip)
self.comboBoxXsltVersion.setToolTip(QCoreApplication.translate("Dialog", u"W\u00e4hlen Sie die XSLT-Version f\u00fcr Saxon-Transformationen:\n"
"\n"
"XSLT 1.0 (JAXP): Verwendet die JAXP Transformer API\n"
"\u2022 Nur f\u00fcr XSLT 1.0 vollst\u00e4ndig spezifiziert\n"
"\u2022 Kann bei XSLT 2.0/3.0 zu fehlerhaften Ausgaben f\u00fchren\n"
"\n"
"XSLT 2.0/3.0 (s9api): Verwendet die Saxon s9api\n"
"\u2022 Vollst\u00e4ndige Unterst\u00fctzung f\u00fcr XSLT 2.0 und 3.0\n"
"\u2022 Empfohlen f\u00fcr moderne XSLT-Stylesheets", None))
#endif // QT_CONFIG(tooltip)
self.labelSaxonPoolInfo.setText(QCoreApplication.translate("Dialog", u"<i>Hinweis: SaxonWorkerPool ben\u00f6tigt ein JDK (Java Development Kit).<br>Mit JRE allein werden Transformationen im Fallback-Modus ausgef\u00fchrt.</i>", None))
self.groupBoxFopPool.setTitle(QCoreApplication.translate("Dialog", u"FopWorkerPool Einstellungen", None))
#if QT_CONFIG(tooltip)
self.checkBoxUseFopPool.setToolTip(QCoreApplication.translate("Dialog", u"Aktiviert persistente JVM-Prozesse f\u00fcr Apache FOP PDF-Generierung.\n"
"Vorteile: Bis zu 10x schneller durch Eliminierung von JVM-Startup-Overhead\n"
"Nachteile: Ben\u00f6tigt JDK (javac) - funktioniert nicht mit JRE allein\n"
"\n"
"Deaktivieren Sie diese Option, wenn:\n"
"\u2022 Sie nur ein JRE (keine JDK) installiert haben\n"
"\u2022 Sie Probleme mit dem Worker-Pool haben\n"
"\u2022 Sie die Funktion testen m\u00f6chten", None))
#endif // QT_CONFIG(tooltip)
self.checkBoxUseFopPool.setText(QCoreApplication.translate("Dialog", u"FopWorkerPool verwenden (empfohlen)", None))
self.labelFopPoolInfo.setText(QCoreApplication.translate("Dialog", u"<i>Hinweis: FopWorkerPool ben\u00f6tigt ein JDK (Java Development Kit).<br>Mit JRE allein werden PDFs im Fallback-Modus generiert.</i>", None))
self.label.setText(QCoreApplication.translate("Dialog", u"<html><head/><body><p><span style=\" font-weight:700; font-style:italic;\">Hinweis: \u00c4nderungen in diesem Dialog sind unter Umst\u00e4nden erst nach neu start der Anwendung wirksam.</span></p></body></html>", None))
self.tabSettings.setTabText(self.tabSettings.indexOf(self.tabPerformance), QCoreApplication.translate("Dialog", u"Performance", None))
# retranslateUi
+150 -106
View File
@@ -7,7 +7,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>1263</width> <width>1263</width>
<height>774</height> <height>779</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@@ -32,6 +32,12 @@
<property name="orientation"> <property name="orientation">
<enum>Qt::Orientation::Horizontal</enum> <enum>Qt::Orientation::Horizontal</enum>
</property> </property>
<property name="opaqueResize">
<bool>false</bool>
</property>
<property name="childrenCollapsible">
<bool>false</bool>
</property>
<widget class="QFrame" name="frame"> <widget class="QFrame" name="frame">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred"> <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
@@ -55,6 +61,26 @@
<property name="bottomMargin"> <property name="bottomMargin">
<number>0</number> <number>0</number>
</property> </property>
<item>
<widget class="QLabel" name="projectPath">
<property name="styleSheet">
<string notr="true">QLabel { padding: 5px; font-weight: bold; }</string>
</property>
<property name="text">
<string>Kein Projekt geladen</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="searchEdit">
<property name="placeholderText">
<string>Knoten oder XSL-Datei filtern...</string>
</property>
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
<item> <item>
<widget class="QTreeWidget" name="treeWidget"> <widget class="QTreeWidget" name="treeWidget">
<property name="sizePolicy"> <property name="sizePolicy">
@@ -63,6 +89,16 @@
<verstretch>0</verstretch> <verstretch>0</verstretch>
</sizepolicy> </sizepolicy>
</property> </property>
<property name="styleSheet">
<string notr="true">QTreeWidget::item {
padding: 4px 4px;
}
QTreeWidget::item:selected {
background-color: palette(highlight);
color: palette(highlighted-text);
}</string>
</property>
<property name="columnCount"> <property name="columnCount">
<number>2</number> <number>2</number>
</property> </property>
@@ -84,76 +120,6 @@
</column> </column>
</widget> </widget>
</item> </item>
<item>
<widget class="QFrame" name="frame_2">
<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="QPushButton" name="pushButton">
<property name="layoutDirection">
<enum>Qt::LayoutDirection::LeftToRight</enum>
</property>
<property name="text">
<string>nur geänderte generieren</string>
</property>
<property name="icon">
<iconset theme="QIcon::ThemeIcon::MediaPlaybackStart"/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_2">
<property name="autoFillBackground">
<bool>false</bool>
</property>
<property name="text">
<string>Alle generieren</string>
</property>
<property name="icon">
<iconset theme="QIcon::ThemeIcon::MediaSeekForward"/>
</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>
<item>
<widget class="QPushButton" name="pB_lade_aus_fn2">
<property name="text">
<string>lade aus FN2</string>
</property>
<property name="icon">
<iconset theme="QIcon::ThemeIcon::GoDown"/>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<widget class="QScrollArea" name="scrollArea"> <widget class="QScrollArea" name="scrollArea">
@@ -163,6 +129,12 @@
<verstretch>0</verstretch> <verstretch>0</verstretch>
</sizepolicy> </sizepolicy>
</property> </property>
<property name="frameShadow">
<enum>QFrame::Shadow::Raised</enum>
</property>
<property name="midLineWidth">
<number>1</number>
</property>
<property name="widgetResizable"> <property name="widgetResizable">
<bool>true</bool> <bool>true</bool>
</property> </property>
@@ -171,8 +143,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>54</width> <width>68</width>
<height>718</height> <height>733</height>
</rect> </rect>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_2"> <layout class="QVBoxLayout" name="verticalLayout_2">
@@ -208,7 +180,7 @@
</widget> </widget>
<widget class="QFrame" name="frame_3"> <widget class="QFrame" name="frame_3">
<property name="frameShape"> <property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum> <enum>QFrame::Shape::StyledPanel</enum>
</property> </property>
<property name="frameShadow"> <property name="frameShadow">
<enum>QFrame::Shadow::Raised</enum> <enum>QFrame::Shadow::Raised</enum>
@@ -229,7 +201,7 @@
<item> <item>
<widget class="QFrame" name="frame_4"> <widget class="QFrame" name="frame_4">
<property name="frameShape"> <property name="frameShape">
<enum>QFrame::Shape::StyledPanel</enum> <enum>QFrame::Shape::NoFrame</enum>
</property> </property>
<property name="frameShadow"> <property name="frameShadow">
<enum>QFrame::Shadow::Raised</enum> <enum>QFrame::Shadow::Raised</enum>
@@ -261,14 +233,26 @@
</spacer> </spacer>
</item> </item>
<item> <item>
<widget class="QLabel" name="label_6"> <widget class="QPushButton" name="view_ref_pdf">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text"> <property name="text">
<string>Vorher (Referenz)</string> <string>Vorher (Referenz)</string>
</property> </property>
<property name="icon">
<iconset theme="QIcon::ThemeIcon::DocumentOpen"/>
</property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QSlider" name="alpha"> <widget class="QSlider" name="alpha">
<property name="enabled">
<bool>false</bool>
</property>
<property name="toolTip">
<string>Blendet zwischen Referenz-PDF (links) und neuer PDF (rechts) um. Doppelklick setzt auf Mitte zurück.</string>
</property>
<property name="minimum"> <property name="minimum">
<number>-100</number> <number>-100</number>
</property> </property>
@@ -281,10 +265,16 @@
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QLabel" name="label_7"> <widget class="QPushButton" name="view_new_pdf">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text"> <property name="text">
<string>Nachher (Neu)</string> <string>Nachher (Neu)</string>
</property> </property>
<property name="icon">
<iconset theme="QIcon::ThemeIcon::DocumentOpen"/>
</property>
</widget> </widget>
</item> </item>
<item> <item>
@@ -309,6 +299,12 @@
</item> </item>
<item> <item>
<widget class="QSlider" name="zoom"> <widget class="QSlider" name="zoom">
<property name="enabled">
<bool>false</bool>
</property>
<property name="toolTip">
<string>Vergrößert oder verkleinert die PDF-Ansicht (25% bis 300%). Doppelklick setzt auf 100% zurück.</string>
</property>
<property name="minimum"> <property name="minimum">
<number>25</number> <number>25</number>
</property> </property>
@@ -336,11 +332,46 @@
</property> </property>
</spacer> </spacer>
</item> </item>
<item>
<widget class="QPushButton" name="accept_changes">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Änderungen übernehmen</string>
</property>
<property name="icon">
<iconset theme="emblem-default"/>
</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> </layout>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QScrollArea" name="scrollArea_2"> <widget class="QScrollArea" name="scrollArea_2">
<property name="enabled">
<bool>true</bool>
</property>
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Raised</enum>
</property>
<property name="widgetResizable"> <property name="widgetResizable">
<bool>true</bool> <bool>true</bool>
</property> </property>
@@ -349,8 +380,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>649</width> <width>892</width>
<height>690</height> <height>702</height>
</rect> </rect>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_3"> <layout class="QVBoxLayout" name="verticalLayout_3">
@@ -366,20 +397,6 @@
<property name="bottomMargin"> <property name="bottomMargin">
<number>0</number> <number>0</number>
</property> </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> </layout>
</widget> </widget>
</widget> </widget>
@@ -396,7 +413,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>1263</width> <width>1263</width>
<height>33</height> <height>22</height>
</rect> </rect>
</property> </property>
<widget class="QMenu" name="menuProjekt"> <widget class="QMenu" name="menuProjekt">
@@ -404,7 +421,6 @@
<string>Projekt</string> <string>Projekt</string>
</property> </property>
<addaction name="actionNeu"/> <addaction name="actionNeu"/>
<addaction name="action_ffnen"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionVorhandene_Projekte"/> <addaction name="actionVorhandene_Projekte"/>
<addaction name="separator"/> <addaction name="separator"/>
@@ -417,7 +433,20 @@
<string>Thema</string> <string>Thema</string>
</property> </property>
</widget> </widget>
<widget class="QMenu" name="menuAktion">
<property name="enabled">
<bool>false</bool>
</property>
<property name="title">
<string>Aktion</string>
</property>
<addaction name="actionAlle_XML_Dateien_transformieren"/>
<addaction name="actionAlle_XML_Dateien_neu_transformieren_force"/>
<addaction name="separator"/>
<addaction name="actionAus_Datenbank_laden"/>
</widget>
<addaction name="menuProjekt"/> <addaction name="menuProjekt"/>
<addaction name="menuAktion"/>
<addaction name="menuThema"/> <addaction name="menuThema"/>
</widget> </widget>
<widget class="QStatusBar" name="statusbar"/> <widget class="QStatusBar" name="statusbar"/>
@@ -432,17 +461,6 @@
<string>Ctrl+N</string> <string>Ctrl+N</string>
</property> </property>
</action> </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"> <action name="actionBeenden">
<property name="icon"> <property name="icon">
<iconset theme="QIcon::ThemeIcon::ApplicationExit"/> <iconset theme="QIcon::ThemeIcon::ApplicationExit"/>
@@ -466,10 +484,36 @@
<property name="enabled"> <property name="enabled">
<bool>false</bool> <bool>false</bool>
</property> </property>
<property name="icon">
<iconset theme="folder-open"/>
</property>
<property name="text"> <property name="text">
<string>Vorhandene Projekte</string> <string>Vorhandene Projekte</string>
</property> </property>
</action> </action>
<action name="actionAlle_XML_Dateien_transformieren">
<property name="text">
<string>Alle XML-Dateien transformieren</string>
</property>
</action>
<action name="actionAlle_XML_Dateien_neu_transformieren_force">
<property name="icon">
<iconset theme="QIcon::ThemeIcon::ViewRefresh"/>
</property>
<property name="text">
<string>Alle XML-Dateien neu transformieren (force)</string>
</property>
</action>
<action name="actionFN2">
<property name="text">
<string>FN2</string>
</property>
</action>
<action name="actionAus_Datenbank_laden">
<property name="text">
<string>Aus Datenbank laden</string>
</property>
</action>
</widget> </widget>
<resources/> <resources/>
<connections> <connections>
+320 -300
View File
@@ -1,300 +1,320 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
################################################################################ ################################################################################
## Form generated from reading UI file 'MainWinddow.ui' ## Form generated from reading UI file 'MainWinddow.ui'
## ##
## Created by: Qt User Interface Compiler version 6.9.1 ## Created by: Qt User Interface Compiler version 6.10.1
## ##
## WARNING! All changes made in this file will be lost when recompiling UI file! ## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################ ################################################################################
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
QMetaObject, QObject, QPoint, QRect, QMetaObject, QObject, QPoint, QRect,
QSize, QTime, QUrl, Qt) QSize, QTime, QUrl, Qt)
from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient, from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient,
QCursor, QFont, QFontDatabase, QGradient, QCursor, QFont, QFontDatabase, QGradient,
QIcon, QImage, QKeySequence, QLinearGradient, QIcon, QImage, QKeySequence, QLinearGradient,
QPainter, QPalette, QPixmap, QRadialGradient, QPainter, QPalette, QPixmap, QRadialGradient,
QTransform) QTransform)
from PySide6.QtWidgets import (QApplication, QFrame, QHBoxLayout, QHeaderView, from PySide6.QtWidgets import (QApplication, QFrame, QHBoxLayout, QHeaderView,
QLabel, QMainWindow, QMenu, QMenuBar, QLabel, QLineEdit, QMainWindow, QMenu,
QPushButton, QScrollArea, QSizePolicy, QSlider, QMenuBar, QPushButton, QScrollArea, QSizePolicy,
QSpacerItem, QSplitter, QStatusBar, QTreeWidget, QSlider, QSpacerItem, QSplitter, QStatusBar,
QTreeWidgetItem, QVBoxLayout, QWidget) QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget)
class Ui_MainWindow(object): class Ui_MainWindow(object):
def setupUi(self, MainWindow): def setupUi(self, MainWindow):
if not MainWindow.objectName(): if not MainWindow.objectName():
MainWindow.setObjectName(u"MainWindow") MainWindow.setObjectName(u"MainWindow")
MainWindow.resize(1263, 774) MainWindow.resize(1263, 774)
self.actionNeu = QAction(MainWindow) self.actionNeu = QAction(MainWindow)
self.actionNeu.setObjectName(u"actionNeu") self.actionNeu.setObjectName(u"actionNeu")
icon = QIcon(QIcon.fromTheme(u"folder-new")) icon = QIcon(QIcon.fromTheme(u"folder-new"))
self.actionNeu.setIcon(icon) self.actionNeu.setIcon(icon)
self.action_ffnen = QAction(MainWindow) self.actionBeenden = QAction(MainWindow)
self.action_ffnen.setObjectName(u"action_ffnen") self.actionBeenden.setObjectName(u"actionBeenden")
icon1 = QIcon(QIcon.fromTheme(u"folder-open")) icon1 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.ApplicationExit))
self.action_ffnen.setIcon(icon1) self.actionBeenden.setIcon(icon1)
self.actionBeenden = QAction(MainWindow) self.actionEinstellungen = QAction(MainWindow)
self.actionBeenden.setObjectName(u"actionBeenden") self.actionEinstellungen.setObjectName(u"actionEinstellungen")
icon2 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.ApplicationExit)) icon2 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.DocumentProperties))
self.actionBeenden.setIcon(icon2) self.actionEinstellungen.setIcon(icon2)
self.actionEinstellungen = QAction(MainWindow) self.actionVorhandene_Projekte = QAction(MainWindow)
self.actionEinstellungen.setObjectName(u"actionEinstellungen") self.actionVorhandene_Projekte.setObjectName(u"actionVorhandene_Projekte")
icon3 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.DocumentProperties)) self.actionVorhandene_Projekte.setEnabled(False)
self.actionEinstellungen.setIcon(icon3) icon3 = QIcon(QIcon.fromTheme(u"folder-open"))
self.actionVorhandene_Projekte = QAction(MainWindow) self.actionVorhandene_Projekte.setIcon(icon3)
self.actionVorhandene_Projekte.setObjectName(u"actionVorhandene_Projekte") self.actionAlle_XML_Dateien_transformieren = QAction(MainWindow)
self.actionVorhandene_Projekte.setEnabled(False) self.actionAlle_XML_Dateien_transformieren.setObjectName(u"actionAlle_XML_Dateien_transformieren")
self.centralwidget = QWidget(MainWindow) self.actionAlle_XML_Dateien_neu_transformieren_force = QAction(MainWindow)
self.centralwidget.setObjectName(u"centralwidget") self.actionAlle_XML_Dateien_neu_transformieren_force.setObjectName(u"actionAlle_XML_Dateien_neu_transformieren_force")
self.horizontalLayout = QHBoxLayout(self.centralwidget) icon4 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.ViewRefresh))
self.horizontalLayout.setObjectName(u"horizontalLayout") self.actionAlle_XML_Dateien_neu_transformieren_force.setIcon(icon4)
self.horizontalLayout.setContentsMargins(0, 0, 0, 0) self.actionFN2 = QAction(MainWindow)
self.splitter = QSplitter(self.centralwidget) self.actionFN2.setObjectName(u"actionFN2")
self.splitter.setObjectName(u"splitter") self.actionAus_Datenbank_laden = QAction(MainWindow)
self.splitter.setOrientation(Qt.Orientation.Horizontal) self.actionAus_Datenbank_laden.setObjectName(u"actionAus_Datenbank_laden")
self.frame = QFrame(self.splitter) self.centralwidget = QWidget(MainWindow)
self.frame.setObjectName(u"frame") self.centralwidget.setObjectName(u"centralwidget")
sizePolicy = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred) self.horizontalLayout = QHBoxLayout(self.centralwidget)
sizePolicy.setHorizontalStretch(0) self.horizontalLayout.setObjectName(u"horizontalLayout")
sizePolicy.setVerticalStretch(0) self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
sizePolicy.setHeightForWidth(self.frame.sizePolicy().hasHeightForWidth()) self.splitter = QSplitter(self.centralwidget)
self.frame.setSizePolicy(sizePolicy) self.splitter.setObjectName(u"splitter")
self.frame.setMinimumSize(QSize(200, 0)) self.splitter.setOrientation(Qt.Orientation.Horizontal)
self.frame.setFrameShape(QFrame.Shape.StyledPanel) self.splitter.setOpaqueResize(False)
self.frame.setFrameShadow(QFrame.Shadow.Raised) self.splitter.setChildrenCollapsible(False)
self.verticalLayout = QVBoxLayout(self.frame) self.frame = QFrame(self.splitter)
self.verticalLayout.setObjectName(u"verticalLayout") self.frame.setObjectName(u"frame")
self.verticalLayout.setContentsMargins(-1, -1, -1, 0) sizePolicy = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred)
self.treeWidget = QTreeWidget(self.frame) sizePolicy.setHorizontalStretch(0)
__qtreewidgetitem = QTreeWidgetItem() sizePolicy.setVerticalStretch(0)
__qtreewidgetitem.setText(1, u"2"); sizePolicy.setHeightForWidth(self.frame.sizePolicy().hasHeightForWidth())
__qtreewidgetitem.setText(0, u"1"); self.frame.setSizePolicy(sizePolicy)
self.treeWidget.setHeaderItem(__qtreewidgetitem) self.frame.setMinimumSize(QSize(200, 0))
self.treeWidget.setObjectName(u"treeWidget") self.frame.setFrameShape(QFrame.Shape.StyledPanel)
sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) self.frame.setFrameShadow(QFrame.Shadow.Raised)
sizePolicy1.setHorizontalStretch(0) self.verticalLayout = QVBoxLayout(self.frame)
sizePolicy1.setVerticalStretch(0) self.verticalLayout.setObjectName(u"verticalLayout")
sizePolicy1.setHeightForWidth(self.treeWidget.sizePolicy().hasHeightForWidth()) self.verticalLayout.setContentsMargins(-1, -1, -1, 0)
self.treeWidget.setSizePolicy(sizePolicy1) self.projectPath = QLabel(self.frame)
self.treeWidget.setColumnCount(2) self.projectPath.setObjectName(u"projectPath")
self.treeWidget.header().setHighlightSections(True) self.projectPath.setStyleSheet(u"QLabel { padding: 5px; font-weight: bold; }")
self.treeWidget.header().setStretchLastSection(True)
self.verticalLayout.addWidget(self.projectPath)
self.verticalLayout.addWidget(self.treeWidget)
self.searchEdit = QLineEdit(self.frame)
self.frame_2 = QFrame(self.frame) self.searchEdit.setObjectName(u"searchEdit")
self.frame_2.setObjectName(u"frame_2") self.searchEdit.setClearButtonEnabled(True)
self.frame_2.setFrameShadow(QFrame.Shadow.Raised)
self.horizontalLayout_2 = QHBoxLayout(self.frame_2) self.verticalLayout.addWidget(self.searchEdit)
self.horizontalLayout_2.setObjectName(u"horizontalLayout_2")
self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0) self.treeWidget = QTreeWidget(self.frame)
self.pushButton = QPushButton(self.frame_2) __qtreewidgetitem = QTreeWidgetItem()
self.pushButton.setObjectName(u"pushButton") __qtreewidgetitem.setText(1, u"2");
self.pushButton.setLayoutDirection(Qt.LayoutDirection.LeftToRight) __qtreewidgetitem.setText(0, u"1");
icon4 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.MediaPlaybackStart)) self.treeWidget.setHeaderItem(__qtreewidgetitem)
self.pushButton.setIcon(icon4) self.treeWidget.setObjectName(u"treeWidget")
sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
self.horizontalLayout_2.addWidget(self.pushButton) sizePolicy1.setHorizontalStretch(0)
sizePolicy1.setVerticalStretch(0)
self.pushButton_2 = QPushButton(self.frame_2) sizePolicy1.setHeightForWidth(self.treeWidget.sizePolicy().hasHeightForWidth())
self.pushButton_2.setObjectName(u"pushButton_2") self.treeWidget.setSizePolicy(sizePolicy1)
self.pushButton_2.setAutoFillBackground(False) self.treeWidget.setStyleSheet(u"QTreeWidget::item {\n"
icon5 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.MediaSeekForward)) " padding: 4px 4px;\n"
self.pushButton_2.setIcon(icon5) "}\n"
"\n"
self.horizontalLayout_2.addWidget(self.pushButton_2) "QTreeWidget::item:selected {\n"
" background-color: palette(highlight);\n"
self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) " color: palette(highlighted-text);\n"
"}")
self.horizontalLayout_2.addItem(self.horizontalSpacer) self.treeWidget.setColumnCount(2)
self.treeWidget.header().setHighlightSections(True)
self.pB_lade_aus_fn2 = QPushButton(self.frame_2) self.treeWidget.header().setStretchLastSection(True)
self.pB_lade_aus_fn2.setObjectName(u"pB_lade_aus_fn2")
icon6 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.GoDown)) self.verticalLayout.addWidget(self.treeWidget)
self.pB_lade_aus_fn2.setIcon(icon6)
self.splitter.addWidget(self.frame)
self.horizontalLayout_2.addWidget(self.pB_lade_aus_fn2) self.scrollArea = QScrollArea(self.splitter)
self.scrollArea.setObjectName(u"scrollArea")
sizePolicy2 = QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum)
self.verticalLayout.addWidget(self.frame_2) sizePolicy2.setHorizontalStretch(0)
sizePolicy2.setVerticalStretch(0)
self.splitter.addWidget(self.frame) sizePolicy2.setHeightForWidth(self.scrollArea.sizePolicy().hasHeightForWidth())
self.scrollArea = QScrollArea(self.splitter) self.scrollArea.setSizePolicy(sizePolicy2)
self.scrollArea.setObjectName(u"scrollArea") self.scrollArea.setFrameShadow(QFrame.Shadow.Raised)
sizePolicy2 = QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum) self.scrollArea.setMidLineWidth(1)
sizePolicy2.setHorizontalStretch(0) self.scrollArea.setWidgetResizable(True)
sizePolicy2.setVerticalStretch(0) self.scrollAreaWidgetContents = QWidget()
sizePolicy2.setHeightForWidth(self.scrollArea.sizePolicy().hasHeightForWidth()) self.scrollAreaWidgetContents.setObjectName(u"scrollAreaWidgetContents")
self.scrollArea.setSizePolicy(sizePolicy2) self.scrollAreaWidgetContents.setGeometry(QRect(0, 0, 68, 728))
self.scrollArea.setWidgetResizable(True) self.verticalLayout_2 = QVBoxLayout(self.scrollAreaWidgetContents)
self.scrollAreaWidgetContents = QWidget() self.verticalLayout_2.setObjectName(u"verticalLayout_2")
self.scrollAreaWidgetContents.setObjectName(u"scrollAreaWidgetContents") self.label = QLabel(self.scrollAreaWidgetContents)
self.scrollAreaWidgetContents.setGeometry(QRect(0, 0, 54, 718)) self.label.setObjectName(u"label")
self.verticalLayout_2 = QVBoxLayout(self.scrollAreaWidgetContents)
self.verticalLayout_2.setObjectName(u"verticalLayout_2") self.verticalLayout_2.addWidget(self.label)
self.label = QLabel(self.scrollAreaWidgetContents)
self.label.setObjectName(u"label") self.label_2 = QLabel(self.scrollAreaWidgetContents)
self.label_2.setObjectName(u"label_2")
self.verticalLayout_2.addWidget(self.label)
self.verticalLayout_2.addWidget(self.label_2)
self.label_2 = QLabel(self.scrollAreaWidgetContents)
self.label_2.setObjectName(u"label_2") self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
self.verticalLayout_2.addWidget(self.label_2) self.verticalLayout_2.addItem(self.verticalSpacer)
self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) self.scrollArea.setWidget(self.scrollAreaWidgetContents)
self.splitter.addWidget(self.scrollArea)
self.verticalLayout_2.addItem(self.verticalSpacer) self.frame_3 = QFrame(self.splitter)
self.frame_3.setObjectName(u"frame_3")
self.scrollArea.setWidget(self.scrollAreaWidgetContents) self.frame_3.setFrameShape(QFrame.Shape.StyledPanel)
self.splitter.addWidget(self.scrollArea) self.frame_3.setFrameShadow(QFrame.Shadow.Raised)
self.frame_3 = QFrame(self.splitter) self.verticalLayout_4 = QVBoxLayout(self.frame_3)
self.frame_3.setObjectName(u"frame_3") self.verticalLayout_4.setObjectName(u"verticalLayout_4")
self.frame_3.setFrameShape(QFrame.Shape.NoFrame) self.verticalLayout_4.setContentsMargins(0, 0, 0, 0)
self.frame_3.setFrameShadow(QFrame.Shadow.Raised) self.frame_4 = QFrame(self.frame_3)
self.verticalLayout_4 = QVBoxLayout(self.frame_3) self.frame_4.setObjectName(u"frame_4")
self.verticalLayout_4.setObjectName(u"verticalLayout_4") self.frame_4.setFrameShape(QFrame.Shape.NoFrame)
self.verticalLayout_4.setContentsMargins(0, 0, 0, 0) self.frame_4.setFrameShadow(QFrame.Shadow.Raised)
self.frame_4 = QFrame(self.frame_3) self.horizontalLayout_3 = QHBoxLayout(self.frame_4)
self.frame_4.setObjectName(u"frame_4") self.horizontalLayout_3.setObjectName(u"horizontalLayout_3")
self.frame_4.setFrameShape(QFrame.Shape.StyledPanel) self.horizontalLayout_3.setContentsMargins(0, 0, 0, 0)
self.frame_4.setFrameShadow(QFrame.Shadow.Raised) self.horizontalSpacer_4 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
self.horizontalLayout_3 = QHBoxLayout(self.frame_4)
self.horizontalLayout_3.setObjectName(u"horizontalLayout_3") self.horizontalLayout_3.addItem(self.horizontalSpacer_4)
self.horizontalLayout_3.setContentsMargins(0, 0, 0, 0)
self.horizontalSpacer_4 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) self.view_ref_pdf = QPushButton(self.frame_4)
self.view_ref_pdf.setObjectName(u"view_ref_pdf")
self.horizontalLayout_3.addItem(self.horizontalSpacer_4) self.view_ref_pdf.setEnabled(False)
icon5 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.DocumentOpen))
self.label_6 = QLabel(self.frame_4) self.view_ref_pdf.setIcon(icon5)
self.label_6.setObjectName(u"label_6")
self.horizontalLayout_3.addWidget(self.view_ref_pdf)
self.horizontalLayout_3.addWidget(self.label_6)
self.alpha = QSlider(self.frame_4)
self.alpha = QSlider(self.frame_4) self.alpha.setObjectName(u"alpha")
self.alpha.setObjectName(u"alpha") self.alpha.setEnabled(False)
self.alpha.setMinimum(-100) self.alpha.setMinimum(-100)
self.alpha.setMaximum(100) self.alpha.setMaximum(100)
self.alpha.setOrientation(Qt.Orientation.Horizontal) self.alpha.setOrientation(Qt.Orientation.Horizontal)
self.horizontalLayout_3.addWidget(self.alpha) self.horizontalLayout_3.addWidget(self.alpha)
self.label_7 = QLabel(self.frame_4) self.view_new_pdf = QPushButton(self.frame_4)
self.label_7.setObjectName(u"label_7") self.view_new_pdf.setObjectName(u"view_new_pdf")
self.view_new_pdf.setEnabled(False)
self.horizontalLayout_3.addWidget(self.label_7) self.view_new_pdf.setIcon(icon5)
self.horizontalSpacer_2 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) self.horizontalLayout_3.addWidget(self.view_new_pdf)
self.horizontalLayout_3.addItem(self.horizontalSpacer_2) self.horizontalSpacer_2 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
self.label_5 = QLabel(self.frame_4) self.horizontalLayout_3.addItem(self.horizontalSpacer_2)
self.label_5.setObjectName(u"label_5")
self.label_5 = QLabel(self.frame_4)
self.horizontalLayout_3.addWidget(self.label_5) self.label_5.setObjectName(u"label_5")
self.zoom = QSlider(self.frame_4) self.horizontalLayout_3.addWidget(self.label_5)
self.zoom.setObjectName(u"zoom")
self.zoom.setMinimum(25) self.zoom = QSlider(self.frame_4)
self.zoom.setMaximum(300) self.zoom.setObjectName(u"zoom")
self.zoom.setValue(100) self.zoom.setEnabled(False)
self.zoom.setOrientation(Qt.Orientation.Horizontal) self.zoom.setMinimum(25)
self.zoom.setMaximum(300)
self.horizontalLayout_3.addWidget(self.zoom) self.zoom.setValue(100)
self.zoom.setOrientation(Qt.Orientation.Horizontal)
self.horizontalSpacer_5 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
self.horizontalLayout_3.addWidget(self.zoom)
self.horizontalLayout_3.addItem(self.horizontalSpacer_5)
self.horizontalSpacer_5 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
self.verticalLayout_4.addWidget(self.frame_4) self.horizontalLayout_3.addItem(self.horizontalSpacer_5)
self.scrollArea_2 = QScrollArea(self.frame_3) self.accept_changes = QPushButton(self.frame_4)
self.scrollArea_2.setObjectName(u"scrollArea_2") self.accept_changes.setObjectName(u"accept_changes")
self.scrollArea_2.setWidgetResizable(True) self.accept_changes.setEnabled(False)
self.scrollAreaWidgetContents_2 = QWidget() icon6 = QIcon(QIcon.fromTheme(u"emblem-default"))
self.scrollAreaWidgetContents_2.setObjectName(u"scrollAreaWidgetContents_2") self.accept_changes.setIcon(icon6)
self.scrollAreaWidgetContents_2.setGeometry(QRect(0, 0, 649, 690))
self.verticalLayout_3 = QVBoxLayout(self.scrollAreaWidgetContents_2) self.horizontalLayout_3.addWidget(self.accept_changes)
self.verticalLayout_3.setObjectName(u"verticalLayout_3")
self.verticalLayout_3.setContentsMargins(0, 0, 0, 0) self.horizontalSpacer_3 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
self.label_3 = QLabel(self.scrollAreaWidgetContents_2)
self.label_3.setObjectName(u"label_3") self.horizontalLayout_3.addItem(self.horizontalSpacer_3)
self.verticalLayout_3.addWidget(self.label_3)
self.verticalLayout_4.addWidget(self.frame_4)
self.label_4 = QLabel(self.scrollAreaWidgetContents_2)
self.label_4.setObjectName(u"label_4") self.scrollArea_2 = QScrollArea(self.frame_3)
self.scrollArea_2.setObjectName(u"scrollArea_2")
self.verticalLayout_3.addWidget(self.label_4) self.scrollArea_2.setEnabled(True)
self.scrollArea_2.setFrameShape(QFrame.Shape.NoFrame)
self.scrollArea_2.setWidget(self.scrollAreaWidgetContents_2) self.scrollArea_2.setFrameShadow(QFrame.Shadow.Raised)
self.scrollArea_2.setWidgetResizable(True)
self.verticalLayout_4.addWidget(self.scrollArea_2) self.scrollAreaWidgetContents_2 = QWidget()
self.scrollAreaWidgetContents_2.setObjectName(u"scrollAreaWidgetContents_2")
self.splitter.addWidget(self.frame_3) self.scrollAreaWidgetContents_2.setGeometry(QRect(0, 0, 880, 697))
self.verticalLayout_3 = QVBoxLayout(self.scrollAreaWidgetContents_2)
self.horizontalLayout.addWidget(self.splitter) self.verticalLayout_3.setObjectName(u"verticalLayout_3")
self.verticalLayout_3.setContentsMargins(0, 0, 0, 0)
MainWindow.setCentralWidget(self.centralwidget) self.scrollArea_2.setWidget(self.scrollAreaWidgetContents_2)
self.menubar = QMenuBar(MainWindow)
self.menubar.setObjectName(u"menubar") self.verticalLayout_4.addWidget(self.scrollArea_2)
self.menubar.setGeometry(QRect(0, 0, 1263, 33))
self.menuProjekt = QMenu(self.menubar) self.splitter.addWidget(self.frame_3)
self.menuProjekt.setObjectName(u"menuProjekt")
self.menuThema = QMenu(self.menubar) self.horizontalLayout.addWidget(self.splitter)
self.menuThema.setObjectName(u"menuThema")
MainWindow.setMenuBar(self.menubar) MainWindow.setCentralWidget(self.centralwidget)
self.statusbar = QStatusBar(MainWindow) self.menubar = QMenuBar(MainWindow)
self.statusbar.setObjectName(u"statusbar") self.menubar.setObjectName(u"menubar")
MainWindow.setStatusBar(self.statusbar) self.menubar.setGeometry(QRect(0, 0, 1263, 22))
self.menuProjekt = QMenu(self.menubar)
self.menubar.addAction(self.menuProjekt.menuAction()) self.menuProjekt.setObjectName(u"menuProjekt")
self.menubar.addAction(self.menuThema.menuAction()) self.menuThema = QMenu(self.menubar)
self.menuProjekt.addAction(self.actionNeu) self.menuThema.setObjectName(u"menuThema")
self.menuProjekt.addAction(self.action_ffnen) self.menuAktion = QMenu(self.menubar)
self.menuProjekt.addSeparator() self.menuAktion.setObjectName(u"menuAktion")
self.menuProjekt.addAction(self.actionVorhandene_Projekte) self.menuAktion.setEnabled(False)
self.menuProjekt.addSeparator() MainWindow.setMenuBar(self.menubar)
self.menuProjekt.addAction(self.actionEinstellungen) self.statusbar = QStatusBar(MainWindow)
self.menuProjekt.addSeparator() self.statusbar.setObjectName(u"statusbar")
self.menuProjekt.addAction(self.actionBeenden) MainWindow.setStatusBar(self.statusbar)
self.retranslateUi(MainWindow) self.menubar.addAction(self.menuProjekt.menuAction())
self.actionBeenden.triggered.connect(MainWindow.close) self.menubar.addAction(self.menuAktion.menuAction())
self.menubar.addAction(self.menuThema.menuAction())
QMetaObject.connectSlotsByName(MainWindow) self.menuProjekt.addAction(self.actionNeu)
# setupUi self.menuProjekt.addSeparator()
self.menuProjekt.addAction(self.actionVorhandene_Projekte)
def retranslateUi(self, MainWindow): self.menuProjekt.addSeparator()
MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"DocuMentor", None)) self.menuProjekt.addAction(self.actionEinstellungen)
self.actionNeu.setText(QCoreApplication.translate("MainWindow", u"Neu ...", None)) self.menuProjekt.addSeparator()
#if QT_CONFIG(shortcut) self.menuProjekt.addAction(self.actionBeenden)
self.actionNeu.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+N", None)) self.menuAktion.addAction(self.actionAlle_XML_Dateien_transformieren)
#endif // QT_CONFIG(shortcut) self.menuAktion.addAction(self.actionAlle_XML_Dateien_neu_transformieren_force)
self.action_ffnen.setText(QCoreApplication.translate("MainWindow", u"\u00d6ffnen ...", None)) self.menuAktion.addSeparator()
#if QT_CONFIG(shortcut) self.menuAktion.addAction(self.actionAus_Datenbank_laden)
self.action_ffnen.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+O", None))
#endif // QT_CONFIG(shortcut) self.retranslateUi(MainWindow)
self.actionBeenden.setText(QCoreApplication.translate("MainWindow", u"Beenden", None)) self.actionBeenden.triggered.connect(MainWindow.close)
self.actionEinstellungen.setText(QCoreApplication.translate("MainWindow", u"Einstellungen ...", None))
#if QT_CONFIG(shortcut) QMetaObject.connectSlotsByName(MainWindow)
self.actionEinstellungen.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+S", None)) # setupUi
#endif // QT_CONFIG(shortcut)
self.actionVorhandene_Projekte.setText(QCoreApplication.translate("MainWindow", u"Vorhandene Projekte", None)) def retranslateUi(self, MainWindow):
self.pushButton.setText(QCoreApplication.translate("MainWindow", u"nur ge\u00e4nderte generieren", None)) MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"DocuMentor", None))
self.pushButton_2.setText(QCoreApplication.translate("MainWindow", u"Alle generieren", None)) self.actionNeu.setText(QCoreApplication.translate("MainWindow", u"Neu ...", None))
self.pB_lade_aus_fn2.setText(QCoreApplication.translate("MainWindow", u"lade aus FN2", None)) #if QT_CONFIG(shortcut)
self.label.setText("") self.actionNeu.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+N", None))
self.label_2.setText("") #endif // QT_CONFIG(shortcut)
self.label_6.setText(QCoreApplication.translate("MainWindow", u"Vorher (Referenz)", None)) self.actionBeenden.setText(QCoreApplication.translate("MainWindow", u"Beenden", None))
self.label_7.setText(QCoreApplication.translate("MainWindow", u"Nachher (Neu)", None)) self.actionEinstellungen.setText(QCoreApplication.translate("MainWindow", u"Einstellungen ...", None))
self.label_5.setText(QCoreApplication.translate("MainWindow", u"Zoom", None)) #if QT_CONFIG(shortcut)
self.label_3.setText(QCoreApplication.translate("MainWindow", u"TextLabel", None)) self.actionEinstellungen.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+S", None))
self.label_4.setText(QCoreApplication.translate("MainWindow", u"TextLabel", None)) #endif // QT_CONFIG(shortcut)
self.menuProjekt.setTitle(QCoreApplication.translate("MainWindow", u"Projekt", None)) self.actionVorhandene_Projekte.setText(QCoreApplication.translate("MainWindow", u"Vorhandene Projekte", None))
self.menuThema.setTitle(QCoreApplication.translate("MainWindow", u"Thema", None)) self.actionAlle_XML_Dateien_transformieren.setText(QCoreApplication.translate("MainWindow", u"Alle XML-Dateien transformieren", None))
# retranslateUi self.actionAlle_XML_Dateien_neu_transformieren_force.setText(QCoreApplication.translate("MainWindow", u"Alle XML-Dateien neu transformieren (force)", None))
self.actionFN2.setText(QCoreApplication.translate("MainWindow", u"FN2", None))
self.actionAus_Datenbank_laden.setText(QCoreApplication.translate("MainWindow", u"Aus Datenbank laden", None))
self.projectPath.setText(QCoreApplication.translate("MainWindow", u"Kein Projekt geladen", None))
self.searchEdit.setPlaceholderText(QCoreApplication.translate("MainWindow", u"Knoten oder XSL-Datei filtern...", None))
self.label.setText("")
self.label_2.setText("")
self.view_ref_pdf.setText(QCoreApplication.translate("MainWindow", u"Vorher (Referenz)", None))
#if QT_CONFIG(tooltip)
self.alpha.setToolTip(QCoreApplication.translate("MainWindow", u"Blendet zwischen Referenz-PDF (links) und neuer PDF (rechts) um. Doppelklick setzt auf Mitte zur\u00fcck.", None))
#endif // QT_CONFIG(tooltip)
self.view_new_pdf.setText(QCoreApplication.translate("MainWindow", u"Nachher (Neu)", None))
self.label_5.setText(QCoreApplication.translate("MainWindow", u"Zoom", None))
#if QT_CONFIG(tooltip)
self.zoom.setToolTip(QCoreApplication.translate("MainWindow", u"Vergr\u00f6\u00dfert oder verkleinert die PDF-Ansicht (25% bis 300%). Doppelklick setzt auf 100% zur\u00fcck.", None))
#endif // QT_CONFIG(tooltip)
self.accept_changes.setText(QCoreApplication.translate("MainWindow", u"\u00c4nderungen \u00fcbernehmen", None))
self.menuProjekt.setTitle(QCoreApplication.translate("MainWindow", u"Projekt", None))
self.menuThema.setTitle(QCoreApplication.translate("MainWindow", u"Thema", None))
self.menuAktion.setTitle(QCoreApplication.translate("MainWindow", u"Aktion", None))
# retranslateUi
+765 -1977
View File
File diff suppressed because it is too large Load Diff
+137
View File
@@ -0,0 +1,137 @@
"""
ObsoleteEntriesDialog — Dialog zur Bestätigung des Entfernens veralteter Projekteinträge.
Zeigt XslFile-Einträge an, die nicht mehr in der Datenbank vorhanden sind,
und lässt den Benutzer entscheiden ob sie entfernt werden sollen.
"""
import logging
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QCheckBox,
QDialog,
QDialogButtonBox,
QLabel,
QSizePolicy,
QTreeWidget,
QTreeWidgetItem,
QVBoxLayout,
)
from obsolete_detector import ObsoleteGroup
logger = logging.getLogger(__name__)
class ObsoleteEntriesDialog(QDialog):
"""
Dialog zur Anzeige und Bestätigung veralteter Einträge nach einem DB-Import.
Zeigt die veralteten XslFile-Einträge gruppiert nach ihrer Baumhierarchie an.
Der Benutzer kann entscheiden ob die Einträge entfernt und ob nicht mehr
verwendete XML-Dateien physisch gelöscht werden sollen.
"""
def __init__(self, parent, obsolete_groups: list[ObsoleteGroup]):
"""
Args:
parent: Eltern-Widget
obsolete_groups: Veraltete Einträge gruppiert nach Hierarchiepfad
"""
super().__init__(parent)
self._obsolete_groups = obsolete_groups
self._setup_ui()
self._populate_tree()
def _setup_ui(self) -> None:
"""Erstellt die UI-Elemente des Dialogs."""
total_count = sum(len(g.xsl_entries) for g in self._obsolete_groups)
self.setWindowTitle("Veraltete Einträge gefunden")
self.resize(640, 420)
self.setSizeGripEnabled(True)
layout = QVBoxLayout(self)
layout.setSpacing(10)
# Erklärungstext
info_label = QLabel(
f"<b>{total_count} XSL-Datei(en)</b> sind nicht mehr in der Datenbank vorhanden "
f"und können aus dem Projekt entfernt werden."
)
info_label.setWordWrap(True)
layout.addWidget(info_label)
# Baumansicht der veralteten Einträge
self._tree = QTreeWidget()
self._tree.setColumnCount(3)
self._tree.setHeaderLabels(["Bezeichnung", "XSL-Datei", "XML-Dateien"])
self._tree.setColumnWidth(0, 280)
self._tree.setColumnWidth(1, 200)
self._tree.setColumnWidth(2, 80)
self._tree.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
self._tree.setAlternatingRowColors(True)
self._tree.setEditTriggers(QTreeWidget.EditTrigger.NoEditTriggers)
layout.addWidget(self._tree)
# Checkbox für physische XML-Löschung
self._delete_xml_checkbox = QCheckBox("Nicht mehr verwendete XML-Dateien physisch löschen")
self._delete_xml_checkbox.setChecked(False)
layout.addWidget(self._delete_xml_checkbox)
# Dialog-Buttons
self._button_box = QDialogButtonBox()
remove_button = self._button_box.addButton(
"Veraltete Einträge entfernen", QDialogButtonBox.ButtonRole.AcceptRole
)
remove_button.setToolTip("Entfernt alle aufgelisteten Einträge aus dem Projekt")
self._button_box.addButton(QDialogButtonBox.StandardButton.Cancel)
self._button_box.accepted.connect(self.accept)
self._button_box.rejected.connect(self.reject)
layout.addWidget(self._button_box)
def _populate_tree(self) -> None:
"""Befüllt den QTreeWidget mit den veralteten Einträgen."""
self._tree.clear()
for group in self._obsolete_groups:
# Hierarchiepfad als verschachtelte Items aufbauen
parent_item = self._tree.invisibleRootItem()
for path_part in group.node_path:
# Prüfe ob dieser Pfadteil bereits als Kind vorhanden ist
existing = None
for i in range(parent_item.childCount()):
child = parent_item.child(i)
if child.text(0) == path_part and not child.data(0, Qt.ItemDataRole.UserRole):
existing = child
break
if existing:
parent_item = existing
else:
node_item = QTreeWidgetItem(parent_item, [path_part])
font = node_item.font(0)
font.setBold(True)
node_item.setFont(0, font)
parent_item = node_item
# XslFile-Einträge unter dem Hierarchiepfad
for entry in group.xsl_entries:
xsl = entry.xsl_file
xml_count = str(len(xsl.xmls)) if xsl.xmls else "0"
xsl_item = QTreeWidgetItem(
parent_item,
[xsl.bez, xsl.xsl_file.name, xml_count],
)
xsl_item.setData(0, Qt.ItemDataRole.UserRole, xsl)
xsl_item.setToolTip(1, str(xsl.xsl_file))
self._tree.expandAll()
def delete_xml_files(self) -> bool:
"""
Gibt zurück ob der Benutzer die physische Löschung der XML-Dateien gewünscht hat.
Returns:
True wenn die Checkbox aktiviert ist
"""
return self._delete_xml_checkbox.isChecked()
+43 -53
View File
@@ -5,6 +5,7 @@ from PySide6.QtWidgets import QDialog, QFileDialog, QMessageBox
from conf import app_settings from conf import app_settings
from ui.PdfProject_ui import Ui_projectDlg from ui.PdfProject_ui import Ui_projectDlg
from ui.ProjectXsltParamsDialog import ProjectXsltParamsDialog
class PdfProjectDlg(QDialog): class PdfProjectDlg(QDialog):
@@ -15,7 +16,7 @@ class PdfProjectDlg(QDialog):
Args: Args:
parent: Übergeordnetes Widget parent: Übergeordnetes Widget
project_data: Bestehende Projektdaten zum Bearbeiten (optional) project_data: Bestehende Projektdaten zum Bearbeiten (optional)
edit_mode: Wenn True, werden Projekt-Name und -Ordner deaktiviert (nur Einstellungen ändern) edit_mode: Wenn True, wird der Projekt-Ordner deaktiviert (nur Name und Einstellungen ändern)
""" """
super().__init__(parent) super().__init__(parent)
@@ -26,6 +27,7 @@ class PdfProjectDlg(QDialog):
# Projektdaten speichern # Projektdaten speichern
self.project_data = project_data or {} self.project_data = project_data or {}
self.edit_mode = edit_mode self.edit_mode = edit_mode
self.xslt_params: dict[str, str] = dict(self.project_data.get("xslt_params", {}))
# Dialog-Eigenschaften setzen # Dialog-Eigenschaften setzen
self.setModal(True) self.setModal(True)
@@ -49,11 +51,17 @@ class PdfProjectDlg(QDialog):
"""Verbindet die Signale mit den entsprechenden Slots.""" """Verbindet die Signale mit den entsprechenden Slots."""
# Browse-Button für Projekt-Ordner # Browse-Button für Projekt-Ordner
self.ui.pushButton.clicked.connect(self.browse_project_dir) 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)
# XSLT-Parameter bearbeiten
self.ui.btnEditXsltParams.clicked.connect(self._edit_xslt_params)
# OK/Cancel Buttons sind bereits in der UI-Datei verbunden # OK/Cancel Buttons sind bereits in der UI-Datei verbunden
# self.ui.buttonBox.accepted.connect(self.accept) # self.ui.buttonBox.accepted.connect(self.accept)
# self.ui.buttonBox.rejected.connect(self.reject) # self.ui.buttonBox.rejected.connect(self.reject)
# Überschreibe accept() für Validierung # Überschreibe accept() für Validierung
self.ui.buttonBox.accepted.disconnect() self.ui.buttonBox.accepted.disconnect()
self.ui.buttonBox.accepted.connect(self.validate_and_accept) self.ui.buttonBox.accepted.connect(self.validate_and_accept)
@@ -132,6 +140,10 @@ class PdfProjectDlg(QDialog):
if 'postgre_sql_db_id' in self.project_data: 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']) 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): def _select_combo_by_data(self, combo_box, data_value):
""" """
@@ -166,7 +178,29 @@ class PdfProjectDlg(QDialog):
if not self.ui.lineProjectName.text(): if not self.ui.lineProjectName.text():
project_name = os.path.basename(selected_dir) project_name = os.path.basename(selected_dir)
self.ui.lineProjectName.setText(project_name) 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 _edit_xslt_params(self):
"""Öffnet den Dialog zur Bearbeitung der projektweiten XSLT-Parameter."""
dialog = ProjectXsltParamsDialog(self, self.xslt_params)
if dialog.exec() == ProjectXsltParamsDialog.DialogCode.Accepted:
self.xslt_params = dialog.get_params()
def validate_and_accept(self): def validate_and_accept(self):
"""Validiert die Eingaben und akzeptiert den Dialog.""" """Validiert die Eingaben und akzeptiert den Dialog."""
# Projekt-Name prüfen # Projekt-Name prüfen
@@ -232,10 +266,11 @@ class PdfProjectDlg(QDialog):
def get_project_data(self): def get_project_data(self):
""" """
Gibt die eingegebenen Projektdaten zurück. Gibt die eingegebenen Projektdaten zurück.
Returns: Returns:
dict: Dictionary mit allen Projektdaten dict: Dictionary mit allen Projektdaten
""" """
fop_config_dir = self.ui.lineFopConfigDir.text().strip()
return { return {
'name': self.ui.lineProjectName.text().strip(), 'name': self.ui.lineProjectName.text().strip(),
'project_dir': self.ui.lineProjectDir.text().strip(), 'project_dir': self.ui.lineProjectDir.text().strip(),
@@ -244,7 +279,9 @@ class PdfProjectDlg(QDialog):
'saxon_jar_id': self.ui.cB_SaxonJar.currentData(), 'saxon_jar_id': self.ui.cB_SaxonJar.currentData(),
'apache_fop_id': self.ui.cB_ApacheFop.currentData(), 'apache_fop_id': self.ui.cB_ApacheFop.currentData(),
'diff_pdf_id': self.ui.cB_Diff_Pdf.currentData(), 'diff_pdf_id': self.ui.cB_Diff_Pdf.currentData(),
'postgre_sql_db_id': self.ui.cB_Postgres.currentData() 'postgre_sql_db_id': self.ui.cB_Postgres.currentData(),
'fop_config_dir': fop_config_dir if fop_config_dir else None,
'xslt_params': self.xslt_params,
} }
def _configure_edit_mode(self): def _configure_edit_mode(self):
@@ -266,50 +303,3 @@ class PdfProjectDlg(QDialog):
self.project_data = project_data self.project_data = project_data
self._load_project_data() self._load_project_data()
# Convenience-Funktionen für einfache Verwendung
def create_project_dialog(parent=None):
"""
Erstellt einen neuen Projekt-Dialog für ein neues Projekt.
Args:
parent: Übergeordnetes Widget
Returns:
PdfProjectDlg: Der Dialog
"""
return PdfProjectDlg(parent)
def edit_project_dialog(parent=None, project_data=None):
"""
Erstellt einen Projekt-Dialog zum Bearbeiten eines bestehenden Projekts.
Args:
parent: Übergeordnetes Widget
project_data: Bestehende Projektdaten
Returns:
PdfProjectDlg: Der Dialog
"""
return PdfProjectDlg(parent, project_data)
def show_project_dialog(parent=None, project_data=None):
"""
Zeigt einen Projekt-Dialog an und gibt die Ergebnisse zurück.
Args:
parent: Übergeordnetes Widget
project_data: Bestehende Projektdaten (optional)
Returns:
tuple: (accepted: bool, project_data: dict)
"""
dialog = PdfProjectDlg(parent, project_data)
result = dialog.exec()
if result == QDialog.DialogCode.Accepted:
return True, dialog.get_project_data()
else:
return False, None
+59 -4
View File
@@ -7,7 +7,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>608</width> <width>608</width>
<height>299</height> <height>375</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@@ -109,25 +109,80 @@
<widget class="QComboBox" name="cB_ApacheFop"/> <widget class="QComboBox" name="cB_ApacheFop"/>
</item> </item>
<item row="6" column="0"> <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"> <widget class="QLabel" name="label_7">
<property name="text"> <property name="text">
<string>diff-pdf:</string> <string>diff-pdf:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="6" column="1"> <item row="7" column="1">
<widget class="QComboBox" name="cB_Diff_Pdf"/> <widget class="QComboBox" name="cB_Diff_Pdf"/>
</item> </item>
<item row="7" column="0"> <item row="8" column="0">
<widget class="QLabel" name="label_8"> <widget class="QLabel" name="label_8">
<property name="text"> <property name="text">
<string>Postgres:</string> <string>Postgres:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="7" column="1"> <item row="8" column="1">
<widget class="QComboBox" name="cB_Postgres"/> <widget class="QComboBox" name="cB_Postgres"/>
</item> </item>
<item row="9" column="0">
<widget class="QLabel" name="label_10">
<property name="text">
<string>XSLT-Parameter:</string>
</property>
</widget>
</item>
<item row="9" column="1">
<widget class="QPushButton" name="btnEditXsltParams">
<property name="text">
<string>Bearbeiten ...</string>
</property>
</widget>
</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> </layout>
</widget> </widget>
</item> </item>
+199 -160
View File
@@ -1,160 +1,199 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
################################################################################ ################################################################################
## Form generated from reading UI file 'PdfProject.ui' ## Form generated from reading UI file 'PdfProject.ui'
## ##
## Created by: Qt User Interface Compiler version 6.9.1 ## Created by: Qt User Interface Compiler version 6.10.1
## ##
## WARNING! All changes made in this file will be lost when recompiling UI file! ## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################ ################################################################################
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
QMetaObject, QObject, QPoint, QRect, QMetaObject, QObject, QPoint, QRect,
QSize, QTime, QUrl, Qt) QSize, QTime, QUrl, Qt)
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon, QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter, QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform) QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QAbstractButton, QApplication, QComboBox, QDialog, from PySide6.QtWidgets import (QAbstractButton, QApplication, QComboBox, QDialog,
QDialogButtonBox, QFormLayout, QFrame, QHBoxLayout, QDialogButtonBox, QFormLayout, QFrame, QHBoxLayout,
QLabel, QLineEdit, QPushButton, QSizePolicy, QLabel, QLineEdit, QPushButton, QSizePolicy,
QVBoxLayout, QWidget) QVBoxLayout, QWidget)
class Ui_projectDlg(object): class Ui_projectDlg(object):
def setupUi(self, projectDlg): def setupUi(self, projectDlg):
if not projectDlg.objectName(): if not projectDlg.objectName():
projectDlg.setObjectName(u"projectDlg") projectDlg.setObjectName(u"projectDlg")
projectDlg.resize(608, 299) projectDlg.resize(608, 331)
self.verticalLayout = QVBoxLayout(projectDlg) self.verticalLayout = QVBoxLayout(projectDlg)
self.verticalLayout.setObjectName(u"verticalLayout") self.verticalLayout.setObjectName(u"verticalLayout")
self.widget = QWidget(projectDlg) self.widget = QWidget(projectDlg)
self.widget.setObjectName(u"widget") self.widget.setObjectName(u"widget")
self.formLayout = QFormLayout(self.widget) self.formLayout = QFormLayout(self.widget)
self.formLayout.setObjectName(u"formLayout") self.formLayout.setObjectName(u"formLayout")
self.label = QLabel(self.widget) self.label = QLabel(self.widget)
self.label.setObjectName(u"label") self.label.setObjectName(u"label")
self.formLayout.setWidget(0, QFormLayout.ItemRole.LabelRole, self.label) self.formLayout.setWidget(0, QFormLayout.ItemRole.LabelRole, self.label)
self.lineProjectName = QLineEdit(self.widget) self.lineProjectName = QLineEdit(self.widget)
self.lineProjectName.setObjectName(u"lineProjectName") self.lineProjectName.setObjectName(u"lineProjectName")
self.formLayout.setWidget(0, QFormLayout.ItemRole.FieldRole, self.lineProjectName) self.formLayout.setWidget(0, QFormLayout.ItemRole.FieldRole, self.lineProjectName)
self.label_2 = QLabel(self.widget) self.label_2 = QLabel(self.widget)
self.label_2.setObjectName(u"label_2") self.label_2.setObjectName(u"label_2")
self.formLayout.setWidget(1, QFormLayout.ItemRole.LabelRole, self.label_2) self.formLayout.setWidget(1, QFormLayout.ItemRole.LabelRole, self.label_2)
self.frame = QFrame(self.widget) self.frame = QFrame(self.widget)
self.frame.setObjectName(u"frame") self.frame.setObjectName(u"frame")
self.frame.setFrameShape(QFrame.Shape.StyledPanel) self.frame.setFrameShape(QFrame.Shape.StyledPanel)
self.frame.setFrameShadow(QFrame.Shadow.Raised) self.frame.setFrameShadow(QFrame.Shadow.Raised)
self.horizontalLayout = QHBoxLayout(self.frame) self.horizontalLayout = QHBoxLayout(self.frame)
self.horizontalLayout.setObjectName(u"horizontalLayout") self.horizontalLayout.setObjectName(u"horizontalLayout")
self.horizontalLayout.setContentsMargins(0, 0, 0, 0) self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
self.lineProjectDir = QLineEdit(self.frame) self.lineProjectDir = QLineEdit(self.frame)
self.lineProjectDir.setObjectName(u"lineProjectDir") self.lineProjectDir.setObjectName(u"lineProjectDir")
self.horizontalLayout.addWidget(self.lineProjectDir) self.horizontalLayout.addWidget(self.lineProjectDir)
self.pushButton = QPushButton(self.frame) self.pushButton = QPushButton(self.frame)
self.pushButton.setObjectName(u"pushButton") self.pushButton.setObjectName(u"pushButton")
self.horizontalLayout.addWidget(self.pushButton) self.horizontalLayout.addWidget(self.pushButton)
self.formLayout.setWidget(1, QFormLayout.ItemRole.FieldRole, self.frame) self.formLayout.setWidget(1, QFormLayout.ItemRole.FieldRole, self.frame)
self.label_3 = QLabel(self.widget) self.label_3 = QLabel(self.widget)
self.label_3.setObjectName(u"label_3") self.label_3.setObjectName(u"label_3")
self.formLayout.setWidget(2, QFormLayout.ItemRole.LabelRole, self.label_3) self.formLayout.setWidget(2, QFormLayout.ItemRole.LabelRole, self.label_3)
self.cB_XslDir = QComboBox(self.widget) self.cB_XslDir = QComboBox(self.widget)
self.cB_XslDir.setObjectName(u"cB_XslDir") self.cB_XslDir.setObjectName(u"cB_XslDir")
self.formLayout.setWidget(2, QFormLayout.ItemRole.FieldRole, self.cB_XslDir) self.formLayout.setWidget(2, QFormLayout.ItemRole.FieldRole, self.cB_XslDir)
self.label_4 = QLabel(self.widget) self.label_4 = QLabel(self.widget)
self.label_4.setObjectName(u"label_4") self.label_4.setObjectName(u"label_4")
self.formLayout.setWidget(3, QFormLayout.ItemRole.LabelRole, self.label_4) self.formLayout.setWidget(3, QFormLayout.ItemRole.LabelRole, self.label_4)
self.cB_JavaVm = QComboBox(self.widget) self.cB_JavaVm = QComboBox(self.widget)
self.cB_JavaVm.setObjectName(u"cB_JavaVm") self.cB_JavaVm.setObjectName(u"cB_JavaVm")
self.formLayout.setWidget(3, QFormLayout.ItemRole.FieldRole, self.cB_JavaVm) self.formLayout.setWidget(3, QFormLayout.ItemRole.FieldRole, self.cB_JavaVm)
self.label_5 = QLabel(self.widget) self.label_5 = QLabel(self.widget)
self.label_5.setObjectName(u"label_5") self.label_5.setObjectName(u"label_5")
self.formLayout.setWidget(4, QFormLayout.ItemRole.LabelRole, self.label_5) self.formLayout.setWidget(4, QFormLayout.ItemRole.LabelRole, self.label_5)
self.cB_SaxonJar = QComboBox(self.widget) self.cB_SaxonJar = QComboBox(self.widget)
self.cB_SaxonJar.setObjectName(u"cB_SaxonJar") self.cB_SaxonJar.setObjectName(u"cB_SaxonJar")
self.formLayout.setWidget(4, QFormLayout.ItemRole.FieldRole, self.cB_SaxonJar) self.formLayout.setWidget(4, QFormLayout.ItemRole.FieldRole, self.cB_SaxonJar)
self.label_6 = QLabel(self.widget) self.label_6 = QLabel(self.widget)
self.label_6.setObjectName(u"label_6") self.label_6.setObjectName(u"label_6")
self.formLayout.setWidget(5, QFormLayout.ItemRole.LabelRole, self.label_6) self.formLayout.setWidget(5, QFormLayout.ItemRole.LabelRole, self.label_6)
self.cB_ApacheFop = QComboBox(self.widget) self.cB_ApacheFop = QComboBox(self.widget)
self.cB_ApacheFop.setObjectName(u"cB_ApacheFop") self.cB_ApacheFop.setObjectName(u"cB_ApacheFop")
self.formLayout.setWidget(5, QFormLayout.ItemRole.FieldRole, self.cB_ApacheFop) self.formLayout.setWidget(5, QFormLayout.ItemRole.FieldRole, self.cB_ApacheFop)
self.label_7 = QLabel(self.widget) self.label_9 = QLabel(self.widget)
self.label_7.setObjectName(u"label_7") self.label_9.setObjectName(u"label_9")
self.formLayout.setWidget(6, QFormLayout.ItemRole.LabelRole, self.label_7) self.formLayout.setWidget(6, QFormLayout.ItemRole.LabelRole, self.label_9)
self.cB_Diff_Pdf = QComboBox(self.widget) self.label_7 = QLabel(self.widget)
self.cB_Diff_Pdf.setObjectName(u"cB_Diff_Pdf") self.label_7.setObjectName(u"label_7")
self.formLayout.setWidget(6, QFormLayout.ItemRole.FieldRole, self.cB_Diff_Pdf) self.formLayout.setWidget(7, QFormLayout.ItemRole.LabelRole, self.label_7)
self.label_8 = QLabel(self.widget) self.cB_Diff_Pdf = QComboBox(self.widget)
self.label_8.setObjectName(u"label_8") self.cB_Diff_Pdf.setObjectName(u"cB_Diff_Pdf")
self.formLayout.setWidget(7, QFormLayout.ItemRole.LabelRole, self.label_8) self.formLayout.setWidget(7, QFormLayout.ItemRole.FieldRole, self.cB_Diff_Pdf)
self.cB_Postgres = QComboBox(self.widget) self.label_8 = QLabel(self.widget)
self.cB_Postgres.setObjectName(u"cB_Postgres") self.label_8.setObjectName(u"label_8")
self.formLayout.setWidget(7, QFormLayout.ItemRole.FieldRole, self.cB_Postgres) self.formLayout.setWidget(8, QFormLayout.ItemRole.LabelRole, self.label_8)
self.cB_Postgres = QComboBox(self.widget)
self.verticalLayout.addWidget(self.widget) self.cB_Postgres.setObjectName(u"cB_Postgres")
self.buttonBox = QDialogButtonBox(projectDlg) self.formLayout.setWidget(8, QFormLayout.ItemRole.FieldRole, self.cB_Postgres)
self.buttonBox.setObjectName(u"buttonBox")
self.buttonBox.setOrientation(Qt.Orientation.Horizontal) self.label_10 = QLabel(self.widget)
self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.Ok) self.label_10.setObjectName(u"label_10")
self.buttonBox.setCenterButtons(True)
self.formLayout.setWidget(9, QFormLayout.ItemRole.LabelRole, self.label_10)
self.verticalLayout.addWidget(self.buttonBox)
self.btnEditXsltParams = QPushButton(self.widget)
self.btnEditXsltParams.setObjectName(u"btnEditXsltParams")
self.retranslateUi(projectDlg)
self.buttonBox.accepted.connect(projectDlg.accept) self.formLayout.setWidget(9, QFormLayout.ItemRole.FieldRole, self.btnEditXsltParams)
self.buttonBox.rejected.connect(projectDlg.reject)
self.frame_2 = QFrame(self.widget)
QMetaObject.connectSlotsByName(projectDlg) self.frame_2.setObjectName(u"frame_2")
# setupUi self.frame_2.setFrameShape(QFrame.Shape.StyledPanel)
self.frame_2.setFrameShadow(QFrame.Shadow.Raised)
def retranslateUi(self, projectDlg): self.horizontalLayout_2 = QHBoxLayout(self.frame_2)
projectDlg.setWindowTitle(QCoreApplication.translate("projectDlg", u"PDF-Projekt", None)) self.horizontalLayout_2.setObjectName(u"horizontalLayout_2")
self.label.setText(QCoreApplication.translate("projectDlg", u"Name:", None)) self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0)
self.label_2.setText(QCoreApplication.translate("projectDlg", u"Projekt-Ordner:", None)) self.lineFopConfigDir = QLineEdit(self.frame_2)
self.pushButton.setText(QCoreApplication.translate("projectDlg", u"Durchsuchen ...", None)) self.lineFopConfigDir.setObjectName(u"lineFopConfigDir")
self.label_3.setText(QCoreApplication.translate("projectDlg", u"XSL-Ordner:", None))
self.label_4.setText(QCoreApplication.translate("projectDlg", u"Java VM:", None)) self.horizontalLayout_2.addWidget(self.lineFopConfigDir)
self.label_5.setText(QCoreApplication.translate("projectDlg", u"Saxon Jar:", None))
self.label_6.setText(QCoreApplication.translate("projectDlg", u"Apache FOP:", None)) self.btnBrowseFopConfig = QPushButton(self.frame_2)
self.label_7.setText(QCoreApplication.translate("projectDlg", u"diff-pdf:", None)) self.btnBrowseFopConfig.setObjectName(u"btnBrowseFopConfig")
self.label_8.setText(QCoreApplication.translate("projectDlg", u"Postgres:", None))
# retranslateUi self.horizontalLayout_2.addWidget(self.btnBrowseFopConfig)
self.formLayout.setWidget(6, QFormLayout.ItemRole.FieldRole, self.frame_2)
self.verticalLayout.addWidget(self.widget)
self.buttonBox = QDialogButtonBox(projectDlg)
self.buttonBox.setObjectName(u"buttonBox")
self.buttonBox.setOrientation(Qt.Orientation.Horizontal)
self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.Ok)
self.buttonBox.setCenterButtons(True)
self.verticalLayout.addWidget(self.buttonBox)
self.retranslateUi(projectDlg)
self.buttonBox.accepted.connect(projectDlg.accept)
self.buttonBox.rejected.connect(projectDlg.reject)
QMetaObject.connectSlotsByName(projectDlg)
# setupUi
def retranslateUi(self, projectDlg):
projectDlg.setWindowTitle(QCoreApplication.translate("projectDlg", u"PDF-Projekt", None))
self.label.setText(QCoreApplication.translate("projectDlg", u"Name:", None))
self.label_2.setText(QCoreApplication.translate("projectDlg", u"Projekt-Ordner:", None))
self.pushButton.setText(QCoreApplication.translate("projectDlg", u"Durchsuchen ...", None))
self.label_3.setText(QCoreApplication.translate("projectDlg", u"XSL-Ordner:", None))
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.label_10.setText(QCoreApplication.translate("projectDlg", u"XSLT-Parameter:", None))
self.btnEditXsltParams.setText(QCoreApplication.translate("projectDlg", u"Bearbeiten ...", None))
self.btnBrowseFopConfig.setText(QCoreApplication.translate("projectDlg", u"Durchsuchen ...", None))
# retranslateUi
+14 -7
View File
@@ -3,24 +3,29 @@ from PySide6.QtCore import QThread, Signal, Qt
from ui.PostgreSqlConfigDialog_ui import Ui_PostgreSqlConfigDialog from ui.PostgreSqlConfigDialog_ui import Ui_PostgreSqlConfigDialog
import polars as pl
class DatabaseTestThread(QThread): class DatabaseTestThread(QThread):
"""Thread für den Datenbankverbindungstest.""" """Thread für den Datenbankverbindungstest."""
# Signale für die Kommunikation mit dem UI-Thread # Signale für die Kommunikation mit dem UI-Thread
test_completed = Signal(bool, str) # success, message test_completed = Signal(bool, str) # success, message
def __init__(self, connection_data): def __init__(self, connection_data):
super().__init__() super().__init__()
self.connection_data = connection_data self.connection_data = connection_data
def run(self): def run(self):
"""Führt den Datenbanktest in einem separaten Thread aus.""" """Führt den Datenbanktest in einem separaten Thread aus."""
import polars as pl
try: try:
uri = f"postgresql://{self.connection_data['username']}:{self.connection_data['password']}@{self.connection_data['host']}:{self.connection_data['port']}/{self.connection_data['database']}" timeout = self.connection_data.get("timeout", 10)
uri = (
f"postgresql://{self.connection_data['username']}:{self.connection_data['password']}"
f"@{self.connection_data['host']}:{self.connection_data['port']}"
f"/{self.connection_data['database']}"
f"?connect_timeout={timeout}"
)
# Datenbankverbindung testen # Datenbankverbindung testen
r = pl.read_database_uri( r = pl.read_database_uri(
query="SELECT 1", query="SELECT 1",
@@ -106,6 +111,7 @@ class PostgreSqlConfigDialog(QDialog):
self.ui.usernameEdit.setText(data.get("username", "")) self.ui.usernameEdit.setText(data.get("username", ""))
self.ui.passwordEdit.setText(data.get("password", "")) self.ui.passwordEdit.setText(data.get("password", ""))
self.ui.sslModeComboBox.setCurrentText(data.get("ssl_mode", "prefer")) self.ui.sslModeComboBox.setCurrentText(data.get("ssl_mode", "prefer"))
self.ui.timeoutSpinBox.setValue(data.get("timeout", 10))
def get_data(self): def get_data(self):
"""Gibt die eingegebenen Daten zurück.""" """Gibt die eingegebenen Daten zurück."""
@@ -126,4 +132,5 @@ class PostgreSqlConfigDialog(QDialog):
"username": self.ui.usernameEdit.text().strip(), "username": self.ui.usernameEdit.text().strip(),
"password": self.ui.passwordEdit.text(), # Passwort kann leer sein "password": self.ui.passwordEdit.text(), # Passwort kann leer sein
"ssl_mode": self.ui.sslModeComboBox.currentText(), "ssl_mode": self.ui.sslModeComboBox.currentText(),
"timeout": self.ui.timeoutSpinBox.value(),
} }
+20
View File
@@ -138,7 +138,27 @@
</item> </item>
</widget> </widget>
</item> </item>
<item row="7" column="0">
<widget class="QLabel" name="timeoutLabel">
<property name="text">
<string>Timeout (s):</string>
</property>
</widget>
</item>
<item row="7" column="1"> <item row="7" column="1">
<widget class="QSpinBox" name="timeoutSpinBox">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>300</number>
</property>
<property name="value">
<number>10</number>
</property>
</widget>
</item>
<item row="8" column="1">
<widget class="QPushButton" name="testConnectionButton"> <widget class="QPushButton" name="testConnectionButton">
<property name="text"> <property name="text">
<string>Verbindung testen</string> <string>Verbindung testen</string>
+170 -156
View File
@@ -1,156 +1,170 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
################################################################################ ################################################################################
## Form generated from reading UI file 'PostgreSqlConfigDialog.ui' ## Form generated from reading UI file 'PostgreSqlConfigDialog.ui'
## ##
## Created by: Qt User Interface Compiler version 6.9.1 ## Created by: Qt User Interface Compiler version 6.10.1
## ##
## WARNING! All changes made in this file will be lost when recompiling UI file! ## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################ ################################################################################
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
QMetaObject, QObject, QPoint, QRect, QMetaObject, QObject, QPoint, QRect,
QSize, QTime, QUrl, Qt) QSize, QTime, QUrl, Qt)
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon, QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter, QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform) QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QAbstractButton, QApplication, QComboBox, QDialog, from PySide6.QtWidgets import (QAbstractButton, QApplication, QComboBox, QDialog,
QDialogButtonBox, QFormLayout, QLabel, QLineEdit, QDialogButtonBox, QFormLayout, QLabel, QLineEdit,
QPushButton, QSizePolicy, QSpinBox, QVBoxLayout, QPushButton, QSizePolicy, QSpinBox, QVBoxLayout,
QWidget) QWidget)
class Ui_PostgreSqlConfigDialog(object): class Ui_PostgreSqlConfigDialog(object):
def setupUi(self, PostgreSqlConfigDialog): def setupUi(self, PostgreSqlConfigDialog):
if not PostgreSqlConfigDialog.objectName(): if not PostgreSqlConfigDialog.objectName():
PostgreSqlConfigDialog.setObjectName(u"PostgreSqlConfigDialog") PostgreSqlConfigDialog.setObjectName(u"PostgreSqlConfigDialog")
PostgreSqlConfigDialog.resize(397, 268) PostgreSqlConfigDialog.resize(397, 268)
PostgreSqlConfigDialog.setModal(True) PostgreSqlConfigDialog.setModal(True)
self.verticalLayout = QVBoxLayout(PostgreSqlConfigDialog) self.verticalLayout = QVBoxLayout(PostgreSqlConfigDialog)
self.verticalLayout.setObjectName(u"verticalLayout") self.verticalLayout.setObjectName(u"verticalLayout")
self.formLayout = QFormLayout() self.formLayout = QFormLayout()
self.formLayout.setObjectName(u"formLayout") self.formLayout.setObjectName(u"formLayout")
self.nameLabel = QLabel(PostgreSqlConfigDialog) self.nameLabel = QLabel(PostgreSqlConfigDialog)
self.nameLabel.setObjectName(u"nameLabel") self.nameLabel.setObjectName(u"nameLabel")
self.formLayout.setWidget(0, QFormLayout.ItemRole.LabelRole, self.nameLabel) self.formLayout.setWidget(0, QFormLayout.ItemRole.LabelRole, self.nameLabel)
self.nameEdit = QLineEdit(PostgreSqlConfigDialog) self.nameEdit = QLineEdit(PostgreSqlConfigDialog)
self.nameEdit.setObjectName(u"nameEdit") self.nameEdit.setObjectName(u"nameEdit")
self.formLayout.setWidget(0, QFormLayout.ItemRole.FieldRole, self.nameEdit) self.formLayout.setWidget(0, QFormLayout.ItemRole.FieldRole, self.nameEdit)
self.hostLabel = QLabel(PostgreSqlConfigDialog) self.hostLabel = QLabel(PostgreSqlConfigDialog)
self.hostLabel.setObjectName(u"hostLabel") self.hostLabel.setObjectName(u"hostLabel")
self.formLayout.setWidget(1, QFormLayout.ItemRole.LabelRole, self.hostLabel) self.formLayout.setWidget(1, QFormLayout.ItemRole.LabelRole, self.hostLabel)
self.hostEdit = QLineEdit(PostgreSqlConfigDialog) self.hostEdit = QLineEdit(PostgreSqlConfigDialog)
self.hostEdit.setObjectName(u"hostEdit") self.hostEdit.setObjectName(u"hostEdit")
self.formLayout.setWidget(1, QFormLayout.ItemRole.FieldRole, self.hostEdit) self.formLayout.setWidget(1, QFormLayout.ItemRole.FieldRole, self.hostEdit)
self.portLabel = QLabel(PostgreSqlConfigDialog) self.portLabel = QLabel(PostgreSqlConfigDialog)
self.portLabel.setObjectName(u"portLabel") self.portLabel.setObjectName(u"portLabel")
self.formLayout.setWidget(2, QFormLayout.ItemRole.LabelRole, self.portLabel) self.formLayout.setWidget(2, QFormLayout.ItemRole.LabelRole, self.portLabel)
self.portSpinBox = QSpinBox(PostgreSqlConfigDialog) self.portSpinBox = QSpinBox(PostgreSqlConfigDialog)
self.portSpinBox.setObjectName(u"portSpinBox") self.portSpinBox.setObjectName(u"portSpinBox")
self.portSpinBox.setMinimum(1) self.portSpinBox.setMinimum(1)
self.portSpinBox.setMaximum(65535) self.portSpinBox.setMaximum(65535)
self.portSpinBox.setValue(5432) self.portSpinBox.setValue(5432)
self.formLayout.setWidget(2, QFormLayout.ItemRole.FieldRole, self.portSpinBox) self.formLayout.setWidget(2, QFormLayout.ItemRole.FieldRole, self.portSpinBox)
self.databaseLabel = QLabel(PostgreSqlConfigDialog) self.databaseLabel = QLabel(PostgreSqlConfigDialog)
self.databaseLabel.setObjectName(u"databaseLabel") self.databaseLabel.setObjectName(u"databaseLabel")
self.formLayout.setWidget(3, QFormLayout.ItemRole.LabelRole, self.databaseLabel) self.formLayout.setWidget(3, QFormLayout.ItemRole.LabelRole, self.databaseLabel)
self.databaseEdit = QLineEdit(PostgreSqlConfigDialog) self.databaseEdit = QLineEdit(PostgreSqlConfigDialog)
self.databaseEdit.setObjectName(u"databaseEdit") self.databaseEdit.setObjectName(u"databaseEdit")
self.formLayout.setWidget(3, QFormLayout.ItemRole.FieldRole, self.databaseEdit) self.formLayout.setWidget(3, QFormLayout.ItemRole.FieldRole, self.databaseEdit)
self.usernameLabel = QLabel(PostgreSqlConfigDialog) self.usernameLabel = QLabel(PostgreSqlConfigDialog)
self.usernameLabel.setObjectName(u"usernameLabel") self.usernameLabel.setObjectName(u"usernameLabel")
self.formLayout.setWidget(4, QFormLayout.ItemRole.LabelRole, self.usernameLabel) self.formLayout.setWidget(4, QFormLayout.ItemRole.LabelRole, self.usernameLabel)
self.usernameEdit = QLineEdit(PostgreSqlConfigDialog) self.usernameEdit = QLineEdit(PostgreSqlConfigDialog)
self.usernameEdit.setObjectName(u"usernameEdit") self.usernameEdit.setObjectName(u"usernameEdit")
self.formLayout.setWidget(4, QFormLayout.ItemRole.FieldRole, self.usernameEdit) self.formLayout.setWidget(4, QFormLayout.ItemRole.FieldRole, self.usernameEdit)
self.passwordLabel = QLabel(PostgreSqlConfigDialog) self.passwordLabel = QLabel(PostgreSqlConfigDialog)
self.passwordLabel.setObjectName(u"passwordLabel") self.passwordLabel.setObjectName(u"passwordLabel")
self.formLayout.setWidget(5, QFormLayout.ItemRole.LabelRole, self.passwordLabel) self.formLayout.setWidget(5, QFormLayout.ItemRole.LabelRole, self.passwordLabel)
self.passwordEdit = QLineEdit(PostgreSqlConfigDialog) self.passwordEdit = QLineEdit(PostgreSqlConfigDialog)
self.passwordEdit.setObjectName(u"passwordEdit") self.passwordEdit.setObjectName(u"passwordEdit")
self.passwordEdit.setEchoMode(QLineEdit.EchoMode.Password) self.passwordEdit.setEchoMode(QLineEdit.EchoMode.Password)
self.formLayout.setWidget(5, QFormLayout.ItemRole.FieldRole, self.passwordEdit) self.formLayout.setWidget(5, QFormLayout.ItemRole.FieldRole, self.passwordEdit)
self.sslModeLabel = QLabel(PostgreSqlConfigDialog) self.sslModeLabel = QLabel(PostgreSqlConfigDialog)
self.sslModeLabel.setObjectName(u"sslModeLabel") self.sslModeLabel.setObjectName(u"sslModeLabel")
self.formLayout.setWidget(6, QFormLayout.ItemRole.LabelRole, self.sslModeLabel) self.formLayout.setWidget(6, QFormLayout.ItemRole.LabelRole, self.sslModeLabel)
self.sslModeComboBox = QComboBox(PostgreSqlConfigDialog) self.sslModeComboBox = QComboBox(PostgreSqlConfigDialog)
self.sslModeComboBox.addItem("") self.sslModeComboBox.addItem("")
self.sslModeComboBox.addItem("") self.sslModeComboBox.addItem("")
self.sslModeComboBox.addItem("") self.sslModeComboBox.addItem("")
self.sslModeComboBox.addItem("") self.sslModeComboBox.addItem("")
self.sslModeComboBox.addItem("") self.sslModeComboBox.addItem("")
self.sslModeComboBox.addItem("") self.sslModeComboBox.addItem("")
self.sslModeComboBox.setObjectName(u"sslModeComboBox") self.sslModeComboBox.setObjectName(u"sslModeComboBox")
self.formLayout.setWidget(6, QFormLayout.ItemRole.FieldRole, self.sslModeComboBox) self.formLayout.setWidget(6, QFormLayout.ItemRole.FieldRole, self.sslModeComboBox)
self.testConnectionButton = QPushButton(PostgreSqlConfigDialog) self.timeoutLabel = QLabel(PostgreSqlConfigDialog)
self.testConnectionButton.setObjectName(u"testConnectionButton") self.timeoutLabel.setObjectName(u"timeoutLabel")
self.formLayout.setWidget(7, QFormLayout.ItemRole.FieldRole, self.testConnectionButton) self.formLayout.setWidget(7, QFormLayout.ItemRole.LabelRole, self.timeoutLabel)
self.timeoutSpinBox = QSpinBox(PostgreSqlConfigDialog)
self.verticalLayout.addLayout(self.formLayout) self.timeoutSpinBox.setObjectName(u"timeoutSpinBox")
self.timeoutSpinBox.setMinimum(1)
self.buttonBox = QDialogButtonBox(PostgreSqlConfigDialog) self.timeoutSpinBox.setMaximum(300)
self.buttonBox.setObjectName(u"buttonBox") self.timeoutSpinBox.setValue(10)
self.buttonBox.setOrientation(Qt.Orientation.Horizontal)
self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.Ok) self.formLayout.setWidget(7, QFormLayout.ItemRole.FieldRole, self.timeoutSpinBox)
self.buttonBox.setCenterButtons(True)
self.testConnectionButton = QPushButton(PostgreSqlConfigDialog)
self.verticalLayout.addWidget(self.buttonBox) self.testConnectionButton.setObjectName(u"testConnectionButton")
self.formLayout.setWidget(8, QFormLayout.ItemRole.FieldRole, self.testConnectionButton)
self.retranslateUi(PostgreSqlConfigDialog)
self.buttonBox.accepted.connect(PostgreSqlConfigDialog.accept)
self.buttonBox.rejected.connect(PostgreSqlConfigDialog.reject) self.verticalLayout.addLayout(self.formLayout)
QMetaObject.connectSlotsByName(PostgreSqlConfigDialog) self.buttonBox = QDialogButtonBox(PostgreSqlConfigDialog)
# setupUi self.buttonBox.setObjectName(u"buttonBox")
self.buttonBox.setOrientation(Qt.Orientation.Horizontal)
def retranslateUi(self, PostgreSqlConfigDialog): self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.Ok)
PostgreSqlConfigDialog.setWindowTitle(QCoreApplication.translate("PostgreSqlConfigDialog", u"PostgreSQL Datenbank Konfiguration", None)) self.buttonBox.setCenterButtons(True)
self.nameLabel.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"Name:", None))
self.hostLabel.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"Host:", None)) self.verticalLayout.addWidget(self.buttonBox)
self.hostEdit.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"localhost", None))
self.portLabel.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"Port:", None))
self.databaseLabel.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"Datenbank:", None)) self.retranslateUi(PostgreSqlConfigDialog)
self.usernameLabel.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"Benutzername:", None)) self.buttonBox.accepted.connect(PostgreSqlConfigDialog.accept)
self.passwordLabel.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"Passwort:", None)) self.buttonBox.rejected.connect(PostgreSqlConfigDialog.reject)
self.sslModeLabel.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"SSL-Modus:", None))
self.sslModeComboBox.setItemText(0, QCoreApplication.translate("PostgreSqlConfigDialog", u"disable", None)) QMetaObject.connectSlotsByName(PostgreSqlConfigDialog)
self.sslModeComboBox.setItemText(1, QCoreApplication.translate("PostgreSqlConfigDialog", u"allow", None)) # setupUi
self.sslModeComboBox.setItemText(2, QCoreApplication.translate("PostgreSqlConfigDialog", u"prefer", None))
self.sslModeComboBox.setItemText(3, QCoreApplication.translate("PostgreSqlConfigDialog", u"require", None)) def retranslateUi(self, PostgreSqlConfigDialog):
self.sslModeComboBox.setItemText(4, QCoreApplication.translate("PostgreSqlConfigDialog", u"verify-ca", None)) PostgreSqlConfigDialog.setWindowTitle(QCoreApplication.translate("PostgreSqlConfigDialog", u"PostgreSQL Datenbank Konfiguration", None))
self.sslModeComboBox.setItemText(5, QCoreApplication.translate("PostgreSqlConfigDialog", u"verify-full", None)) self.nameLabel.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"Name:", None))
self.hostLabel.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"Host:", None))
self.testConnectionButton.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"Verbindung testen", None)) self.hostEdit.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"localhost", None))
# retranslateUi self.portLabel.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"Port:", None))
self.databaseLabel.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"Datenbank:", None))
self.usernameLabel.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"Benutzername:", None))
self.passwordLabel.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"Passwort:", None))
self.sslModeLabel.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"SSL-Modus:", None))
self.sslModeComboBox.setItemText(0, QCoreApplication.translate("PostgreSqlConfigDialog", u"disable", None))
self.sslModeComboBox.setItemText(1, QCoreApplication.translate("PostgreSqlConfigDialog", u"allow", None))
self.sslModeComboBox.setItemText(2, QCoreApplication.translate("PostgreSqlConfigDialog", u"prefer", None))
self.sslModeComboBox.setItemText(3, QCoreApplication.translate("PostgreSqlConfigDialog", u"require", None))
self.sslModeComboBox.setItemText(4, QCoreApplication.translate("PostgreSqlConfigDialog", u"verify-ca", None))
self.sslModeComboBox.setItemText(5, QCoreApplication.translate("PostgreSqlConfigDialog", u"verify-full", None))
self.timeoutLabel.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"Timeout (s):", None))
self.testConnectionButton.setText(QCoreApplication.translate("PostgreSqlConfigDialog", u"Verbindung testen", None))
# retranslateUi
+68
View File
@@ -0,0 +1,68 @@
from PySide6.QtWidgets import QDialog, QTableWidgetItem
from ui.ProjectXsltParamsDialog_ui import Ui_ProjectXsltParamsDialog
from icons import icon
class ProjectXsltParamsDialog(QDialog):
"""Dialog zur Bearbeitung projektweiter XSLT-Parameter."""
def __init__(self, parent=None, xslt_params: dict[str, str] | None = None):
super().__init__(parent)
self.ui = Ui_ProjectXsltParamsDialog()
self.ui.setupUi(self)
self.ui.addParamButton.setIcon(icon("plus-circle"))
self.ui.removeParamButton.setIcon(icon("minus-circle"))
self.ui.addParamButton.clicked.connect(self._add_parameter)
self.ui.removeParamButton.clicked.connect(self._remove_parameter)
self._setup_table()
if xslt_params:
self._load_params(xslt_params)
def _setup_table(self):
"""Konfiguriert die Tabelle."""
self.ui.xsltParamsTable.setColumnWidth(0, 200)
self.ui.xsltParamsTable.setColumnWidth(1, 300)
self.ui.xsltParamsTable.horizontalHeader().setStretchLastSection(True)
def _load_params(self, params: dict[str, str]):
"""Lädt die XSLT-Parameter in die Tabelle."""
self.ui.xsltParamsTable.setRowCount(len(params))
for row, (key, value) in enumerate(params.items()):
self.ui.xsltParamsTable.setItem(row, 0, QTableWidgetItem(str(key)))
self.ui.xsltParamsTable.setItem(row, 1, QTableWidgetItem(str(value)))
def _add_parameter(self):
"""Fügt einen neuen Parameter hinzu."""
row_count = self.ui.xsltParamsTable.rowCount()
self.ui.xsltParamsTable.insertRow(row_count)
self.ui.xsltParamsTable.setItem(row_count, 0, QTableWidgetItem(""))
self.ui.xsltParamsTable.setItem(row_count, 1, QTableWidgetItem(""))
self.ui.xsltParamsTable.setCurrentCell(row_count, 0)
def _remove_parameter(self):
"""Entfernt den ausgewählten Parameter."""
current_row = self.ui.xsltParamsTable.currentRow()
if current_row >= 0:
self.ui.xsltParamsTable.removeRow(current_row)
def get_params(self) -> dict[str, str]:
"""
Gibt die bearbeiteten XSLT-Parameter zurück.
Returns:
dict[str, str]: Dictionary mit allen XSLT-Parametern
"""
params = {}
for row in range(self.ui.xsltParamsTable.rowCount()):
key_item = self.ui.xsltParamsTable.item(row, 0)
value_item = self.ui.xsltParamsTable.item(row, 1)
if key_item and value_item:
key = key_item.text().strip()
if key:
params[key] = value_item.text().strip()
return params
+164
View File
@@ -0,0 +1,164 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ProjectXsltParamsDialog</class>
<widget class="QDialog" name="ProjectXsltParamsDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>600</width>
<height>400</height>
</rect>
</property>
<property name="windowTitle">
<string>Projektweite XSLT-Parameter</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="xsltParamsGroupBox">
<property name="title">
<string>XSLT-Parameter</string>
</property>
<layout class="QVBoxLayout" name="xsltParamsLayout">
<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="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_left">
<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_right">
<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="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set>
</property>
<property name="centerButtons">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>ProjectXsltParamsDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>ProjectXsltParamsDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>
+106
View File
@@ -0,0 +1,106 @@
# -*- coding: utf-8 -*-
################################################################################
## Form generated from reading UI file 'ProjectXsltParamsDialog.ui'
##
## Created by: Qt User Interface Compiler version 6.10.1
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
QMetaObject, QObject, QPoint, QRect,
QSize, QTime, QUrl, Qt)
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, QDialog, QDialogButtonBox,
QFrame, QGroupBox, QHBoxLayout, QHeaderView,
QPushButton, QSizePolicy, QSpacerItem, QTableWidget,
QTableWidgetItem, QVBoxLayout, QWidget)
class Ui_ProjectXsltParamsDialog(object):
def setupUi(self, ProjectXsltParamsDialog):
if not ProjectXsltParamsDialog.objectName():
ProjectXsltParamsDialog.setObjectName(u"ProjectXsltParamsDialog")
ProjectXsltParamsDialog.resize(600, 400)
ProjectXsltParamsDialog.setModal(True)
self.verticalLayout = QVBoxLayout(ProjectXsltParamsDialog)
self.verticalLayout.setObjectName(u"verticalLayout")
self.xsltParamsGroupBox = QGroupBox(ProjectXsltParamsDialog)
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)
__qtablewidgetitem = QTableWidgetItem()
self.xsltParamsTable.setHorizontalHeaderItem(0, __qtablewidgetitem)
__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)
self.xsltParamsLayout.addWidget(self.xsltParamsTable)
self.xsltParamsButtonLayout = QHBoxLayout()
self.xsltParamsButtonLayout.setObjectName(u"xsltParamsButtonLayout")
self.horizontalSpacer_left = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
self.xsltParamsButtonLayout.addItem(self.horizontalSpacer_left)
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)
self.horizontalSpacer_right = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
self.xsltParamsButtonLayout.addItem(self.horizontalSpacer_right)
self.xsltParamsLayout.addLayout(self.xsltParamsButtonLayout)
self.verticalLayout.addWidget(self.xsltParamsGroupBox)
self.buttonBox = QDialogButtonBox(ProjectXsltParamsDialog)
self.buttonBox.setObjectName(u"buttonBox")
self.buttonBox.setOrientation(Qt.Orientation.Horizontal)
self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.Ok)
self.buttonBox.setCenterButtons(True)
self.verticalLayout.addWidget(self.buttonBox)
self.retranslateUi(ProjectXsltParamsDialog)
self.buttonBox.accepted.connect(ProjectXsltParamsDialog.accept)
self.buttonBox.rejected.connect(ProjectXsltParamsDialog.reject)
QMetaObject.connectSlotsByName(ProjectXsltParamsDialog)
# setupUi
def retranslateUi(self, ProjectXsltParamsDialog):
ProjectXsltParamsDialog.setWindowTitle(QCoreApplication.translate("ProjectXsltParamsDialog", u"Projektweite XSLT-Parameter", None))
self.xsltParamsGroupBox.setTitle(QCoreApplication.translate("ProjectXsltParamsDialog", u"XSLT-Parameter", None))
___qtablewidgetitem = self.xsltParamsTable.horizontalHeaderItem(0)
___qtablewidgetitem.setText(QCoreApplication.translate("ProjectXsltParamsDialog", u"Parameter", None));
___qtablewidgetitem1 = self.xsltParamsTable.horizontalHeaderItem(1)
___qtablewidgetitem1.setText(QCoreApplication.translate("ProjectXsltParamsDialog", u"Wert", None));
self.addParamButton.setText(QCoreApplication.translate("ProjectXsltParamsDialog", u"Parameter hinzuf\u00fcgen", None))
self.removeParamButton.setText(QCoreApplication.translate("ProjectXsltParamsDialog", u"Parameter entfernen", None))
# retranslateUi
+4 -153
View File
@@ -1,158 +1,9 @@
from PySide6.QtWidgets import QDialog, QTableWidgetItem, QMessageBox
from PySide6.QtCore import Qt
from ui.TreeNodeEditDialog_ui import Ui_TreeNodeEditDialog from ui.TreeNodeEditDialog_ui import Ui_TreeNodeEditDialog
from conf import TreeNode from ui.XsltParamsEditDialog import XsltParamsEditDialog
class TreeNodeEditDialog(QDialog): class TreeNodeEditDialog(XsltParamsEditDialog):
"""Dialog zur Bearbeitung von TreeNode-Objekten.""" """Dialog zur Bearbeitung von TreeNode-Objekten."""
def __init__(self, parent=None, node=None, parent_params=None): def _create_ui(self):
""" return Ui_TreeNodeEditDialog()
Initialisiert den Dialog.
Args:
parent: Übergeordnetes Widget
node: TreeNode-Objekt zum Bearbeiten
parent_params: Dictionary mit Eltern-Parametern (nur anzeigen)
"""
super().__init__(parent)
# UI einrichten
self.ui = Ui_TreeNodeEditDialog()
self.ui.setupUi(self)
# Node-Objekt speichern
self.node = node
self.parent_params = parent_params or {}
# Signale verbinden
self.ui.addParamButton.clicked.connect(self.add_parameter)
self.ui.removeParamButton.clicked.connect(self.remove_parameter)
# Tabellen konfigurieren
self._setup_tables()
# Daten laden
if self.node:
self._load_data()
def _setup_tables(self):
"""Konfiguriert die Tabellen."""
# XSLT Parameter Tabelle
self.ui.xsltParamsTable.setColumnWidth(0, 200)
self.ui.xsltParamsTable.setColumnWidth(1, 300)
self.ui.xsltParamsTable.horizontalHeader().setStretchLastSection(True)
# Eltern-Parameter Tabelle
self.ui.parentParamsTable.setColumnWidth(0, 200)
self.ui.parentParamsTable.setColumnWidth(1, 300)
self.ui.parentParamsTable.horizontalHeader().setStretchLastSection(True)
def _load_data(self):
"""Lädt die Daten des TreeNode in den Dialog."""
if not self.node:
return
# Bezeichnung setzen
self.ui.bezEdit.setText(str(self.node.bez) if self.node.bez else "")
# XSLT Parameter laden
self._load_xslt_params()
# Eltern-Parameter laden
self._load_parent_params()
def _load_xslt_params(self):
"""Lädt die XSLT Parameter in die Tabelle."""
if not self.node or not self.node.xslt_params:
return
params = self.node.xslt_params
self.ui.xsltParamsTable.setRowCount(len(params))
for row, (key, value) in enumerate(params.items()):
# Parameter-Name
key_item = QTableWidgetItem(str(key))
self.ui.xsltParamsTable.setItem(row, 0, key_item)
# Parameter-Wert
value_item = QTableWidgetItem(str(value))
self.ui.xsltParamsTable.setItem(row, 1, value_item)
def _load_parent_params(self):
"""Lädt die Eltern-Parameter in die Tabelle (nur anzeigen)."""
if not self.parent_params:
return
self.ui.parentParamsTable.setRowCount(len(self.parent_params))
for row, (key, value) in enumerate(self.parent_params.items()):
# Parameter-Name
key_item = QTableWidgetItem(str(key))
key_item.setFlags(key_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
self.ui.parentParamsTable.setItem(row, 0, key_item)
# Parameter-Wert
value_item = QTableWidgetItem(str(value))
value_item.setFlags(value_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
self.ui.parentParamsTable.setItem(row, 1, value_item)
def add_parameter(self):
"""Fügt einen neuen Parameter hinzu."""
row_count = self.ui.xsltParamsTable.rowCount()
self.ui.xsltParamsTable.insertRow(row_count)
# Leere Items hinzufügen
key_item = QTableWidgetItem("")
value_item = QTableWidgetItem("")
self.ui.xsltParamsTable.setItem(row_count, 0, key_item)
self.ui.xsltParamsTable.setItem(row_count, 1, value_item)
# Fokus auf den neuen Parameter setzen
self.ui.xsltParamsTable.setCurrentCell(row_count, 0)
def remove_parameter(self):
"""Entfernt den ausgewählten Parameter."""
current_row = self.ui.xsltParamsTable.currentRow()
if current_row >= 0:
self.ui.xsltParamsTable.removeRow(current_row)
def get_data(self):
"""
Gibt die bearbeiteten Daten zurück.
Returns:
dict: Dictionary mit den bearbeiteten Daten oder None bei Fehler
"""
# Bezeichnung prüfen
bez = self.ui.bezEdit.text().strip()
if not bez:
QMessageBox.warning(self, "Warnung", "Bitte geben Sie eine Bezeichnung ein.")
return None
# XSLT Parameter sammeln
xslt_params = {}
for row in range(self.ui.xsltParamsTable.rowCount()):
key_item = self.ui.xsltParamsTable.item(row, 0)
value_item = self.ui.xsltParamsTable.item(row, 1)
if key_item and value_item:
key = key_item.text().strip()
value = value_item.text().strip()
if key: # Nur Parameter mit nicht-leerem Schlüssel hinzufügen
xslt_params[key] = value
return {
"bez": bez,
"xslt_params": xslt_params
}
def accept(self):
"""Überschreibt accept() um Datenvalidierung durchzuführen."""
data = self.get_data()
if data is not None:
super().accept()
+156 -76
View File
@@ -6,7 +6,7 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>600</width> <width>870</width>
<height>400</height> <height>400</height>
</rect> </rect>
</property> </property>
@@ -35,95 +35,175 @@
</layout> </layout>
</item> </item>
<item> <item>
<widget class="QGroupBox" name="xsltParamsGroupBox"> <widget class="QFrame" name="frame">
<property name="title"> <property name="frameShape">
<string>XSLT-Parameter</string> <enum>QFrame::Shape::NoFrame</enum>
</property> </property>
<layout class="QVBoxLayout" name="xsltParamsLayout"> <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>
<item> <item>
<widget class="QTableWidget" name="xsltParamsTable"> <widget class="QGroupBox" name="xsltParamsGroupBox">
<property name="columnCount"> <property name="title">
<number>2</number> <string>XSLT-Parameter</string>
</property> </property>
<attribute name="horizontalHeaderVisible"> <layout class="QVBoxLayout" name="xsltParamsLayout">
<bool>true</bool> <property name="leftMargin">
</attribute> <number>0</number>
<column>
<property name="text">
<string>Parameter</string>
</property> </property>
</column> <property name="topMargin">
<column> <number>0</number>
<property name="text">
<string>Wert</string>
</property> </property>
</column> <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>
</widget> </widget>
</item> </item>
<item> <item>
<layout class="QHBoxLayout" name="xsltParamsButtonLayout"> <widget class="QGroupBox" name="parentParamsGroupBox">
<item> <property name="title">
<widget class="QPushButton" name="addParamButton"> <string>Geerbte XSLT-Parameter (nur anzeigen)</string>
<property name="text"> </property>
<string>Parameter hinzufügen</string> <layout class="QVBoxLayout" name="parentParamsLayout">
</property> <property name="leftMargin">
</widget> <number>0</number>
</item> </property>
<item> <property name="topMargin">
<widget class="QPushButton" name="removeParamButton"> <number>0</number>
<property name="text"> </property>
<string>Parameter entfernen</string> <property name="rightMargin">
</property> <number>0</number>
</widget> </property>
</item> <property name="bottomMargin">
<item> <number>0</number>
<spacer name="horizontalSpacer"> </property>
<property name="orientation"> <item>
<enum>Qt::Orientation::Horizontal</enum> <widget class="QTableWidget" name="parentParamsTable">
</property> <property name="frameShape">
<property name="sizeHint" stdset="0"> <enum>QFrame::Shape::NoFrame</enum>
<size> </property>
<width>40</width> <property name="editTriggers">
<height>20</height> <set>QAbstractItemView::EditTrigger::NoEditTriggers</set>
</size> </property>
</property> <property name="columnCount">
</spacer> <number>2</number>
</item> </property>
</layout> <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>
</layout> </layout>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QGroupBox" name="parentParamsGroupBox"> <widget class="QCheckBox" name="alle_xml_transformieren">
<property name="title"> <property name="text">
<string>Geerbte XSLT-Parameter (nur anzeigen)</string> <string>Alle XML-Dateien neu transformieren (force)</string>
</property> </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> </widget>
</item> </item>
<item> <item>
+169 -140
View File
@@ -1,140 +1,169 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
################################################################################ ################################################################################
## Form generated from reading UI file 'TreeNodeEditDialog.ui' ## Form generated from reading UI file 'TreeNodeEditDialog.ui'
## ##
## Created by: Qt User Interface Compiler version 6.9.1 ## Created by: Qt User Interface Compiler version 6.9.2
## ##
## WARNING! All changes made in this file will be lost when recompiling UI file! ## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################ ################################################################################
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
QMetaObject, QObject, QPoint, QRect, QMetaObject, QObject, QPoint, QRect,
QSize, QTime, QUrl, Qt) QSize, QTime, QUrl, Qt)
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon, QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter, QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform) QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QAbstractButton, QAbstractItemView, QApplication, QDialog, from PySide6.QtWidgets import (QAbstractButton, QAbstractItemView, QApplication, QCheckBox,
QDialogButtonBox, QFormLayout, QGroupBox, QHBoxLayout, QDialog, QDialogButtonBox, QFormLayout, QFrame,
QHeaderView, QLabel, QLayout, QLineEdit, QGroupBox, QHBoxLayout, QHeaderView, QLabel,
QPushButton, QSizePolicy, QSpacerItem, QTableWidget, QLayout, QLineEdit, QPushButton, QSizePolicy,
QTableWidgetItem, QVBoxLayout, QWidget) QSpacerItem, QTableWidget, QTableWidgetItem, QVBoxLayout,
QWidget)
class Ui_TreeNodeEditDialog(object):
def setupUi(self, TreeNodeEditDialog): class Ui_TreeNodeEditDialog(object):
if not TreeNodeEditDialog.objectName(): def setupUi(self, TreeNodeEditDialog):
TreeNodeEditDialog.setObjectName(u"TreeNodeEditDialog") if not TreeNodeEditDialog.objectName():
TreeNodeEditDialog.resize(600, 400) TreeNodeEditDialog.setObjectName(u"TreeNodeEditDialog")
TreeNodeEditDialog.setModal(True) TreeNodeEditDialog.resize(870, 400)
self.verticalLayout = QVBoxLayout(TreeNodeEditDialog) TreeNodeEditDialog.setModal(True)
self.verticalLayout.setObjectName(u"verticalLayout") self.verticalLayout = QVBoxLayout(TreeNodeEditDialog)
self.formLayout = QFormLayout() self.verticalLayout.setObjectName(u"verticalLayout")
self.formLayout.setObjectName(u"formLayout") self.formLayout = QFormLayout()
self.formLayout.setSizeConstraint(QLayout.SizeConstraint.SetMaximumSize) self.formLayout.setObjectName(u"formLayout")
self.bezLabel = QLabel(TreeNodeEditDialog) self.formLayout.setSizeConstraint(QLayout.SizeConstraint.SetMaximumSize)
self.bezLabel.setObjectName(u"bezLabel") self.bezLabel = QLabel(TreeNodeEditDialog)
self.bezLabel.setObjectName(u"bezLabel")
self.formLayout.setWidget(0, QFormLayout.ItemRole.LabelRole, self.bezLabel)
self.formLayout.setWidget(0, QFormLayout.ItemRole.LabelRole, self.bezLabel)
self.bezEdit = QLineEdit(TreeNodeEditDialog)
self.bezEdit.setObjectName(u"bezEdit") self.bezEdit = QLineEdit(TreeNodeEditDialog)
self.bezEdit.setObjectName(u"bezEdit")
self.formLayout.setWidget(0, QFormLayout.ItemRole.FieldRole, self.bezEdit)
self.formLayout.setWidget(0, QFormLayout.ItemRole.FieldRole, self.bezEdit)
self.verticalLayout.addLayout(self.formLayout)
self.verticalLayout.addLayout(self.formLayout)
self.xsltParamsGroupBox = QGroupBox(TreeNodeEditDialog)
self.xsltParamsGroupBox.setObjectName(u"xsltParamsGroupBox") self.frame = QFrame(TreeNodeEditDialog)
self.xsltParamsLayout = QVBoxLayout(self.xsltParamsGroupBox) self.frame.setObjectName(u"frame")
self.xsltParamsLayout.setObjectName(u"xsltParamsLayout") self.frame.setFrameShape(QFrame.Shape.NoFrame)
self.xsltParamsTable = QTableWidget(self.xsltParamsGroupBox) self.frame.setFrameShadow(QFrame.Shadow.Raised)
if (self.xsltParamsTable.columnCount() < 2): self.horizontalLayout = QHBoxLayout(self.frame)
self.xsltParamsTable.setColumnCount(2) self.horizontalLayout.setObjectName(u"horizontalLayout")
__qtablewidgetitem = QTableWidgetItem() self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
self.xsltParamsTable.setHorizontalHeaderItem(0, __qtablewidgetitem) self.xsltParamsGroupBox = QGroupBox(self.frame)
__qtablewidgetitem1 = QTableWidgetItem() self.xsltParamsGroupBox.setObjectName(u"xsltParamsGroupBox")
self.xsltParamsTable.setHorizontalHeaderItem(1, __qtablewidgetitem1) self.xsltParamsLayout = QVBoxLayout(self.xsltParamsGroupBox)
self.xsltParamsTable.setObjectName(u"xsltParamsTable") self.xsltParamsLayout.setObjectName(u"xsltParamsLayout")
self.xsltParamsTable.setColumnCount(2) self.xsltParamsLayout.setContentsMargins(0, 0, 0, 0)
self.xsltParamsTable.horizontalHeader().setVisible(True) self.xsltParamsTable = QTableWidget(self.xsltParamsGroupBox)
if (self.xsltParamsTable.columnCount() < 2):
self.xsltParamsLayout.addWidget(self.xsltParamsTable) self.xsltParamsTable.setColumnCount(2)
__qtablewidgetitem = QTableWidgetItem()
self.xsltParamsButtonLayout = QHBoxLayout() self.xsltParamsTable.setHorizontalHeaderItem(0, __qtablewidgetitem)
self.xsltParamsButtonLayout.setObjectName(u"xsltParamsButtonLayout") __qtablewidgetitem1 = QTableWidgetItem()
self.addParamButton = QPushButton(self.xsltParamsGroupBox) self.xsltParamsTable.setHorizontalHeaderItem(1, __qtablewidgetitem1)
self.addParamButton.setObjectName(u"addParamButton") self.xsltParamsTable.setObjectName(u"xsltParamsTable")
self.xsltParamsTable.setFrameShape(QFrame.Shape.NoFrame)
self.xsltParamsButtonLayout.addWidget(self.addParamButton) self.xsltParamsTable.setColumnCount(2)
self.xsltParamsTable.horizontalHeader().setVisible(True)
self.removeParamButton = QPushButton(self.xsltParamsGroupBox)
self.removeParamButton.setObjectName(u"removeParamButton") self.xsltParamsLayout.addWidget(self.xsltParamsTable)
self.xsltParamsButtonLayout.addWidget(self.removeParamButton) self.xsltParamsButtonLayout = QHBoxLayout()
self.xsltParamsButtonLayout.setObjectName(u"xsltParamsButtonLayout")
self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) self.horizontalSpacer_2 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
self.xsltParamsButtonLayout.addItem(self.horizontalSpacer) self.xsltParamsButtonLayout.addItem(self.horizontalSpacer_2)
self.addParamButton = QPushButton(self.xsltParamsGroupBox)
self.xsltParamsLayout.addLayout(self.xsltParamsButtonLayout) self.addParamButton.setObjectName(u"addParamButton")
icon = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.ListAdd))
self.addParamButton.setIcon(icon)
self.verticalLayout.addWidget(self.xsltParamsGroupBox)
self.xsltParamsButtonLayout.addWidget(self.addParamButton)
self.parentParamsGroupBox = QGroupBox(TreeNodeEditDialog)
self.parentParamsGroupBox.setObjectName(u"parentParamsGroupBox") self.removeParamButton = QPushButton(self.xsltParamsGroupBox)
self.parentParamsLayout = QVBoxLayout(self.parentParamsGroupBox) self.removeParamButton.setObjectName(u"removeParamButton")
self.parentParamsLayout.setObjectName(u"parentParamsLayout") icon1 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.ListRemove))
self.parentParamsTable = QTableWidget(self.parentParamsGroupBox) self.removeParamButton.setIcon(icon1)
if (self.parentParamsTable.columnCount() < 2):
self.parentParamsTable.setColumnCount(2) self.xsltParamsButtonLayout.addWidget(self.removeParamButton)
__qtablewidgetitem2 = QTableWidgetItem()
self.parentParamsTable.setHorizontalHeaderItem(0, __qtablewidgetitem2) self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
__qtablewidgetitem3 = QTableWidgetItem()
self.parentParamsTable.setHorizontalHeaderItem(1, __qtablewidgetitem3) self.xsltParamsButtonLayout.addItem(self.horizontalSpacer)
self.parentParamsTable.setObjectName(u"parentParamsTable")
self.parentParamsTable.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
self.parentParamsTable.setColumnCount(2) self.xsltParamsLayout.addLayout(self.xsltParamsButtonLayout)
self.parentParamsTable.horizontalHeader().setVisible(True)
self.parentParamsLayout.addWidget(self.parentParamsTable) self.horizontalLayout.addWidget(self.xsltParamsGroupBox)
self.parentParamsGroupBox = QGroupBox(self.frame)
self.verticalLayout.addWidget(self.parentParamsGroupBox) self.parentParamsGroupBox.setObjectName(u"parentParamsGroupBox")
self.parentParamsLayout = QVBoxLayout(self.parentParamsGroupBox)
self.buttonBox = QDialogButtonBox(TreeNodeEditDialog) self.parentParamsLayout.setObjectName(u"parentParamsLayout")
self.buttonBox.setObjectName(u"buttonBox") self.parentParamsLayout.setContentsMargins(0, 0, 0, 0)
self.buttonBox.setOrientation(Qt.Orientation.Horizontal) self.parentParamsTable = QTableWidget(self.parentParamsGroupBox)
self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.Ok) if (self.parentParamsTable.columnCount() < 2):
self.buttonBox.setCenterButtons(True) self.parentParamsTable.setColumnCount(2)
__qtablewidgetitem2 = QTableWidgetItem()
self.verticalLayout.addWidget(self.buttonBox) self.parentParamsTable.setHorizontalHeaderItem(0, __qtablewidgetitem2)
__qtablewidgetitem3 = QTableWidgetItem()
self.parentParamsTable.setHorizontalHeaderItem(1, __qtablewidgetitem3)
self.retranslateUi(TreeNodeEditDialog) self.parentParamsTable.setObjectName(u"parentParamsTable")
self.buttonBox.accepted.connect(TreeNodeEditDialog.accept) self.parentParamsTable.setFrameShape(QFrame.Shape.NoFrame)
self.buttonBox.rejected.connect(TreeNodeEditDialog.reject) self.parentParamsTable.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
self.parentParamsTable.setColumnCount(2)
QMetaObject.connectSlotsByName(TreeNodeEditDialog) self.parentParamsTable.horizontalHeader().setVisible(True)
# setupUi
self.parentParamsLayout.addWidget(self.parentParamsTable)
def retranslateUi(self, TreeNodeEditDialog):
TreeNodeEditDialog.setWindowTitle(QCoreApplication.translate("TreeNodeEditDialog", u"TreeNode bearbeiten", None))
self.bezLabel.setText(QCoreApplication.translate("TreeNodeEditDialog", u"Bezeichnung:", None)) self.horizontalLayout.addWidget(self.parentParamsGroupBox)
self.xsltParamsGroupBox.setTitle(QCoreApplication.translate("TreeNodeEditDialog", u"XSLT-Parameter", None))
___qtablewidgetitem = self.xsltParamsTable.horizontalHeaderItem(0)
___qtablewidgetitem.setText(QCoreApplication.translate("TreeNodeEditDialog", u"Parameter", None)); self.verticalLayout.addWidget(self.frame)
___qtablewidgetitem1 = self.xsltParamsTable.horizontalHeaderItem(1)
___qtablewidgetitem1.setText(QCoreApplication.translate("TreeNodeEditDialog", u"Wert", None)); self.alle_xml_transformieren = QCheckBox(TreeNodeEditDialog)
self.addParamButton.setText(QCoreApplication.translate("TreeNodeEditDialog", u"Parameter hinzuf\u00fcgen", None)) self.alle_xml_transformieren.setObjectName(u"alle_xml_transformieren")
self.removeParamButton.setText(QCoreApplication.translate("TreeNodeEditDialog", u"Parameter entfernen", None))
self.parentParamsGroupBox.setTitle(QCoreApplication.translate("TreeNodeEditDialog", u"Geerbte XSLT-Parameter (nur anzeigen)", None)) self.verticalLayout.addWidget(self.alle_xml_transformieren)
___qtablewidgetitem2 = self.parentParamsTable.horizontalHeaderItem(0)
___qtablewidgetitem2.setText(QCoreApplication.translate("TreeNodeEditDialog", u"Parameter", None)); self.buttonBox = QDialogButtonBox(TreeNodeEditDialog)
___qtablewidgetitem3 = self.parentParamsTable.horizontalHeaderItem(1) self.buttonBox.setObjectName(u"buttonBox")
___qtablewidgetitem3.setText(QCoreApplication.translate("TreeNodeEditDialog", u"Wert", None)); self.buttonBox.setOrientation(Qt.Orientation.Horizontal)
# retranslateUi self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.Ok)
self.buttonBox.setCenterButtons(True)
self.verticalLayout.addWidget(self.buttonBox)
self.retranslateUi(TreeNodeEditDialog)
self.buttonBox.accepted.connect(TreeNodeEditDialog.accept)
self.buttonBox.rejected.connect(TreeNodeEditDialog.reject)
QMetaObject.connectSlotsByName(TreeNodeEditDialog)
# setupUi
def retranslateUi(self, TreeNodeEditDialog):
TreeNodeEditDialog.setWindowTitle(QCoreApplication.translate("TreeNodeEditDialog", u"TreeNode bearbeiten", None))
self.bezLabel.setText(QCoreApplication.translate("TreeNodeEditDialog", u"Bezeichnung:", None))
self.xsltParamsGroupBox.setTitle(QCoreApplication.translate("TreeNodeEditDialog", u"XSLT-Parameter", None))
___qtablewidgetitem = self.xsltParamsTable.horizontalHeaderItem(0)
___qtablewidgetitem.setText(QCoreApplication.translate("TreeNodeEditDialog", u"Parameter", None));
___qtablewidgetitem1 = self.xsltParamsTable.horizontalHeaderItem(1)
___qtablewidgetitem1.setText(QCoreApplication.translate("TreeNodeEditDialog", u"Wert", None));
self.addParamButton.setText(QCoreApplication.translate("TreeNodeEditDialog", u"Parameter hinzuf\u00fcgen", None))
self.removeParamButton.setText(QCoreApplication.translate("TreeNodeEditDialog", u"Parameter entfernen", None))
self.parentParamsGroupBox.setTitle(QCoreApplication.translate("TreeNodeEditDialog", u"Geerbte XSLT-Parameter (nur anzeigen)", None))
___qtablewidgetitem2 = self.parentParamsTable.horizontalHeaderItem(0)
___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
+253
View File
@@ -0,0 +1,253 @@
"""
Worker Pool Metriken-Dialog.
Zeigt Performance- und Ressourcenverbrauch-Metriken für Saxon- und FOP-Worker-Pools an.
"""
import logging
from PySide6.QtWidgets import (
QDialog,
QVBoxLayout,
QHBoxLayout,
QGroupBox,
QLabel,
QPushButton,
QTabWidget,
QWidget,
)
logger = logging.getLogger(__name__)
class WorkerPoolMetricsDialog(QDialog):
"""
Dialog zur Anzeige von Worker-Pool-Metriken.
"""
def __init__(self, parent=None):
"""
Initialisiert den Metriken-Dialog.
Args:
parent: Eltern-Widget
"""
super().__init__(parent)
self.setWindowTitle("Worker Pool Performance-Metriken")
self.resize(800, 600)
self._setup_ui()
self._load_metrics()
def _setup_ui(self):
"""Erstellt die UI-Elemente."""
layout = QVBoxLayout(self)
# Tab-Widget für Saxon und FOP
self.tab_widget = QTabWidget()
layout.addWidget(self.tab_widget)
# Saxon-Tab
self.saxon_tab = self._create_metrics_tab("Saxon Worker Pool")
self.tab_widget.addTab(self.saxon_tab, "Saxon (XSLT)")
# FOP-Tab
self.fop_tab = self._create_metrics_tab("FOP Worker Pool")
self.tab_widget.addTab(self.fop_tab, "FOP (PDF)")
# Schließen-Button
button_layout = QHBoxLayout()
button_layout.addStretch()
close_button = QPushButton("Schließen")
close_button.clicked.connect(self.accept)
button_layout.addWidget(close_button)
layout.addLayout(button_layout)
def _create_metrics_tab(self, title: str) -> QWidget:
"""
Erstellt ein Tab-Widget für Metriken.
Args:
title: Titel der Metrik-Gruppe
Returns:
QWidget mit Metriken
"""
tab = QWidget()
layout = QVBoxLayout(tab)
# Status-Label
status_label = QLabel("Status: <i>Nicht initialisiert</i>")
status_label.setObjectName("status_label")
layout.addWidget(status_label)
# Kompilierungs-Metriken
compile_group = QGroupBox("Kompilierung")
compile_layout = QVBoxLayout(compile_group)
compile_time_label = QLabel("Kompilierungszeit: -")
compile_time_label.setObjectName("compile_time_label")
compile_layout.addWidget(compile_time_label)
layout.addWidget(compile_group)
# Worker-Start-Metriken
worker_start_group = QGroupBox("Worker-Start")
worker_start_layout = QVBoxLayout(worker_start_group)
worker_count_label = QLabel("Anzahl Worker: -")
worker_count_label.setObjectName("worker_count_label")
worker_start_layout.addWidget(worker_count_label)
total_start_time_label = QLabel("Summe Startzeit: -")
total_start_time_label.setObjectName("total_start_time_label")
worker_start_layout.addWidget(total_start_time_label)
avg_start_time_label = QLabel("Durchschnitt Startzeit: -")
avg_start_time_label.setObjectName("avg_start_time_label")
worker_start_layout.addWidget(avg_start_time_label)
layout.addWidget(worker_start_group)
# RAM-Metriken
ram_group = QGroupBox("Arbeitsspeicher (RAM)")
ram_layout = QVBoxLayout(ram_group)
ram_before_label = QLabel("RAM vor Transformation: -")
ram_before_label.setObjectName("ram_before_label")
ram_layout.addWidget(ram_before_label)
ram_before_avg_label = QLabel("RAM vor Transformation (Durchschnitt/Worker): -")
ram_before_avg_label.setObjectName("ram_before_avg_label")
ram_layout.addWidget(ram_before_avg_label)
ram_after_label = QLabel("RAM nach Transformation: -")
ram_after_label.setObjectName("ram_after_label")
ram_layout.addWidget(ram_after_label)
ram_after_avg_label = QLabel("RAM nach Transformation (Durchschnitt/Worker): -")
ram_after_avg_label.setObjectName("ram_after_avg_label")
ram_layout.addWidget(ram_after_avg_label)
ram_delta_label = QLabel("RAM-Zunahme: -")
ram_delta_label.setObjectName("ram_delta_label")
ram_layout.addWidget(ram_delta_label)
layout.addWidget(ram_group)
layout.addStretch()
return tab
def _load_metrics(self):
"""Lädt und zeigt die Metriken an."""
# Hole MainWindow-Instanz (parent)
main_window = self.parent()
# Saxon Worker Pool - verwende gespeicherte Metriken
if hasattr(main_window, "last_saxon_metrics") and main_window.last_saxon_metrics:
self._update_metrics_tab_from_metrics(
self.saxon_tab, main_window.last_saxon_metrics, "Saxon Worker Pool"
)
else:
self._set_tab_status(
self.saxon_tab, "<i>Keine Metriken verfügbar - bitte erst eine Transformation durchführen</i>"
)
# FOP Worker Pool - verwende gespeicherte Metriken
if hasattr(main_window, "last_fop_metrics") and main_window.last_fop_metrics:
self._update_metrics_tab_from_metrics(self.fop_tab, main_window.last_fop_metrics, "FOP Worker Pool")
else:
self._set_tab_status(
self.fop_tab, "<i>Keine Metriken verfügbar - bitte erst eine Transformation durchführen</i>"
)
def _set_tab_status(self, tab: QWidget, status: str):
"""
Setzt den Status-Text eines Tabs.
Args:
tab: Das Tab-Widget
status: Der Status-Text
"""
status_label = tab.findChild(QLabel, "status_label")
if status_label:
status_label.setText(f"Status: {status}")
def _update_metrics_tab_from_metrics(self, tab: QWidget, metrics, pool_name: str):
"""
Aktualisiert die Metriken in einem Tab (direkt aus Metriken-Objekt).
Args:
tab: Das Tab-Widget
metrics: Das WorkerPoolMetrics-Objekt
pool_name: Name des Pools
"""
# Status - berechne Worker-Anzahl aus Metriken
num_workers = len(metrics.worker_start_times) if metrics.worker_start_times else 0
self._set_tab_status(tab, f"<b>Letzte Transformation</b> ({num_workers} Worker)")
# Kompilierung
compile_time_label = tab.findChild(QLabel, "compile_time_label")
if compile_time_label:
compile_time_label.setText(f"Kompilierungszeit: <b>{metrics.compilation_time_seconds:.3f} s</b>")
# Worker-Start
worker_count_label = tab.findChild(QLabel, "worker_count_label")
if worker_count_label:
worker_count_label.setText(f"Anzahl Worker: <b>{len(metrics.worker_start_times)}</b>")
total_start_time_label = tab.findChild(QLabel, "total_start_time_label")
if total_start_time_label:
total_start_time_label.setText(
f"Summe Startzeit: <b>{metrics.total_worker_start_time_seconds:.3f} s</b>"
)
avg_start_time_label = tab.findChild(QLabel, "avg_start_time_label")
if avg_start_time_label:
avg_start_time_label.setText(
f"Durchschnitt Startzeit: <b>{metrics.average_worker_start_time_seconds:.3f} s/Worker</b>"
)
# RAM
ram_before_label = tab.findChild(QLabel, "ram_before_label")
if ram_before_label:
if metrics.total_ram_before_mb > 0:
ram_before_label.setText(f"RAM vor Transformation: <b>{metrics.total_ram_before_mb:.1f} MB</b>")
else:
ram_before_label.setText("RAM vor Transformation: <i>Noch nicht gemessen</i>")
ram_before_avg_label = tab.findChild(QLabel, "ram_before_avg_label")
if ram_before_avg_label:
if metrics.average_ram_before_mb > 0:
ram_before_avg_label.setText(
f"RAM vor Transformation (Durchschnitt/Worker): <b>{metrics.average_ram_before_mb:.1f} MB</b>"
)
else:
ram_before_avg_label.setText(
"RAM vor Transformation (Durchschnitt/Worker): <i>Noch nicht gemessen</i>"
)
ram_after_label = tab.findChild(QLabel, "ram_after_label")
if ram_after_label:
if metrics.total_ram_after_mb > 0:
ram_after_label.setText(f"RAM nach Transformation: <b>{metrics.total_ram_after_mb:.1f} MB</b>")
else:
ram_after_label.setText("RAM nach Transformation: <i>Noch nicht gemessen</i>")
ram_after_avg_label = tab.findChild(QLabel, "ram_after_avg_label")
if ram_after_avg_label:
if metrics.average_ram_after_mb > 0:
ram_after_avg_label.setText(
f"RAM nach Transformation (Durchschnitt/Worker): <b>{metrics.average_ram_after_mb:.1f} MB</b>"
)
else:
ram_after_avg_label.setText(
"RAM nach Transformation (Durchschnitt/Worker): <i>Noch nicht gemessen</i>"
)
ram_delta_label = tab.findChild(QLabel, "ram_delta_label")
if ram_delta_label:
if metrics.total_ram_before_mb > 0 and metrics.total_ram_after_mb > 0:
delta = metrics.total_ram_after_mb - metrics.total_ram_before_mb
delta_percent = (delta / metrics.total_ram_before_mb * 100) if metrics.total_ram_before_mb > 0 else 0
ram_delta_label.setText(f"RAM-Zunahme: <b>{delta:.1f} MB ({delta_percent:+.1f}%)</b>")
else:
ram_delta_label.setText("RAM-Zunahme: <i>Noch nicht gemessen</i>")
+120 -22
View File
@@ -1,3 +1,4 @@
import logging
from PySide6.QtWidgets import QDialog, QTreeWidgetItem, QCheckBox, QMessageBox, QWidget, QHBoxLayout from PySide6.QtWidgets import QDialog, QTreeWidgetItem, QCheckBox, QMessageBox, QWidget, QHBoxLayout
from PySide6.QtCore import Qt from PySide6.QtCore import Qt
from pathlib import Path from pathlib import Path
@@ -6,17 +7,29 @@ from ui.XmlToXslAssignDialog_ui import Ui_XmlToXslAssignDialog
from conf import TreeNode, XslFile from conf import TreeNode, XslFile
logger = logging.getLogger(__name__)
class XmlToXslAssignDialog(QDialog): class XmlToXslAssignDialog(QDialog):
"""Dialog zur Zuordnung einer XML-Datei zu XSL-Knoten.""" """Dialog zur Zuordnung einer XML-Datei zu XSL-Knoten."""
def __init__(self, parent=None, xml_file_path=None, project_nodes=None): def __init__(
self,
parent=None,
xml_file_path=None,
project_nodes=None,
preselected_xsl_ids: set | None = None,
edit_mode: bool = False,
):
""" """
Initialisiert den Dialog. Initialisiert den Dialog.
Args: Args:
parent: Übergeordnetes Widget parent: Übergeordnetes Widget
xml_file_path: Pfad zur XML-Datei xml_file_path: Pfad zur XML-Datei
project_nodes: Liste der Projekt-Knoten project_nodes: Liste der Projekt-Knoten
preselected_xsl_ids: Set von XslFile-IDs (tuple), deren Checkbox initial angehakt sein soll
edit_mode: True = Bearbeiten-Modus (Zuordnungen ändern), False = Neu-Zuordnen
""" """
super().__init__(parent) super().__init__(parent)
@@ -27,9 +40,23 @@ class XmlToXslAssignDialog(QDialog):
# Parameter speichern # Parameter speichern
self.xml_file_path = Path(xml_file_path) if xml_file_path else None self.xml_file_path = Path(xml_file_path) if xml_file_path else None
self.project_nodes = project_nodes or [] self.project_nodes = project_nodes or []
self.preselected_xsl_ids: set = set(preselected_xsl_ids) if preselected_xsl_ids else set()
self.edit_mode = edit_mode
# Dictionary zum Speichern der Checkbox-Referenzen # Dictionary zum Speichern der Checkbox-Referenzen
self.xsl_checkboxes = {} # {xsl_node_id: checkbox} self.xsl_checkboxes = {} # {python_id(node): checkbox}
self.xsl_nodes = {} # {python_id(node): XslFile}
# Initial ausgewählte XslFile-IDs (tuple), um Diff beim Accept zu berechnen
self._initial_selected_ids: set = set()
# Edit-Modus: UI anpassen
if self.edit_mode:
self.setWindowTitle("XML-Zuordnungen bearbeiten")
self.ui.infoLabel.setText(
"Passen Sie die Zuordnungen der XML-Datei an. Hinzufügen per Haken, "
"Entfernen durch Abhaken (zugehörige PDFs werden gelöscht):"
)
self.ui.alle_xml.setVisible(False)
# Signale verbinden # Signale verbinden
self.ui.selectAllButton.clicked.connect(self.select_all) self.ui.selectAllButton.clicked.connect(self.select_all)
@@ -41,6 +68,10 @@ class XmlToXslAssignDialog(QDialog):
# Daten laden # Daten laden
self._load_data() self._load_data()
# Duplikat-Warnung nur im Edit-Modus (um bestehende Aufrufer nicht bei jeder XML zu stören)
if self.edit_mode:
self._warn_on_duplicate_xsl_ids()
def _setup_tree(self): def _setup_tree(self):
"""Konfiguriert das TreeWidget.""" """Konfiguriert das TreeWidget."""
# Spaltenbreiten setzen # Spaltenbreiten setzen
@@ -85,10 +116,10 @@ class XmlToXslAssignDialog(QDialog):
root = self.ui.xslNodesTree.invisibleRootItem() root = self.ui.xslNodesTree.invisibleRootItem()
self._add_checkboxes_recursive(root) self._add_checkboxes_recursive(root)
print(f"Checkboxen zu {len(self.xsl_checkboxes)} XSL-Knoten hinzugefügt") logger.debug(f"Checkboxen zu {len(self.xsl_checkboxes)} XSL-Knoten hinzugefügt")
except Exception as e: except Exception as e:
print(f"Fehler beim Hinzufügen der Checkboxen: {e}") logger.error(f"Fehler beim Hinzufügen der Checkboxen: {e}")
def _add_checkboxes_recursive(self, parent_item): def _add_checkboxes_recursive(self, parent_item):
""" """
@@ -106,14 +137,20 @@ class XmlToXslAssignDialog(QDialog):
if isinstance(node, XslFile): if isinstance(node, XslFile):
# Erstelle zentrierte Checkbox für XSL-Knoten # Erstelle zentrierte Checkbox für XSL-Knoten
checkbox_widget, checkbox = self._create_centered_checkbox() checkbox_widget, checkbox = self._create_centered_checkbox()
# Vorauswahl setzen, wenn ID in preselected_xsl_ids
if node.id in self.preselected_xsl_ids:
checkbox.setChecked(True)
self._initial_selected_ids.add(node.id)
# Setze das Widget in Spalte 2 # Setze das Widget in Spalte 2
self.ui.xslNodesTree.setItemWidget(item, 2, checkbox_widget) self.ui.xslNodesTree.setItemWidget(item, 2, checkbox_widget)
# Speichere Checkbox-Referenz # Speichere Checkbox- und Node-Referenzen (id() als Key, da XSL-IDs theoretisch doppelt sein können)
self.xsl_checkboxes[id(node)] = checkbox self.xsl_checkboxes[id(node)] = checkbox
self.xsl_nodes[id(node)] = node
print(f"Checkbox für XSL-Knoten '{node.bez}' hinzugefügt")
logger.debug(f"Checkbox für XSL-Knoten '{node.bez}' hinzugefügt")
# Rekursiv für Kinder # Rekursiv für Kinder
if item.childCount() > 0: if item.childCount() > 0:
@@ -137,6 +174,8 @@ class XmlToXslAssignDialog(QDialog):
# TreeWidget leeren # TreeWidget leeren
self.ui.xslNodesTree.clear() self.ui.xslNodesTree.clear()
self.xsl_checkboxes.clear() self.xsl_checkboxes.clear()
self.xsl_nodes.clear()
self._initial_selected_ids.clear()
# Sortiere Root-Nodes alphabetisch nach ID # Sortiere Root-Nodes alphabetisch nach ID
sorted_nodes = sorted(self.project_nodes, key=lambda node: node.id) sorted_nodes = sorted(self.project_nodes, key=lambda node: node.id)
@@ -201,7 +240,7 @@ class XmlToXslAssignDialog(QDialog):
return item return item
except Exception as e: except Exception as e:
print(f"Fehler beim Erstellen des Tree-Items: {e}") logger.error(f"Fehler beim Erstellen des Tree-Items: {e}")
return None return None
def select_all(self): def select_all(self):
@@ -235,7 +274,7 @@ class XmlToXslAssignDialog(QDialog):
return selected_nodes return selected_nodes
except Exception as e: except Exception as e:
print(f"Fehler beim Sammeln der ausgewählten XSL-Knoten: {e}") logger.error(f"Fehler beim Sammeln der ausgewählten XSL-Knoten: {e}")
return [] return []
def _find_xsl_node_by_id(self, node_id): def _find_xsl_node_by_id(self, node_id):
@@ -276,22 +315,81 @@ class XmlToXslAssignDialog(QDialog):
def get_xml_file_path(self): def get_xml_file_path(self):
""" """
Gibt den Pfad zur XML-Datei zurück. Gibt den Pfad zur XML-Datei zurück.
Returns: Returns:
Path: Pfad zur XML-Datei Path: Pfad zur XML-Datei
""" """
return self.xml_file_path 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 get_selection_diff(self) -> tuple[list, list]:
"""
Gibt die Änderungen der Auswahl gegenüber der Vorauswahl zurück.
Nützlich im Edit-Modus, um nur die tatsächlich geänderten Zuordnungen zu verarbeiten.
Returns:
tuple[list[XslFile], list[XslFile]]: (added_nodes, removed_nodes)
- added_nodes: XslFile-Objekte, die neu angehakt wurden
- removed_nodes: XslFile-Objekte, deren Haken entfernt wurde
"""
added_nodes = []
removed_nodes = []
for py_id, checkbox in self.xsl_checkboxes.items():
node = self.xsl_nodes.get(py_id)
if node is None:
continue
was_selected = node.id in self._initial_selected_ids
is_selected = checkbox.isChecked()
if is_selected and not was_selected:
added_nodes.append(node)
elif not is_selected and was_selected:
removed_nodes.append(node)
return added_nodes, removed_nodes
def _warn_on_duplicate_xsl_ids(self):
"""
Zeigt eine Warnung, wenn im Projekt mehrere XslFile-Instanzen mit identischer ID existieren.
Die ID soll eindeutig sein - Duplikate weisen auf einen Datenfehler hin.
"""
id_to_nodes: dict = {}
for py_id, node in self.xsl_nodes.items():
id_to_nodes.setdefault(node.id, []).append(node)
duplicates = {xsl_id: nodes for xsl_id, nodes in id_to_nodes.items() if len(nodes) > 1}
if not duplicates:
return
dup_lines = [f" - ID {xsl_id}: {len(nodes)}× ({', '.join(n.bez for n in nodes)})" for xsl_id, nodes in duplicates.items()]
logger.warning(f"Doppelte XSL-IDs im Projekt gefunden:\n{chr(10).join(dup_lines)}")
QMessageBox.warning(
self,
"Doppelte XSL-IDs",
"Im Projekt existieren XSL-Knoten mit identischer ID. "
"Die IDs sollten eindeutig sein:\n\n" + "\n".join(dup_lines),
)
def accept(self): def accept(self):
"""Überschreibt accept() um Validierung durchzuführen.""" """Überschreibt accept() um Validierung durchzuführen."""
selected_nodes = self.get_selected_xsl_nodes() # Im Edit-Modus: 0 Auswahlen erlauben (bedeutet: XML überall entfernen)
if self.edit_mode:
if not selected_nodes: super().accept()
QMessageBox.warning(
self,
"Warnung",
"Bitte wählen Sie mindestens einen XSL-Knoten aus."
)
return return
selected_nodes = self.get_selected_xsl_nodes()
if not selected_nodes:
QMessageBox.warning(self, "Warnung", "Bitte wählen Sie mindestens einen XSL-Knoten aus.")
return
super().accept() super().accept()
+17
View File
@@ -42,6 +42,16 @@
</item> </item>
<item> <item>
<widget class="QTreeWidget" name="xslNodesTree"> <widget class="QTreeWidget" name="xslNodesTree">
<property name="styleSheet">
<string notr="true"> QTreeWidget::item {
padding: 4px 4px;
}
QTreeWidget::item:selected {
background-color: palette(highlight);
color: palette(highlighted-text);
}</string>
</property>
<property name="headerHidden"> <property name="headerHidden">
<bool>false</bool> <bool>false</bool>
</property> </property>
@@ -97,6 +107,13 @@
</property> </property>
</spacer> </spacer>
</item> </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> </layout>
</item> </item>
<item> <item>
+19 -5
View File
@@ -3,7 +3,7 @@
################################################################################ ################################################################################
## Form generated from reading UI file 'XmlToXslAssignDialog.ui' ## Form generated from reading UI file 'XmlToXslAssignDialog.ui'
## ##
## Created by: Qt User Interface Compiler version 6.9.1 ## Created by: Qt User Interface Compiler version 6.10.1
## ##
## WARNING! All changes made in this file will be lost when recompiling UI file! ## 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, QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter, QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform) QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QAbstractButton, QApplication, QDialog, QDialogButtonBox, from PySide6.QtWidgets import (QAbstractButton, QApplication, QCheckBox, QDialog,
QHBoxLayout, QHeaderView, QLabel, QPushButton, QDialogButtonBox, QHBoxLayout, QHeaderView, QLabel,
QSizePolicy, QSpacerItem, QTreeWidget, QTreeWidgetItem, QPushButton, QSizePolicy, QSpacerItem, QTreeWidget,
QVBoxLayout, QWidget) QTreeWidgetItem, QVBoxLayout, QWidget)
class Ui_XmlToXslAssignDialog(object): class Ui_XmlToXslAssignDialog(object):
def setupUi(self, XmlToXslAssignDialog): def setupUi(self, XmlToXslAssignDialog):
@@ -42,6 +42,14 @@ class Ui_XmlToXslAssignDialog(object):
self.xslNodesTree = QTreeWidget(XmlToXslAssignDialog) self.xslNodesTree = QTreeWidget(XmlToXslAssignDialog)
self.xslNodesTree.setObjectName(u"xslNodesTree") self.xslNodesTree.setObjectName(u"xslNodesTree")
self.xslNodesTree.setStyleSheet(u" QTreeWidget::item {\n"
" padding: 4px 4px;\n"
" }\n"
"\n"
"QTreeWidget::item:selected {\n"
" background-color: palette(highlight);\n"
" color: palette(highlighted-text);\n"
"}")
self.xslNodesTree.setHeaderHidden(False) self.xslNodesTree.setHeaderHidden(False)
self.xslNodesTree.setColumnCount(3) self.xslNodesTree.setColumnCount(3)
self.xslNodesTree.header().setVisible(True) self.xslNodesTree.header().setVisible(True)
@@ -64,6 +72,11 @@ class Ui_XmlToXslAssignDialog(object):
self.buttonLayout.addItem(self.horizontalSpacer) 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) self.verticalLayout.addLayout(self.buttonLayout)
@@ -94,5 +107,6 @@ class Ui_XmlToXslAssignDialog(object):
___qtreewidgetitem.setText(0, QCoreApplication.translate("XmlToXslAssignDialog", u"XSL-Knoten", None)); ___qtreewidgetitem.setText(0, QCoreApplication.translate("XmlToXslAssignDialog", u"XSL-Knoten", None));
self.selectAllButton.setText(QCoreApplication.translate("XmlToXslAssignDialog", u"Alle ausw\u00e4hlen", None)) self.selectAllButton.setText(QCoreApplication.translate("XmlToXslAssignDialog", u"Alle ausw\u00e4hlen", None))
self.deselectAllButton.setText(QCoreApplication.translate("XmlToXslAssignDialog", u"Alle abw\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 # retranslateUi

Some files were not shown because too many files have changed in this diff Show More