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>
This commit is contained in:
2026-03-22 19:57:17 +01:00
parent 84d0866f72
commit bf352a1fcd
6 changed files with 107 additions and 19 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
<!-- Paket-Definition (ersetzt Product in v4) -->
<Package
Name="DocuMentor"
Version="1.2.3"
Version="1.2.4"
Manufacturer="Vitali Graf / Software- und Datenbankentwicklung"
UpgradeCode="F498B66C-726D-44AA-95F4-CB4FBDCEF26E"
Language="1031"
+1 -1
View File
@@ -253,5 +253,5 @@ HINWEISE
================================================================================
Stand: März 2026
Erstellt für: DocuMentor v1.2.3
Erstellt für: DocuMentor v1.2.4
================================================================================
+1 -1
View File
@@ -10,7 +10,7 @@
; Build-Befehl: iscc installer.iss
#define MyAppName "DocuMentor"
#define MyAppVersion "1.2.3"
#define MyAppVersion "1.2.4"
#define MyAppPublisher "Ihr Name/Organisation"
#define MyAppURL "https://github.com/yourusername/xsl-validator"
#define MyAppExeName "DocuMentor.exe"
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "DocuMentor"
version = "1.2.3"
version = "1.2.4"
description = "Professionelle XSL-Transformations-Verwaltung und PDF-Generierung"
readme = "README.md"
license = {text = "MIT"}
+10 -1
View File
@@ -448,10 +448,19 @@ class MainWindow(
self.xsl_dependency_graph = XslDependencyGraph()
# Projekt-XSL-Pfade sammeln (absolute Pfade aller im Projekt referenzierten XSL-Dateien)
project_xsl_paths: set[Path] = set()
if hasattr(self, "pdf_project") and self.pdf_project is not None:
for top_node in self.pdf_project.nodes:
for xsl_file in self._collect_xsl_files_recursive(top_node):
project_xsl_paths.add((xsl_dir.path_to_root_dir / xsl_file.xsl_file).resolve())
from ui.XslDependencyDialog import XslDependencyDialog
# open() statt exec() verwenden — QWebEngineView verträgt keinen verschachtelten Event-Loop
self._xsl_dep_dialog = XslDependencyDialog(self, xsl_dir.path_to_root_dir, self.xsl_dependency_graph)
self._xsl_dep_dialog = XslDependencyDialog(
self, xsl_dir.path_to_root_dir, self.xsl_dependency_graph, project_xsl_paths
)
self._xsl_dep_dialog.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
self._xsl_dep_dialog.open()
except Exception as e:
+93 -14
View File
@@ -123,16 +123,24 @@ except ImportError:
class XslDependencyDialog(QDialog):
"""Dialog zur Anzeige des vollständigen XSL-Abhängigkeitsgraphen."""
def __init__(self, parent, xsl_root_dir: Path, dependency_graph: XslDependencyGraph):
def __init__(
self,
parent,
xsl_root_dir: Path,
dependency_graph: XslDependencyGraph,
project_xsl_paths: set[Path] | None = None,
):
"""
Args:
parent: Eltern-Widget
xsl_root_dir: Wurzelverzeichnis der XSL-Dateien
dependency_graph: XSL-Abhängigkeitsgraph-Instanz
project_xsl_paths: Absolute Pfade aller im Projekt referenzierten XSL-Dateien (None = kein Projekt)
"""
super().__init__(parent)
self.xsl_root_dir = xsl_root_dir
self.dependency_graph = dependency_graph
self._project_xsl_paths: set[Path] = project_xsl_paths if project_xsl_paths is not None else set()
self._web_view: "QWebEngineView | None" = None
self._graph_loaded = False
self._temp_html_file: tempfile.NamedTemporaryFile | None = None
@@ -653,44 +661,88 @@ class XslDependencyDialog(QDialog):
"""Reagiert auf Doppelklick im Netzwerkgraph: Kopiert Knotenname ins Suchfeld."""
prefix = "nodeSearch:"
if title.startswith(prefix):
node_label = title[len(prefix):]
node_label = title[len(prefix) :]
self.ui.searchEdit.setText(node_label)
def _build_graph_data(self) -> tuple[str, str]:
"""
Konvertiert den Abhängigkeitsgraph in vis.js-kompatible JSON-Strukturen.
Knoten werden in drei Kategorien eingeteilt:
- Kategorie 1 (blau): Nur im Verzeichnis, nicht im Projekt
- Kategorie 2 (grün): Im Projekt und im Verzeichnis
- Kategorie 3 (rot): Im Projekt referenziert, aber Datei fehlt
Returns:
tuple[str, str]: (nodes_json, edges_json)
"""
# Direkten Graph verwenden (nicht transitiv)
direct_graph = self.dependency_graph.build_direct_graph(self.xsl_root_dir)
# Pfad → ID Mapping
all_paths = sorted(direct_graph.keys(), key=lambda p: self._rel_path(p).lower())
path_to_id: dict[Path, int] = {path: idx for idx, path in enumerate(all_paths)}
# Dateisystem-Pfade (Kategorie 1 und 2)
fs_paths = sorted(direct_graph.keys(), key=lambda p: self._rel_path(p).lower())
path_to_id: dict[Path, int] = {path: idx for idx, path in enumerate(fs_paths)}
# Kategorie 3: Im Projekt referenziert, aber nicht im Dateisystem
ghost_paths = sorted(
self._project_xsl_paths - set(direct_graph.keys()),
key=lambda p: p.name.lower(),
)
ghost_offset = len(fs_paths)
for idx, path in enumerate(ghost_paths):
path_to_id[path] = ghost_offset + idx
# Farb-Definitionen pro Kategorie
color_fs_only = {"background": "#4a90d9", "border": "#2c5f9e"} # Kat. 1: nur Verzeichnis
color_in_project = {"background": "#4caf50", "border": "#2e7d32"} # Kat. 2: Projekt + Verzeichnis
color_ghost = {"background": "#e74c3c", "border": "#c0392b"} # Kat. 3: Projekt, Datei fehlt
# Nodes
nodes = []
for path, node_id in path_to_id.items():
# Kategorie 1 und 2: Dateien die im Verzeichnis existieren
for path in fs_paths:
node_id = path_to_id[path]
rel = self._rel_path(path)
label = path.name
direct_deps = len(direct_graph.get(path, set()))
reverse_count = len(self._reverse_map.get(path, set()))
title = f"<b>{rel}</b><br>Importiert: {direct_deps}<br>Importiert von: {reverse_count}"
# Knotengröße basierend auf Verbindungsanzahl
value = direct_deps + reverse_count
value = max(direct_deps + reverse_count, 1)
in_project = path in self._project_xsl_paths
title = (
f"<b>{rel}</b><br>"
f"Importiert: {direct_deps}<br>"
f"Importiert von: {reverse_count}<br>"
f"Im Projekt: {'Ja' if in_project else 'Nein'}"
)
node: dict = {
"id": node_id,
"label": label,
"title": title,
"value": value,
"color": color_in_project if in_project else color_fs_only,
"borderWidth": 3 if in_project else 1,
}
nodes.append(node)
# Kategorie 3: Ghost-Knoten (im Projekt, aber Datei fehlt im Verzeichnis)
for path in ghost_paths:
rel = self._rel_path(path)
title = f"<b>{rel}</b><br><i>Datei nicht gefunden</i><br>Im Projekt: Ja"
nodes.append(
{
"id": node_id,
"label": label,
"id": path_to_id[path],
"label": path.name,
"title": title,
"value": max(value, 1),
"value": 1,
"color": color_ghost,
"borderWidth": 2,
"shapeProperties": {"borderDashes": [5, 5]},
}
)
# Edges (nur direkte Abhängigkeiten)
# Edges (nur zwischen Dateisystem-Knoten — Ghost-Knoten haben keine bekannten Abhängigkeiten)
edges = []
for path, deps in direct_graph.items():
from_id = path_to_id.get(path)
@@ -757,6 +809,28 @@ class XslDependencyDialog(QDialog):
font-size: 13px !important;
box-shadow: 2px 2px 6px rgba(0,0,0,0.3) !important;
}}
#graph-legend {{
position: absolute;
bottom: 16px;
left: 16px;
background: {bg_hex}cc;
border: 1px solid #888;
border-radius: 6px;
padding: 10px 14px;
font-size: 12px;
color: {text_hex};
z-index: 10;
line-height: 1.8;
pointer-events: none;
}}
.legend-dot {{
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 6px;
vertical-align: middle;
}}
</style>
<script type="text/javascript">
{vis_js_content}
@@ -764,6 +838,11 @@ class XslDependencyDialog(QDialog):
</head>
<body>
<div id="graph-container"></div>
<div id="graph-legend">
<span class="legend-dot" style="background:#4a90d9;border:2px solid #2c5f9e;"></span>Nur im Verzeichnis<br>
<span class="legend-dot" style="background:#4caf50;border:3px solid #2e7d32;"></span>Im Projekt &amp; Verzeichnis<br>
<span class="legend-dot" style="background:#e74c3c;border:2px dashed #c0392b;"></span>Im Projekt, Datei fehlt
</div>
<script type="text/javascript">
var nodesData = {nodes_json};
var edgesData = {edges_json};