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:
@@ -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>"""
|
||||
|
||||
Reference in New Issue
Block a user