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) --> <!-- Paket-Definition (ersetzt Product in v4) -->
<Package <Package
Name="DocuMentor" Name="DocuMentor"
Version="1.2.3" Version="1.2.4"
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"
+1 -1
View File
@@ -253,5 +253,5 @@ HINWEISE
================================================================================ ================================================================================
Stand: März 2026 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 ; Build-Befehl: iscc installer.iss
#define MyAppName "DocuMentor" #define MyAppName "DocuMentor"
#define MyAppVersion "1.2.3" #define MyAppVersion "1.2.4"
#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
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "DocuMentor" name = "DocuMentor"
version = "1.2.3" version = "1.2.4"
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"}
+10 -1
View File
@@ -448,10 +448,19 @@ class MainWindow(
self.xsl_dependency_graph = XslDependencyGraph() 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 from ui.XslDependencyDialog import XslDependencyDialog
# open() statt exec() verwenden — QWebEngineView verträgt keinen verschachtelten Event-Loop # 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.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
self._xsl_dep_dialog.open() self._xsl_dep_dialog.open()
except Exception as e: except Exception as e:
+92 -13
View File
@@ -123,16 +123,24 @@ except ImportError:
class XslDependencyDialog(QDialog): class XslDependencyDialog(QDialog):
"""Dialog zur Anzeige des vollständigen XSL-Abhängigkeitsgraphen.""" """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: Args:
parent: Eltern-Widget parent: Eltern-Widget
xsl_root_dir: Wurzelverzeichnis der XSL-Dateien xsl_root_dir: Wurzelverzeichnis der XSL-Dateien
dependency_graph: XSL-Abhängigkeitsgraph-Instanz dependency_graph: XSL-Abhängigkeitsgraph-Instanz
project_xsl_paths: Absolute Pfade aller im Projekt referenzierten XSL-Dateien (None = kein Projekt)
""" """
super().__init__(parent) super().__init__(parent)
self.xsl_root_dir = xsl_root_dir self.xsl_root_dir = xsl_root_dir
self.dependency_graph = dependency_graph 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._web_view: "QWebEngineView | None" = None
self._graph_loaded = False self._graph_loaded = False
self._temp_html_file: tempfile.NamedTemporaryFile | None = None self._temp_html_file: tempfile.NamedTemporaryFile | None = None
@@ -660,37 +668,81 @@ class XslDependencyDialog(QDialog):
""" """
Konvertiert den Abhängigkeitsgraph in vis.js-kompatible JSON-Strukturen. 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: Returns:
tuple[str, str]: (nodes_json, edges_json) tuple[str, str]: (nodes_json, edges_json)
""" """
# Direkten Graph verwenden (nicht transitiv) # Direkten Graph verwenden (nicht transitiv)
direct_graph = self.dependency_graph.build_direct_graph(self.xsl_root_dir) direct_graph = self.dependency_graph.build_direct_graph(self.xsl_root_dir)
# Pfad → ID Mapping # Dateisystem-Pfade (Kategorie 1 und 2)
all_paths = sorted(direct_graph.keys(), key=lambda p: self._rel_path(p).lower()) 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(all_paths)} 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 = [] 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) rel = self._rel_path(path)
label = path.name label = path.name
direct_deps = len(direct_graph.get(path, set())) direct_deps = len(direct_graph.get(path, set()))
reverse_count = len(self._reverse_map.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}" value = max(direct_deps + reverse_count, 1)
# Knotengröße basierend auf Verbindungsanzahl in_project = path in self._project_xsl_paths
value = direct_deps + reverse_count
nodes.append( 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, "id": node_id,
"label": label, "label": label,
"title": title, "title": title,
"value": max(value, 1), "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": path_to_id[path],
"label": path.name,
"title": title,
"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 = [] edges = []
for path, deps in direct_graph.items(): for path, deps in direct_graph.items():
from_id = path_to_id.get(path) from_id = path_to_id.get(path)
@@ -757,6 +809,28 @@ class XslDependencyDialog(QDialog):
font-size: 13px !important; font-size: 13px !important;
box-shadow: 2px 2px 6px rgba(0,0,0,0.3) !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> </style>
<script type="text/javascript"> <script type="text/javascript">
{vis_js_content} {vis_js_content}
@@ -764,6 +838,11 @@ class XslDependencyDialog(QDialog):
</head> </head>
<body> <body>
<div id="graph-container"></div> <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"> <script type="text/javascript">
var nodesData = {nodes_json}; var nodesData = {nodes_json};
var edgesData = {edges_json}; var edgesData = {edges_json};