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>
This commit is contained in:
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
<!-- Paket-Definition (ersetzt Product in v4) -->
|
<!-- Paket-Definition (ersetzt Product in v4) -->
|
||||||
<Package
|
<Package
|
||||||
Name="DocuMentor"
|
Name="DocuMentor"
|
||||||
Version="1.0.0"
|
Version="1.1.0"
|
||||||
Manufacturer="Vitali Graf / Software- und Datenbankentwicklung"
|
Manufacturer="Vitali Graf / Software- und Datenbankentwicklung"
|
||||||
UpgradeCode="F498B66C-726D-44AA-95F4-CB4FBDCEF26E"
|
UpgradeCode="F498B66C-726D-44AA-95F4-CB4FBDCEF26E"
|
||||||
Language="1031"
|
Language="1031"
|
||||||
|
|||||||
@@ -245,5 +245,5 @@ HINWEISE
|
|||||||
|
|
||||||
================================================================================
|
================================================================================
|
||||||
Stand: März 2026
|
Stand: März 2026
|
||||||
Erstellt für: DocuMentor v1.0.0
|
Erstellt für: DocuMentor v1.1.0
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
; Build-Befehl: iscc installer.iss
|
; Build-Befehl: iscc installer.iss
|
||||||
|
|
||||||
#define MyAppName "DocuMentor"
|
#define MyAppName "DocuMentor"
|
||||||
#define MyAppVersion "1.0.0"
|
#define MyAppVersion "1.1.0"
|
||||||
#define MyAppPublisher "Ihr Name/Organisation"
|
#define MyAppPublisher "Ihr Name/Organisation"
|
||||||
#define MyAppURL "https://github.com/yourusername/xsl-validator"
|
#define MyAppURL "https://github.com/yourusername/xsl-validator"
|
||||||
#define MyAppExeName "DocuMentor.exe"
|
#define MyAppExeName "DocuMentor.exe"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "DocuMentor"
|
name = "DocuMentor"
|
||||||
version = "1.0.0"
|
version = "1.1.0"
|
||||||
description = "Professionelle XSL-Transformations-Verwaltung und PDF-Generierung"
|
description = "Professionelle XSL-Transformations-Verwaltung und PDF-Generierung"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = {text = "MIT"}
|
license = {text = "MIT"}
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
"""
|
||||||
|
Parser für THIRD_PARTY_LICENSES.txt.
|
||||||
|
|
||||||
|
Extrahiert strukturierte Lizenzinformationen und ergänzt sie
|
||||||
|
mit den tatsächlich installierten Paketversionen via importlib.metadata.
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from importlib.metadata import PackageNotFoundError, version
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Pfad zur Lizenzdatei relativ zum Projektroot
|
||||||
|
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()
|
||||||
|
|
||||||
|
|
||||||
|
def _get_installed_version(package_name: str) -> str:
|
||||||
|
"""Ermittelt die installierte Version eines Pakets."""
|
||||||
|
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})")
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
"""
|
||||||
|
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 (Entwicklungsmodus ohne Installation)
|
||||||
|
try:
|
||||||
|
import tomllib
|
||||||
|
|
||||||
|
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, 140) # 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 "—"))
|
||||||
|
self.table.setItem(row, 3, QTableWidgetItem(entry.website))
|
||||||
|
self.table.setItem(row, 4, QTableWidgetItem(entry.copyright))
|
||||||
|
self.table.setItem(row, 5, QTableWidgetItem(entry.category))
|
||||||
@@ -400,6 +400,14 @@ class MainWindow(
|
|||||||
self.ui.view_ref_pdf.clicked.connect(self._on_view_ref_pdf_clicked)
|
self.ui.view_ref_pdf.clicked.connect(self._on_view_ref_pdf_clicked)
|
||||||
self.ui.view_new_pdf.clicked.connect(self._on_view_new_pdf_clicked)
|
self.ui.view_new_pdf.clicked.connect(self._on_view_new_pdf_clicked)
|
||||||
|
|
||||||
|
# Hilfe-Menü programmatisch erstellen
|
||||||
|
self.menu_hilfe = QMenu("Hilfe", self)
|
||||||
|
self.action_info = QAction("Info ...", self)
|
||||||
|
self.action_info.setIcon(_QIcon(_QIcon.fromTheme("help-about")))
|
||||||
|
self.action_info.triggered.connect(self._show_about_dialog)
|
||||||
|
self.menu_hilfe.addAction(self.action_info)
|
||||||
|
self.ui.menubar.addMenu(self.menu_hilfe)
|
||||||
|
|
||||||
def open_settings_dialog(self):
|
def open_settings_dialog(self):
|
||||||
"""Öffnet den Einstellungen-Dialog."""
|
"""Öffnet den Einstellungen-Dialog."""
|
||||||
try:
|
try:
|
||||||
@@ -411,6 +419,13 @@ class MainWindow(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Fehler beim Öffnen des Einstellungen-Dialogs: {e}")
|
logger.error(f"Fehler beim Öffnen des Einstellungen-Dialogs: {e}")
|
||||||
|
|
||||||
|
def _show_about_dialog(self):
|
||||||
|
"""Öffnet den Info-Dialog mit Versionsinformationen und Drittanbieter-Lizenzen."""
|
||||||
|
from ui.AboutDialog import AboutDialog
|
||||||
|
|
||||||
|
dialog = AboutDialog(self)
|
||||||
|
dialog.exec()
|
||||||
|
|
||||||
def _show_xsl_dependency_dialog(self):
|
def _show_xsl_dependency_dialog(self):
|
||||||
"""Öffnet den XSL-Abhängigkeitsgraph-Dialog."""
|
"""Öffnet den XSL-Abhängigkeitsgraph-Dialog."""
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user