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>
This commit is contained in:
2026-03-16 21:15:16 +01:00
parent 48ab596476
commit 3dcbf783b1
6 changed files with 639 additions and 183 deletions
+158 -2
View File
@@ -47,9 +47,17 @@ class XslDependencyDialog(QDialog):
self._graph_loaded = False
self._temp_html_file: tempfile.NamedTemporaryFile | None = None
self._sidebar_visible = False
self.ui = Ui_XslDependencyDialog()
self.ui.setupUi(self)
# Sidebar initial ausblenden
self.ui.settingsButton.setIcon(QIcon.fromTheme("preferences-system"))
self.ui.sidebarWidget.setVisible(False)
self.ui.mainSplitter.setSizes([1000, 0])
self.ui.settingsButton.clicked.connect(self._toggle_sidebar)
# Netzwerkgraph-Tab ausblenden wenn WebEngine nicht verfügbar
if not HAS_WEBENGINE:
self.ui.tabWidget.removeTab(1)
@@ -72,6 +80,9 @@ class XslDependencyDialog(QDialog):
self.ui.fileTree.currentItemChanged.connect(self._on_file_selected)
self.ui.searchEdit.textChanged.connect(self._on_search_changed)
self.ui.tabWidget.currentChanged.connect(self._on_tab_changed)
self.ui.graphFilterConnectedCheck.toggled.connect(
lambda: self._on_search_changed(self.ui.searchEdit.text())
)
def closeEvent(self, event):
"""Räumt temporäre Dateien und QWebEngineView auf."""
@@ -197,17 +208,46 @@ class XslDependencyDialog(QDialog):
self.ui.depTree.addTopLevelItem(no_deps_item)
def _on_search_changed(self, text: str):
"""Filtert je nach aktivem Tab die Dateiliste oder den Netzwerkgraph."""
current_tab = self.ui.tabWidget.currentIndex()
if current_tab == 0:
self._search_tree(text)
elif current_tab == 1 and self._web_view is not None:
self._search_graph(text)
def _search_tree(self, text: str):
"""Filtert die Dateiliste nach dem Suchbegriff."""
search_lower = text.lower()
for i in range(self.ui.fileTree.topLevelItemCount()):
item = self.ui.fileTree.topLevelItem(i)
matches = search_lower in item.text(0).lower()
matches = not search_lower or search_lower in item.text(0).lower()
item.setHidden(not matches)
def _search_graph(self, text: str):
"""Filtert oder dimmt Knoten im Netzwerkgraph je nach Checkbox-Einstellung."""
escaped = text.replace("\\", "\\\\").replace("'", "\\'")
hide_unrelated = str(self.ui.graphFilterConnectedCheck.isChecked()).lower()
self._web_view.page().runJavaScript(f"filterNodes('{escaped}', {hide_unrelated})")
# === Sidebar ===
def _toggle_sidebar(self):
"""Blendet die Einstellungs-Sidebar ein oder aus."""
self._sidebar_visible = not self._sidebar_visible
if self._sidebar_visible:
self.ui.sidebarWidget.setVisible(True)
self.ui.mainSplitter.setSizes([750, 250])
else:
self.ui.mainSplitter.setSizes([1000, 0])
self.ui.sidebarWidget.setVisible(False)
# === Netzwerkgraph (vis.js) ===
def _on_tab_changed(self, index: int):
"""Lazy-Init des Netzwerkgraphs beim ersten Tab-Wechsel."""
"""Lazy-Init des Netzwerkgraphs beim ersten Tab-Wechsel und Sidebar-Seite synchronisieren."""
# Sidebar-Seite synchronisieren
self.ui.settingsStack.setCurrentIndex(index)
if not HAS_WEBENGINE:
return
@@ -215,6 +255,11 @@ class XslDependencyDialog(QDialog):
if index == 1 and not self._graph_loaded:
self._setup_network_graph()
# Suchbegriff auf den neuen Tab anwenden
search_text = self.ui.searchEdit.text()
if search_text:
self._on_search_changed(search_text)
def _setup_network_graph(self):
"""Erstellt QWebEngineView und lädt den vis.js Netzwerkgraph."""
self._web_view = QWebEngineView(self.ui.graphContainer)
@@ -474,6 +519,117 @@ class XslDependencyDialog(QDialog):
}});
edges.update(edgeUpdates);
}});
// Suchfunktion: Knoten nach Label filtern
function filterNodes(searchTerm, hideUnrelated) {{
var term = searchTerm.toLowerCase();
if (hideUnrelated) {{
// Originalzustand wiederherstellen
nodes.clear();
edges.clear();
nodes.add(nodesData);
edges.add(edgesData);
allNodes = nodes.get();
if (!term) return;
// 1. Matching-Knoten finden
var matchedIds = new Set();
nodesData.forEach(function(node) {{
if (node.label.toLowerCase().indexOf(term) !== -1) {{
matchedIds.add(node.id);
}}
}});
// 2. Gerichtete Adjazenzlisten aufbauen
var fwd = {{}}; // from → to (Import-Richtung)
var bwd = {{}}; // to → from (Reverse)
edgesData.forEach(function(edge) {{
if (!fwd[edge.from]) fwd[edge.from] = [];
if (!bwd[edge.to]) bwd[edge.to] = [];
fwd[edge.from].push(edge.to);
bwd[edge.to].push(edge.from);
}});
// 3. BFS vorwärts (Nachkommen) + rückwärts (Vorfahren)
var reachable = new Set(matchedIds);
var queue = Array.from(matchedIds);
while (queue.length > 0) {{
var current = queue.shift();
(fwd[current] || []).forEach(function(n) {{
if (!reachable.has(n)) {{ reachable.add(n); queue.push(n); }}
}});
}}
queue = Array.from(matchedIds);
while (queue.length > 0) {{
var current = queue.shift();
(bwd[current] || []).forEach(function(n) {{
if (!reachable.has(n)) {{ reachable.add(n); queue.push(n); }}
}});
}}
// 4. Nicht-erreichbare Knoten und Kanten entfernen
var removeNodeIds = [];
nodesData.forEach(function(node) {{
if (!reachable.has(node.id)) removeNodeIds.push(node.id);
}});
nodes.remove(removeNodeIds);
var removeEdgeIds = [];
edgesData.forEach(function(edge) {{
if (!reachable.has(edge.from) || !reachable.has(edge.to))
removeEdgeIds.push(edge.id);
}});
edges.remove(removeEdgeIds);
// Matching-Knoten hervorheben, verbundene leicht dimmen
var updates = [];
matchedIds.forEach(function(id) {{
updates.push({{ id: id, opacity: 1.0 }});
}});
reachable.forEach(function(id) {{
if (!matchedIds.has(id)) {{
updates.push({{ id: id, opacity: 0.5 }});
}}
}});
nodes.update(updates);
allNodes = nodes.get();
}} else {{
// Originalzustand wiederherstellen falls vorher gefiltert
nodes.clear();
edges.clear();
nodes.add(nodesData);
edges.add(edgesData);
allNodes = nodes.get();
// Bisheriges Verhalten: nur dimmen
var updates = [];
var matchedIds = new Set();
allNodes.forEach(function(node) {{
if (!term || node.label.toLowerCase().indexOf(term) !== -1) {{
updates.push({{ id: node.id, opacity: 1.0 }});
matchedIds.add(node.id);
}} else {{
updates.push({{ id: node.id, opacity: 0.12 }});
}}
}});
nodes.update(updates);
var allEdges = edges.get();
var edgeUpdates = [];
allEdges.forEach(function(edge) {{
if (!term || (matchedIds.has(edge.from) && matchedIds.has(edge.to))) {{
edgeUpdates.push({{ id: edge.id, color: {{ opacity: 0.7 }} }});
}} else {{
edgeUpdates.push({{ id: edge.id, color: {{ opacity: 0.05 }} }});
}}
}});
edges.update(edgeUpdates);
}}
}}
</script>
</body>
</html>"""