Files
xsl-validator/src/ui/XmlToXslAssignDialog.py
T
info 9fad317891 Feat: XML-Knoten-Bearbeiten-Dialog implementiert (v1.6.0)
Neuer Dialog ermöglicht es, einem XML-Knoten XSL-Zuordnungen hinzuzufügen
oder zu entfernen. XmlToXslAssignDialog wiederverwendet mit edit_mode,
Vorauswahl per preselected_xsl_ids und get_selection_diff(). Beim Entfernen
werden zugehörige PDF-Dateien gelöscht; bei verbleibend leerer Zuordnung
wird das physische Löschen der XML-Datei angeboten.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 21:10:58 +02:00

396 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import logging
from PySide6.QtWidgets import QDialog, QTreeWidgetItem, QCheckBox, QMessageBox, QWidget, QHBoxLayout
from PySide6.QtCore import Qt
from pathlib import Path
from ui.XmlToXslAssignDialog_ui import Ui_XmlToXslAssignDialog
from conf import TreeNode, XslFile
logger = logging.getLogger(__name__)
class XmlToXslAssignDialog(QDialog):
"""Dialog zur Zuordnung einer XML-Datei zu XSL-Knoten."""
def __init__(
self,
parent=None,
xml_file_path=None,
project_nodes=None,
preselected_xsl_ids: set | None = None,
edit_mode: bool = False,
):
"""
Initialisiert den Dialog.
Args:
parent: Übergeordnetes Widget
xml_file_path: Pfad zur XML-Datei
project_nodes: Liste der Projekt-Knoten
preselected_xsl_ids: Set von XslFile-IDs (tuple), deren Checkbox initial angehakt sein soll
edit_mode: True = Bearbeiten-Modus (Zuordnungen ändern), False = Neu-Zuordnen
"""
super().__init__(parent)
# UI einrichten
self.ui = Ui_XmlToXslAssignDialog()
self.ui.setupUi(self)
# Parameter speichern
self.xml_file_path = Path(xml_file_path) if xml_file_path else None
self.project_nodes = project_nodes or []
self.preselected_xsl_ids: set = set(preselected_xsl_ids) if preselected_xsl_ids else set()
self.edit_mode = edit_mode
# Dictionary zum Speichern der Checkbox-Referenzen
self.xsl_checkboxes = {} # {python_id(node): checkbox}
self.xsl_nodes = {} # {python_id(node): XslFile}
# Initial ausgewählte XslFile-IDs (tuple), um Diff beim Accept zu berechnen
self._initial_selected_ids: set = set()
# Edit-Modus: UI anpassen
if self.edit_mode:
self.setWindowTitle("XML-Zuordnungen bearbeiten")
self.ui.infoLabel.setText(
"Passen Sie die Zuordnungen der XML-Datei an. Hinzufügen per Haken, "
"Entfernen durch Abhaken (zugehörige PDFs werden gelöscht):"
)
self.ui.alle_xml.setVisible(False)
# Signale verbinden
self.ui.selectAllButton.clicked.connect(self.select_all)
self.ui.deselectAllButton.clicked.connect(self.deselect_all)
# Baum konfigurieren
self._setup_tree()
# Daten laden
self._load_data()
# Duplikat-Warnung nur im Edit-Modus (um bestehende Aufrufer nicht bei jeder XML zu stören)
if self.edit_mode:
self._warn_on_duplicate_xsl_ids()
def _setup_tree(self):
"""Konfiguriert das TreeWidget."""
# Spaltenbreiten setzen
self.ui.xslNodesTree.setColumnWidth(0, 300) # XSL-Knoten
self.ui.xslNodesTree.setColumnWidth(1, 200) # Details
self.ui.xslNodesTree.setColumnWidth(2, 100) # Auswählen
# Header-Eigenschaften
self.ui.xslNodesTree.header().setStretchLastSection(False)
def _create_centered_checkbox(self):
"""
Erstellt eine zentrierte Checkbox in einem Widget.
Returns:
tuple: (widget, checkbox) - Das Container-Widget und die Checkbox
"""
# Erstelle Container-Widget
widget = QWidget()
# Erstelle horizontales Layout
layout = QHBoxLayout(widget)
layout.setContentsMargins(0, 0, 0, 0)
layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
# Erstelle Checkbox
checkbox = QCheckBox()
checkbox.setChecked(False)
# Füge Checkbox zum Layout hinzu
layout.addWidget(checkbox)
return widget, checkbox
def _add_checkboxes_to_tree(self):
"""
Fügt Checkboxen zu allen XSL-Knoten im Tree hinzu.
Diese Methode wird nach dem Hinzufügen aller Items aufgerufen.
"""
try:
# Durchlaufe alle Items im Tree rekursiv
root = self.ui.xslNodesTree.invisibleRootItem()
self._add_checkboxes_recursive(root)
logger.debug(f"Checkboxen zu {len(self.xsl_checkboxes)} XSL-Knoten hinzugefügt")
except Exception as e:
logger.error(f"Fehler beim Hinzufügen der Checkboxen: {e}")
def _add_checkboxes_recursive(self, parent_item):
"""
Fügt rekursiv Checkboxen zu XSL-Knoten hinzu.
Args:
parent_item: Das Eltern-Item
"""
for i in range(parent_item.childCount()):
item = parent_item.child(i)
# Hole das Node-Objekt
node = item.data(0, Qt.ItemDataRole.UserRole)
if isinstance(node, XslFile):
# Erstelle zentrierte Checkbox für XSL-Knoten
checkbox_widget, checkbox = self._create_centered_checkbox()
# Vorauswahl setzen, wenn ID in preselected_xsl_ids
if node.id in self.preselected_xsl_ids:
checkbox.setChecked(True)
self._initial_selected_ids.add(node.id)
# Setze das Widget in Spalte 2
self.ui.xslNodesTree.setItemWidget(item, 2, checkbox_widget)
# Speichere Checkbox- und Node-Referenzen (id() als Key, da XSL-IDs theoretisch doppelt sein können)
self.xsl_checkboxes[id(node)] = checkbox
self.xsl_nodes[id(node)] = node
logger.debug(f"Checkbox für XSL-Knoten '{node.bez}' hinzugefügt")
# Rekursiv für Kinder
if item.childCount() > 0:
self._add_checkboxes_recursive(item)
def _load_data(self):
"""Lädt die Daten in den Dialog."""
# XML-Datei-Label setzen
if self.xml_file_path:
self.ui.xmlFileLabel.setText(f"XML-Datei: {self.xml_file_path.name}")
# Projekt-Knoten in Baum laden
self._load_project_nodes()
def _load_project_nodes(self):
"""Lädt die Projekt-Knoten in das TreeWidget (ohne XML-Knoten).
Sortiert die Items alphabetisch nach ihrer ID."""
if not self.project_nodes:
return
# TreeWidget leeren
self.ui.xslNodesTree.clear()
self.xsl_checkboxes.clear()
self.xsl_nodes.clear()
self._initial_selected_ids.clear()
# Sortiere Root-Nodes alphabetisch nach ID
sorted_nodes = sorted(self.project_nodes, key=lambda node: node.id)
# Alle Root-Nodes laden (sortiert)
for node in sorted_nodes:
tree_item = self._create_tree_item_from_node(node)
if tree_item: # Nur hinzufügen wenn Item erstellt wurde
self.ui.xslNodesTree.addTopLevelItem(tree_item)
# Baum expandieren
self.ui.xslNodesTree.expandAll()
# Checkboxen nach dem Hinzufügen der Items erstellen
self._add_checkboxes_to_tree()
def _create_tree_item_from_node(self, node):
"""
Erstellt ein QTreeWidgetItem aus einem TreeNode oder XslFile.
XML-Knoten werden ausgeschlossen.
Args:
node: TreeNode oder XslFile Objekt
Returns:
QTreeWidgetItem: Das erstellte Tree-Item oder None wenn ausgeschlossen
"""
try:
# Erstelle Tree-Item
item = QTreeWidgetItem()
# Setze die Bezeichnung in Spalte 0
bez_text = str(node.bez) if node.bez else ""
item.setText(0, bez_text)
# Speichere das komplette Node-Objekt
item.setData(0, Qt.ItemDataRole.UserRole, node)
if isinstance(node, TreeNode):
# TreeNode: Zeige Anzahl der Knoten
child_count = len(node.children) if node.children else 0
item.setText(1, f"{child_count} Knoten")
# Lade Knoten rekursiv (sortiert nach ID, nur TreeNode und XslFile, keine XML)
if node.children:
# Filtere und sortiere Kinder
valid_children = [child for child in node.children if isinstance(child, (TreeNode, XslFile))]
sorted_children = sorted(valid_children, key=lambda child: child.id)
for child in sorted_children:
child_item = self._create_tree_item_from_node(child)
if child_item:
item.addChild(child_item)
elif isinstance(node, XslFile):
# XslFile: Zeige XSL-Datei-Pfad
item.setText(1, str(node.xsl_file))
# Checkbox wird später in _add_checkboxes_to_tree() hinzugefügt
# Keine Knoten für XslFile hinzufügen (XML-Dateien werden ausgeschlossen)
return item
except Exception as e:
logger.error(f"Fehler beim Erstellen des Tree-Items: {e}")
return None
def select_all(self):
"""Wählt alle XSL-Knoten aus."""
for checkbox in self.xsl_checkboxes.values():
checkbox.setChecked(True)
def deselect_all(self):
"""Wählt alle XSL-Knoten ab."""
for checkbox in self.xsl_checkboxes.values():
checkbox.setChecked(False)
def get_selected_xsl_nodes(self):
"""
Gibt die ausgewählten XSL-Knoten zurück.
Returns:
list[XslFile]: Liste der ausgewählten XSL-Knoten
"""
selected_nodes = []
try:
# Durchlaufe alle XSL-Checkboxes
for node_id, checkbox in self.xsl_checkboxes.items():
if checkbox.isChecked():
# Finde den entsprechenden XSL-Knoten
xsl_node = self._find_xsl_node_by_id(node_id)
if xsl_node:
selected_nodes.append(xsl_node)
return selected_nodes
except Exception as e:
logger.error(f"Fehler beim Sammeln der ausgewählten XSL-Knoten: {e}")
return []
def _find_xsl_node_by_id(self, node_id):
"""
Findet einen XSL-Knoten anhand seiner ID.
Args:
node_id: Die ID des Knotens (Python id())
Returns:
XslFile: Der gefundene XSL-Knoten oder None
"""
return self._find_xsl_node_recursive(self.project_nodes, node_id)
def _find_xsl_node_recursive(self, nodes, target_id):
"""
Sucht rekursiv nach einem XSL-Knoten mit der angegebenen ID.
Args:
nodes: Liste der Nodes zum Durchsuchen
target_id: Die zu suchende ID
Returns:
XslFile: Der gefundene XSL-Knoten oder None
"""
for node in nodes:
if isinstance(node, XslFile) and id(node) == target_id:
return node
# Rekursiv in Knoten suchen (nur bei TreeNode)
if isinstance(node, TreeNode) and node.children:
found = self._find_xsl_node_recursive(node.children, target_id)
if found:
return found
return None
def get_xml_file_path(self):
"""
Gibt den Pfad zur XML-Datei zurück.
Returns:
Path: Pfad zur XML-Datei
"""
return self.xml_file_path
def is_apply_to_all(self):
"""
Prüft, ob die Checkbox 'Alle XML-Dateien' aktiviert ist.
Returns:
bool: True wenn die Checkbox aktiviert ist, sonst False
"""
return self.ui.alle_xml.isChecked()
def get_selection_diff(self) -> tuple[list, list]:
"""
Gibt die Änderungen der Auswahl gegenüber der Vorauswahl zurück.
Nützlich im Edit-Modus, um nur die tatsächlich geänderten Zuordnungen zu verarbeiten.
Returns:
tuple[list[XslFile], list[XslFile]]: (added_nodes, removed_nodes)
- added_nodes: XslFile-Objekte, die neu angehakt wurden
- removed_nodes: XslFile-Objekte, deren Haken entfernt wurde
"""
added_nodes = []
removed_nodes = []
for py_id, checkbox in self.xsl_checkboxes.items():
node = self.xsl_nodes.get(py_id)
if node is None:
continue
was_selected = node.id in self._initial_selected_ids
is_selected = checkbox.isChecked()
if is_selected and not was_selected:
added_nodes.append(node)
elif not is_selected and was_selected:
removed_nodes.append(node)
return added_nodes, removed_nodes
def _warn_on_duplicate_xsl_ids(self):
"""
Zeigt eine Warnung, wenn im Projekt mehrere XslFile-Instanzen mit identischer ID existieren.
Die ID soll eindeutig sein - Duplikate weisen auf einen Datenfehler hin.
"""
id_to_nodes: dict = {}
for py_id, node in self.xsl_nodes.items():
id_to_nodes.setdefault(node.id, []).append(node)
duplicates = {xsl_id: nodes for xsl_id, nodes in id_to_nodes.items() if len(nodes) > 1}
if not duplicates:
return
dup_lines = [f" - ID {xsl_id}: {len(nodes)}× ({', '.join(n.bez for n in nodes)})" for xsl_id, nodes in duplicates.items()]
logger.warning(f"Doppelte XSL-IDs im Projekt gefunden:\n{chr(10).join(dup_lines)}")
QMessageBox.warning(
self,
"Doppelte XSL-IDs",
"Im Projekt existieren XSL-Knoten mit identischer ID. "
"Die IDs sollten eindeutig sein:\n\n" + "\n".join(dup_lines),
)
def accept(self):
"""Überschreibt accept() um Validierung durchzuführen."""
# Im Edit-Modus: 0 Auswahlen erlauben (bedeutet: XML überall entfernen)
if self.edit_mode:
super().accept()
return
selected_nodes = self.get_selected_xsl_nodes()
if not selected_nodes:
QMessageBox.warning(self, "Warnung", "Bitte wählen Sie mindestens einen XSL-Knoten aus.")
return
super().accept()