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>"""
+276 -145
View File
@@ -15,157 +15,281 @@
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>0</number>
<layout class="QHBoxLayout" name="toolbarLayout">
<item>
<spacer name="toolbarSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="settingsButton">
<property name="maximumSize">
<size>
<width>28</width>
<height>28</height>
</size>
</property>
<property name="toolTip">
<string>Einstellungen ein-/ausblenden</string>
</property>
<property name="text">
<string/>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QSplitter" name="mainSplitter">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<widget class="QWidget" name="treeTab">
<attribute name="title">
<string>Baumansicht</string>
</attribute>
<layout class="QVBoxLayout" name="treeTabLayout">
<item>
<layout class="QHBoxLayout" name="searchLayout">
<item>
<widget class="QLabel" name="searchLabel">
<property name="text">
<string>Suche:</string>
</property>
<property name="childrenCollapsible">
<bool>true</bool>
</property>
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="treeTab">
<attribute name="title">
<string>Baumansicht</string>
</attribute>
<layout class="QVBoxLayout" name="treeTabLayout">
<item>
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<widget class="QWidget" name="leftWidget" native="true">
<layout class="QVBoxLayout" name="leftLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="leftLabel">
<property name="text">
<string>XSL-Dateien</string>
</property>
</widget>
</item>
<item>
<widget class="QTreeWidget" name="fileTree">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SelectionMode::SingleSelection</enum>
</property>
<property name="rootIsDecorated">
<bool>true</bool>
</property>
<column>
<property name="text">
<string notr="true">1</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QLineEdit" name="searchEdit">
<property name="placeholderText">
<string>XSL-Datei filtern...</string>
</property>
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
<widget class="QWidget" name="rightWidget" native="true">
<layout class="QVBoxLayout" name="rightLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="rightLabel">
<property name="text">
<string>Abhängigkeiten</string>
</property>
</widget>
</item>
<item>
<widget class="QTreeWidget" name="depTree">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="rootIsDecorated">
<bool>true</bool>
</property>
<column>
<property name="text">
<string notr="true">1</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="graphTab">
<attribute name="title">
<string>Netzwerkgraph</string>
</attribute>
<layout class="QVBoxLayout" name="graphTabLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QWidget" name="graphContainer" native="true">
<layout class="QVBoxLayout" name="graphContainerLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
</layout>
</widget>
</item>
</layout>
</widget>
</widget>
<widget class="QWidget" name="sidebarWidget" native="true">
<layout class="QVBoxLayout" name="sidebarLayout">
<item>
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<widget class="QLabel" name="sidebarLabel">
<property name="font">
<font>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Einstellungen</string>
</property>
<widget class="QWidget" name="leftWidget" native="true">
<layout class="QVBoxLayout" name="leftLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="leftLabel">
<property name="text">
<string>XSL-Dateien</string>
</property>
</widget>
</item>
<item>
<widget class="QTreeWidget" name="fileTree">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="rootIsDecorated">
<bool>true</bool>
</property>
<column>
<property name="text">
<string notr="true">1</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="rightWidget" native="true">
<layout class="QVBoxLayout" name="rightLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="rightLabel">
<property name="text">
<string>Abhängigkeiten</string>
</property>
</widget>
</item>
<item>
<widget class="QTreeWidget" name="depTree">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="rootIsDecorated">
<bool>true</bool>
</property>
<column>
<property name="text">
<string notr="true">1</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="graphTab">
<attribute name="title">
<string>Netzwerkgraph</string>
</attribute>
<layout class="QVBoxLayout" name="graphTabLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QWidget" name="graphContainer" native="true">
<layout class="QVBoxLayout" name="graphContainerLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
</layout>
<widget class="QLabel" name="searchLabel">
<property name="text">
<string>Suche:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="searchEdit">
<property name="placeholderText">
<string>XSL-Datei filtern...</string>
</property>
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QStackedWidget" name="settingsStack">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="treeSettingsPage">
<layout class="QVBoxLayout" name="treeSettingsLayout">
<item>
<widget class="QLabel" name="treeSettingsLabel">
<property name="text">
<string>Baumansicht-Einstellungen</string>
</property>
</widget>
</item>
<item>
<spacer name="treeSettingsSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="graphSettingsPage">
<layout class="QVBoxLayout" name="graphSettingsLayout">
<item>
<widget class="QLabel" name="graphSettingsLabel">
<property name="text">
<string>Graph-Einstellungen</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="graphFilterConnectedCheck">
<property name="text">
<string>Nur betroffene Dateien anzeigen</string>
</property>
<property name="toolTip">
<string>Entfernt alle XSL-Dateien aus dem Graph, die nicht zum Suchbegriff passen und nicht direkt oder indirekt mit passenden Dateien verbunden sind</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="graphSettingsSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
@@ -174,6 +298,12 @@
</item>
<item>
<widget class="QLabel" name="statusLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string/>
</property>
@@ -182,10 +312,10 @@
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Close</set>
<set>QDialogButtonBox::StandardButton::Close</set>
</property>
<property name="centerButtons">
<bool>true</bool>
@@ -194,6 +324,7 @@
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
+114 -27
View File
@@ -15,10 +15,12 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QAbstractButton, QAbstractItemView, QApplication, QDialog,
QDialogButtonBox, QHBoxLayout, QHeaderView, QLabel,
QLineEdit, QSizePolicy, QSplitter, QTabWidget,
from PySide6.QtWidgets import (QAbstractButton, QAbstractItemView, QApplication, QCheckBox,
QDialog, QDialogButtonBox, QHBoxLayout, QHeaderView,
QLabel, QLineEdit, QPushButton, QSizePolicy,
QSpacerItem, QSplitter, QStackedWidget, QTabWidget,
QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget)
class Ui_XslDependencyDialog(object):
def setupUi(self, XslDependencyDialog):
if not XslDependencyDialog.objectName():
@@ -26,31 +28,35 @@ class Ui_XslDependencyDialog(object):
XslDependencyDialog.resize(1000, 700)
self.verticalLayout = QVBoxLayout(XslDependencyDialog)
self.verticalLayout.setObjectName(u"verticalLayout")
self.tabWidget = QTabWidget(XslDependencyDialog)
self.toolbarLayout = QHBoxLayout()
self.toolbarLayout.setObjectName(u"toolbarLayout")
self.toolbarSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
self.toolbarLayout.addItem(self.toolbarSpacer)
self.settingsButton = QPushButton(XslDependencyDialog)
self.settingsButton.setObjectName(u"settingsButton")
self.settingsButton.setMaximumSize(QSize(28, 28))
self.settingsButton.setFlat(True)
self.toolbarLayout.addWidget(self.settingsButton)
self.verticalLayout.addLayout(self.toolbarLayout)
self.mainSplitter = QSplitter(XslDependencyDialog)
self.mainSplitter.setObjectName(u"mainSplitter")
self.mainSplitter.setOrientation(Qt.Orientation.Horizontal)
self.mainSplitter.setChildrenCollapsible(True)
self.tabWidget = QTabWidget(self.mainSplitter)
self.tabWidget.setObjectName(u"tabWidget")
self.treeTab = QWidget()
self.treeTab.setObjectName(u"treeTab")
self.treeTabLayout = QVBoxLayout(self.treeTab)
self.treeTabLayout.setObjectName(u"treeTabLayout")
self.searchLayout = QHBoxLayout()
self.searchLayout.setObjectName(u"searchLayout")
self.searchLabel = QLabel(self.treeTab)
self.searchLabel.setObjectName(u"searchLabel")
self.searchLayout.addWidget(self.searchLabel)
self.searchEdit = QLineEdit(self.treeTab)
self.searchEdit.setObjectName(u"searchEdit")
self.searchEdit.setClearButtonEnabled(True)
self.searchLayout.addWidget(self.searchEdit)
self.treeTabLayout.addLayout(self.searchLayout)
self.splitter = QSplitter(self.treeTab)
self.splitter.setObjectName(u"splitter")
self.splitter.setOrientation(Qt.Horizontal)
self.splitter.setOrientation(Qt.Orientation.Horizontal)
self.leftWidget = QWidget(self.splitter)
self.leftWidget.setObjectName(u"leftWidget")
self.leftLayout = QVBoxLayout(self.leftWidget)
@@ -67,7 +73,7 @@ class Ui_XslDependencyDialog(object):
self.fileTree.setHeaderItem(__qtreewidgetitem)
self.fileTree.setObjectName(u"fileTree")
self.fileTree.setAlternatingRowColors(True)
self.fileTree.setSelectionMode(QAbstractItemView.SingleSelection)
self.fileTree.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
self.fileTree.setRootIsDecorated(True)
self.leftLayout.addWidget(self.fileTree)
@@ -112,18 +118,87 @@ class Ui_XslDependencyDialog(object):
self.graphTabLayout.addWidget(self.graphContainer)
self.tabWidget.addTab(self.graphTab, "")
self.mainSplitter.addWidget(self.tabWidget)
self.sidebarWidget = QWidget(self.mainSplitter)
self.sidebarWidget.setObjectName(u"sidebarWidget")
self.sidebarLayout = QVBoxLayout(self.sidebarWidget)
self.sidebarLayout.setObjectName(u"sidebarLayout")
self.sidebarLabel = QLabel(self.sidebarWidget)
self.sidebarLabel.setObjectName(u"sidebarLabel")
font = QFont()
font.setBold(True)
self.sidebarLabel.setFont(font)
self.verticalLayout.addWidget(self.tabWidget)
self.sidebarLayout.addWidget(self.sidebarLabel)
self.searchLabel = QLabel(self.sidebarWidget)
self.searchLabel.setObjectName(u"searchLabel")
self.sidebarLayout.addWidget(self.searchLabel)
self.searchEdit = QLineEdit(self.sidebarWidget)
self.searchEdit.setObjectName(u"searchEdit")
self.searchEdit.setClearButtonEnabled(True)
self.sidebarLayout.addWidget(self.searchEdit)
self.settingsStack = QStackedWidget(self.sidebarWidget)
self.settingsStack.setObjectName(u"settingsStack")
self.treeSettingsPage = QWidget()
self.treeSettingsPage.setObjectName(u"treeSettingsPage")
self.treeSettingsLayout = QVBoxLayout(self.treeSettingsPage)
self.treeSettingsLayout.setObjectName(u"treeSettingsLayout")
self.treeSettingsLabel = QLabel(self.treeSettingsPage)
self.treeSettingsLabel.setObjectName(u"treeSettingsLabel")
self.treeSettingsLayout.addWidget(self.treeSettingsLabel)
self.treeSettingsSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
self.treeSettingsLayout.addItem(self.treeSettingsSpacer)
self.settingsStack.addWidget(self.treeSettingsPage)
self.graphSettingsPage = QWidget()
self.graphSettingsPage.setObjectName(u"graphSettingsPage")
self.graphSettingsLayout = QVBoxLayout(self.graphSettingsPage)
self.graphSettingsLayout.setObjectName(u"graphSettingsLayout")
self.graphSettingsLabel = QLabel(self.graphSettingsPage)
self.graphSettingsLabel.setObjectName(u"graphSettingsLabel")
self.graphSettingsLayout.addWidget(self.graphSettingsLabel)
self.graphFilterConnectedCheck = QCheckBox(self.graphSettingsPage)
self.graphFilterConnectedCheck.setObjectName(u"graphFilterConnectedCheck")
self.graphFilterConnectedCheck.setChecked(True)
self.graphSettingsLayout.addWidget(self.graphFilterConnectedCheck)
self.graphSettingsSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
self.graphSettingsLayout.addItem(self.graphSettingsSpacer)
self.settingsStack.addWidget(self.graphSettingsPage)
self.sidebarLayout.addWidget(self.settingsStack)
self.mainSplitter.addWidget(self.sidebarWidget)
self.verticalLayout.addWidget(self.mainSplitter)
self.statusLabel = QLabel(XslDependencyDialog)
self.statusLabel.setObjectName(u"statusLabel")
sizePolicy = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Maximum)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.statusLabel.sizePolicy().hasHeightForWidth())
self.statusLabel.setSizePolicy(sizePolicy)
self.verticalLayout.addWidget(self.statusLabel)
self.buttonBox = QDialogButtonBox(XslDependencyDialog)
self.buttonBox.setObjectName(u"buttonBox")
self.buttonBox.setOrientation(Qt.Horizontal)
self.buttonBox.setStandardButtons(QDialogButtonBox.Close)
self.buttonBox.setOrientation(Qt.Orientation.Horizontal)
self.buttonBox.setStandardButtons(QDialogButtonBox.StandardButton.Close)
self.buttonBox.setCenterButtons(True)
self.verticalLayout.addWidget(self.buttonBox)
@@ -133,6 +208,7 @@ class Ui_XslDependencyDialog(object):
self.buttonBox.rejected.connect(XslDependencyDialog.reject)
self.tabWidget.setCurrentIndex(0)
self.settingsStack.setCurrentIndex(0)
QMetaObject.connectSlotsByName(XslDependencyDialog)
@@ -140,12 +216,23 @@ class Ui_XslDependencyDialog(object):
def retranslateUi(self, XslDependencyDialog):
XslDependencyDialog.setWindowTitle(QCoreApplication.translate("XslDependencyDialog", u"XSL-Abh\u00e4ngigkeitsgraph", None))
self.searchLabel.setText(QCoreApplication.translate("XslDependencyDialog", u"Suche:", None))
self.searchEdit.setPlaceholderText(QCoreApplication.translate("XslDependencyDialog", u"XSL-Datei filtern...", None))
#if QT_CONFIG(tooltip)
self.settingsButton.setToolTip(QCoreApplication.translate("XslDependencyDialog", u"Einstellungen ein-/ausblenden", None))
#endif // QT_CONFIG(tooltip)
self.settingsButton.setText("")
self.leftLabel.setText(QCoreApplication.translate("XslDependencyDialog", u"XSL-Dateien", None))
self.rightLabel.setText(QCoreApplication.translate("XslDependencyDialog", u"Abh\u00e4ngigkeiten", None))
self.tabWidget.setTabText(self.tabWidget.indexOf(self.treeTab), QCoreApplication.translate("XslDependencyDialog", u"Baumansicht", None))
self.tabWidget.setTabText(self.tabWidget.indexOf(self.graphTab), QCoreApplication.translate("XslDependencyDialog", u"Netzwerkgraph", None))
self.sidebarLabel.setText(QCoreApplication.translate("XslDependencyDialog", u"Einstellungen", None))
self.searchLabel.setText(QCoreApplication.translate("XslDependencyDialog", u"Suche:", None))
self.searchEdit.setPlaceholderText(QCoreApplication.translate("XslDependencyDialog", u"XSL-Datei filtern...", None))
self.treeSettingsLabel.setText(QCoreApplication.translate("XslDependencyDialog", u"Baumansicht-Einstellungen", None))
self.graphSettingsLabel.setText(QCoreApplication.translate("XslDependencyDialog", u"Graph-Einstellungen", None))
self.graphFilterConnectedCheck.setText(QCoreApplication.translate("XslDependencyDialog", u"Nur betroffene Dateien anzeigen", None))
#if QT_CONFIG(tooltip)
self.graphFilterConnectedCheck.setToolTip(QCoreApplication.translate("XslDependencyDialog", u"Entfernt alle XSL-Dateien aus dem Graph, die nicht zum Suchbegriff passen und nicht direkt oder indirekt mit passenden Dateien verbunden sind", None))
#endif // QT_CONFIG(tooltip)
self.statusLabel.setText("")
# retranslateUi
+26 -9
View File
@@ -7,13 +7,28 @@ Der Graph wird im Speicher gehalten und bei Änderungen (mtime) automatisch inva
"""
import logging
import re
import time
from lxml import etree
from pathlib import Path
logger = logging.getLogger(__name__)
# Regex für xsl:import und xsl:include href-Attribute
_IMPORT_INCLUDE_PATTERN = re.compile(r'<xsl:(?:import|include)\s+href=["\']([^"\']+)["\']', re.IGNORECASE)
_XSL_NAMESPACE = "http://www.w3.org/1999/XSL/Transform"
def _parse_import_include_hrefs(content: str) -> list[str]:
"""Parst XSL-Dateiinhalt und gibt alle href-Werte von xsl:import/xsl:include zurück."""
try:
root = etree.fromstring(content.encode("utf-8"))
except etree.XMLSyntaxError:
return []
hrefs = []
for tag in ("import", "include"):
for elem in root.iter(f"{{{_XSL_NAMESPACE}}}{tag}"):
href = elem.get("href")
if href:
hrefs.append(href)
return hrefs
class XslDependencyGraph:
@@ -84,8 +99,7 @@ class XslDependencyGraph:
return result
# Finde alle import/include-Referenzen
for match in _IMPORT_INCLUDE_PATTERN.finditer(content):
href = match.group(1)
for href in _parse_import_include_hrefs(content):
referenced_path = (xsl_file.parent / href).resolve()
if referenced_path.exists() and referenced_path not in visited:
@@ -138,12 +152,14 @@ class XslDependencyGraph:
if not xsl_root_dir.exists():
return graph
start = time.perf_counter()
for xsl_file in sorted(xsl_root_dir.rglob("*.xsl")):
xsl_file = xsl_file.resolve()
deps = self.get_dependencies(xsl_file)
graph[xsl_file] = deps
elapsed = time.perf_counter() - start
logger.info(f"Vollständiger XSL-Graph aufgebaut: {len(graph)} Dateien")
logger.info(f"Vollständiger XSL-Graph aufgebaut: {len(graph)} Dateien in {elapsed:.3f}s")
return graph
def _get_direct_dependencies(self, xsl_file: Path) -> set[Path]:
@@ -168,8 +184,7 @@ class XslDependencyGraph:
logger.warning(f"Konnte XSL-Datei nicht lesen: {xsl_file} ({e})")
return result
for match in _IMPORT_INCLUDE_PATTERN.finditer(content):
href = match.group(1)
for href in _parse_import_include_hrefs(content):
referenced_path = (xsl_file.parent / href).resolve()
if referenced_path.exists():
result.add(referenced_path)
@@ -191,11 +206,13 @@ class XslDependencyGraph:
if not xsl_root_dir.exists():
return graph
start = time.perf_counter()
for xsl_file in sorted(xsl_root_dir.rglob("*.xsl")):
xsl_file = xsl_file.resolve()
graph[xsl_file] = self._get_direct_dependencies(xsl_file)
elapsed = time.perf_counter() - start
logger.info(f"Direkter XSL-Graph aufgebaut: {len(graph)} Dateien")
logger.info(f"Direkter XSL-Graph aufgebaut: {len(graph)} Dateien in {elapsed:.3f}s")
return graph
def invalidate(self, xsl_file: Path | None = None):