diff --git a/DocuMentor.wxs b/DocuMentor.wxs index c98dce1..7276358 100644 --- a/DocuMentor.wxs +++ b/DocuMentor.wxs @@ -4,7 +4,7 @@ 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})") diff --git a/src/ui/AboutDialog.py b/src/ui/AboutDialog.py new file mode 100644 index 0000000..7bfe3f7 --- /dev/null +++ b/src/ui/AboutDialog.py @@ -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)) diff --git a/src/ui/MainWindow.py b/src/ui/MainWindow.py index f7240de..5987b1a 100644 --- a/src/ui/MainWindow.py +++ b/src/ui/MainWindow.py @@ -400,6 +400,14 @@ class MainWindow( 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) + # 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): """Öffnet den Einstellungen-Dialog.""" try: @@ -411,6 +419,13 @@ class MainWindow( except Exception as 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): """Öffnet den XSL-Abhängigkeitsgraph-Dialog.""" try: