Feat: Suchfilter für TreeNodes und XSL-Dateien im Hauptfenster (v1.3.0)

Neues Suchfeld über dem Baum filtert Knoten und XSL-Dateien per
case-insensitive Textsuche. Übergeordnete Knoten bleiben bei
Kind-Treffern sichtbar und werden automatisch expandiert. Der
gespeicherte Expand-Status wird beim Leeren der Suche wiederhergestellt.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-03 19:54:20 +02:00
parent 0560dbafe4
commit d1def05607
9 changed files with 136 additions and 11 deletions
+107 -2
View File
@@ -484,9 +484,112 @@ class TreeManagerMixin:
# Stelle Expand-Status wieder her
self._restore_expanded_state()
# Suchfilter erneut anwenden, falls aktiv
search_text = self.ui.searchEdit.text()
if search_text:
self._filter_tree(search_text)
except Exception as e:
logger.error(f"Fehler beim Laden der Nodes in TreeWidget: {e}")
def _filter_tree(self, text: str):
"""
Filtert das TreeWidget nach TreeNode- und XslFile-Bezeichnungen.
Sichtbarkeitsregeln:
- TreeNode/XslFile wird angezeigt, wenn .bez den Suchtext enthält (case-insensitive)
- Wenn ein Kind matcht, bleibt der übergeordnete Knoten sichtbar und wird expandiert
- Wenn ein TreeNode matcht, bleiben alle Kinder sichtbar
- XmlFile-Items folgen der Sichtbarkeit ihres Eltern-XslFile
- Leerer Suchtext blendet alles ein und stellt den Expand-Status wieder her
Args:
text: Suchtext für den Filter
"""
search_lower = text.strip().lower()
if not search_lower:
# Alles einblenden, kollabieren und gespeicherten Expand-Status wiederherstellen
for i in range(self.ui.treeWidget.topLevelItemCount()):
item = self.ui.treeWidget.topLevelItem(i)
self._reset_filter_recursive(item)
self._restore_expanded_state()
logger.debug("Baumfilter zurückgesetzt")
return
for i in range(self.ui.treeWidget.topLevelItemCount()):
item = self.ui.treeWidget.topLevelItem(i)
self._apply_filter_recursive(item, search_lower)
logger.debug(f"Baumfilter angewendet: '{text}'")
def _apply_filter_recursive(self, item: QTreeWidgetItem, search_lower: str) -> bool:
"""
Wendet den Suchfilter rekursiv auf ein Item und seine Kinder an.
Args:
item: Das zu prüfende TreeWidgetItem
search_lower: Suchtext in Kleinbuchstaben
Returns:
bool: True wenn dieses Item oder ein Kind den Suchtext enthält
"""
node = item.data(0, Qt.ItemDataRole.UserRole)
# Prüfe ob dieses Item selbst matcht (nur TreeNode und XslFile haben .bez)
self_matches = False
if isinstance(node, (TreeNode, XslFile)):
bez_text = str(node.bez).lower() if node.bez else ""
self_matches = search_lower in bez_text
# Wenn dieses Item matcht, sichtbar machen und alle Kinder einblenden
if self_matches:
item.setHidden(False)
self._set_item_visible_recursive(item, True)
item.setExpanded(True)
return True
# Prüfe Kinder rekursiv
any_child_matches = False
for child_idx in range(item.childCount()):
child = item.child(child_idx)
if self._apply_filter_recursive(child, search_lower):
any_child_matches = True
# Item sichtbar lassen wenn ein Kind matcht, expandieren für Übersicht
if any_child_matches:
item.setHidden(False)
item.setExpanded(True)
else:
item.setHidden(True)
return any_child_matches
def _set_item_visible_recursive(self, item: QTreeWidgetItem, visible: bool):
"""
Setzt die Sichtbarkeit eines Items und aller seiner Kinder.
Args:
item: Das TreeWidgetItem
visible: Ob das Item sichtbar sein soll
"""
item.setHidden(not visible)
for child_idx in range(item.childCount()):
self._set_item_visible_recursive(item.child(child_idx), visible)
def _reset_filter_recursive(self, item: QTreeWidgetItem):
"""
Blendet ein Item und alle Kinder ein und kollabiert sie.
Kombiniert Sichtbarkeit und Expand-Reset in einem Durchlauf.
Args:
item: Das TreeWidgetItem
"""
item.setHidden(False)
item.setExpanded(False)
for child_idx in range(item.childCount()):
self._reset_filter_recursive(item.child(child_idx))
def _create_tree_item_from_node(self, node):
"""
Erstellt ein QTreeWidgetItem aus einem TreeNode oder XslFile.
@@ -1622,8 +1725,10 @@ class TreeManagerMixin:
start_time = time.time()
# Speichere Expand-Status der Tree-Knoten vor dem Schreiben
self._save_expanded_state()
# Expand-Status nur speichern, wenn kein Suchfilter aktiv ist
# (sonst werden die durch die Suche erzwungenen Expansionen gespeichert)
if not self.ui.searchEdit.text().strip():
self._save_expanded_state()
# Speichere in project.yaml im Projekt-Verzeichnis
self.pdf_project.writeSettings(project_dir=self.project.project_dir)