diff --git a/functions/__init__.py b/functions/__init__.py index cfbfc4a..8b06c1d 100644 --- a/functions/__init__.py +++ b/functions/__init__.py @@ -15,7 +15,7 @@ from .ly_metadata_wrapper import ( is_layer_editable, ) from .ly_style_wrapper import apply_style -from .dialog_wrapper import ask_yes_no +from .dialog_wrapper import ask_yes_no, ask_overwrite_append_cancel_custom from .message_wrapper import ( _get_message_bar, diff --git a/functions/dialog_wrapper.py b/functions/dialog_wrapper.py index f91c4bf..71bfc48 100644 --- a/functions/dialog_wrapper.py +++ b/functions/dialog_wrapper.py @@ -2,8 +2,10 @@ sn_basis/functions/dialog_wrapper.py – Benutzer-Dialoge (Qt5/6/Mock-kompatibel) """ from typing import Any +from typing import Literal, Optional from sn_basis.functions.qt_wrapper import ( - QMessageBox, YES, NO, QT_VERSION + QMessageBox, YES, NO, CANCEL, QT_VERSION, exec_dialog, ICON_QUESTION, + ) def ask_yes_no( @@ -35,3 +37,48 @@ def ask_yes_no( except Exception as e: print(f"⚠️ ask_yes_no Fehler: {e}") return default + + +OverwriteDecision = Optional[Literal["overwrite", "append", "cancel"]] + + +def ask_overwrite_append_cancel_custom( + parent, + title: str, + message: str, +) -> Literal["overwrite", "append", "cancel"]: + """Zeigt Dialog mit benutzerdefinierten Buttons: Überschreiben/Anhängen/Abbrechen. + + Parameters + ---------- + parent : + Eltern-Widget oder None. + title : str + Dialog-Titel. + message : str + Hauptmeldung mit Erklärung. + + Returns + ------- + Literal["overwrite", "append", "cancel"] + Genaue Entscheidung des Nutzers. + """ + msg = QMessageBox(parent) + msg.setIcon(ICON_QUESTION) + msg.setWindowTitle(title) + msg.setText(message) + + # Eigene Buttons mit exakten Texten + overwrite_btn = msg.addButton("Überschreiben", QMessageBox.ButtonRole.AcceptRole) + append_btn = msg.addButton("Anhängen", QMessageBox.ButtonRole.ActionRole) + cancel_btn = msg.addButton("Abbrechen", QMessageBox.ButtonRole.RejectRole) + + exec_dialog(msg) + + clicked = msg.clickedButton() + if clicked == overwrite_btn: + return "overwrite" + elif clicked == append_btn: + return "append" + else: # cancel_btn + return "cancel" diff --git a/functions/ly_style_wrapper.py b/functions/ly_style_wrapper.py index 3145532..ad0221a 100644 --- a/functions/ly_style_wrapper.py +++ b/functions/ly_style_wrapper.py @@ -1,23 +1,44 @@ # sn_basis/functions/ly_style_wrapper.py from sn_basis.functions.ly_existence_wrapper import layer_exists -from sn_basis.functions.sys_wrapper import ( - get_plugin_root, - join_path, - file_exists, -) - +from sn_basis.functions.sys_wrapper import get_plugin_root, join_path +from sn_basis.modules.stilpruefer import Stilpruefer +from typing import Optional def apply_style(layer, style_name: str) -> bool: + """ + Wendet einen Layerstil an, sofern er gültig ist. + + - Validierung erfolgt ausschließlich über Stilpruefer + - Keine eigenen Dateisystem- oder Endungsprüfungen + - Keine Seiteneffekte bei ungültigem Stil + """ + print(">>> apply_style() START") + if not layer_exists(layer): return False - style_path = join_path(get_plugin_root(), "styles", style_name) - if not file_exists(style_path): + # Stilpfad zusammensetzen + style_path = join_path(get_plugin_root(), "sn_verfahrensgebiet","styles", style_name) + + # Stil prüfen + pruefer = Stilpruefer() + ergebnis = pruefer.pruefe(style_path) + print(">>> Stilprüfung:", ergebnis) + + print( + f"[Stilprüfung] ok={ergebnis.ok} | " + f"aktion={ergebnis.aktion} | " + f"meldung={ergebnis.meldung}" +) + + + if not ergebnis.ok: return False + # Stil anwenden try: - ok, _ = layer.loadNamedStyle(style_path) + ok, _ = layer.loadNamedStyle(str(ergebnis.kontext)) if ok: getattr(layer, "triggerRepaint", lambda: None)() return True diff --git a/functions/qgiscore_wrapper.py b/functions/qgiscore_wrapper.py index 4a3905e..2473fbf 100644 --- a/functions/qgiscore_wrapper.py +++ b/functions/qgiscore_wrapper.py @@ -36,6 +36,9 @@ try: Qgis as _Qgis, QgsMapLayerProxyModel as _QgsMaplLayerProxyModel, QgsVectorFileWriter as _QgsVectorFileWriter, + QgsFeature as _QgsFeature, + QgsField as _QgsField, + QgsGeometry as _QgsGeometry, ) QgsProject = _QgsProject @@ -45,6 +48,9 @@ try: Qgis = _Qgis QgsMapLayerProxyModel = _QgsMaplLayerProxyModel QgsVectorFileWriter = _QgsVectorFileWriter + QgsFeature = _QgsFeature + QgsField = _QgsField + QgsGeometry = _QgsGeometry QGIS_AVAILABLE = True diff --git a/functions/qt_wrapper.py b/functions/qt_wrapper.py index 09dfa40..d0c325f 100644 --- a/functions/qt_wrapper.py +++ b/functions/qt_wrapper.py @@ -11,6 +11,7 @@ NO: Optional[Any] = None CANCEL: Optional[Any] = None ICON_QUESTION: Optional[Any] = None + # Qt-Klassen (werden dynamisch gesetzt) QDockWidget: Type[Any] = object QMessageBox: Type[Any] = object @@ -36,6 +37,8 @@ QToolButton: Type[Any] = object QSizePolicy: Type[Any] = object Qt: Type[Any] = object QComboBox: Type[Any] = object +QHBoxLayout: Type[Any] = object + def exec_dialog(dialog: Any) -> Any: """Führt Dialog modal aus (Qt6: exec(), Qt5: exec_(), Mock: YES)""" @@ -77,12 +80,14 @@ try: QToolButton as _QToolButton, QSizePolicy as _QSizePolicy, QComboBox as _QComboBox, + QHBoxLayout as _QHBoxLayout, ) from qgis.PyQt.QtCore import ( QEventLoop as _QEventLoop, QUrl as _QUrl, QCoreApplication as _QCoreApplication, Qt as _Qt, + QVariant as _QVariant ) from qgis.PyQt.QtNetwork import ( QNetworkRequest as _QNetworkRequest, @@ -115,12 +120,16 @@ try: QToolButton = _QToolButton QSizePolicy = _QSizePolicy QComboBox = _QComboBox - + QVariant = _QVariant + QHBoxLayout= _QHBoxLayout # ✅ QT6 ENUMS YES = QMessageBox.StandardButton.Yes NO = QMessageBox.StandardButton.No CANCEL = QMessageBox.StandardButton.Cancel ICON_QUESTION = QMessageBox.Icon.Question + AcceptRole = QMessageBox.ButtonRole.AcceptRole + ActionRole = QMessageBox.ButtonRole.ActionRole + RejectRole = QMessageBox.ButtonRole.RejectRole # Qt6 Enum-Aliase ToolButtonTextBesideIcon = Qt.ToolButtonStyle.ToolButtonTextBesideIcon @@ -161,12 +170,14 @@ except (ImportError, AttributeError): QToolButton as _QToolButton, QSizePolicy as _QSizePolicy, QComboBox as _QComboBox, + QHBoxLayout as _QHBoxLayout, ) from PyQt5.QtCore import ( QEventLoop as _QEventLoop, QUrl as _QUrl, QCoreApplication as _QCoreApplication, Qt as _Qt, + QVariant as _QVariant ) from PyQt5.QtNetwork import ( QNetworkRequest as _QNetworkRequest, @@ -199,12 +210,18 @@ except (ImportError, AttributeError): QToolButton = _QToolButton QSizePolicy = _QSizePolicy QComboBox = _QComboBox + QVariant = _QVariant + QHBoxLayout = _QHBoxLayout # ✅ PYQT5 ENUMS YES = QMessageBox.Yes NO = QMessageBox.No CANCEL = QMessageBox.Cancel ICON_QUESTION = QMessageBox.Question + AcceptRole = QMessageBox.AcceptRole + ActionRole = QMessageBox.ActionRole + RejectRole = QMessageBox.RejectRole + # PyQt5 Enum-Aliase ToolButtonTextBesideIcon = Qt.ToolButtonTextBesideIcon @@ -244,6 +261,10 @@ except (ImportError, AttributeError): No = NO Cancel = CANCEL Question = ICON_QUESTION + AcceptRole = 0 + ActionRole = 3 + RejectRole = 1 + @classmethod def question(cls, parent, title, message, buttons, default_button): @@ -423,9 +444,71 @@ except (ImportError, AttributeError): QComboBox = _MockComboBox + + # --------------------------- + # Mock für QVariant + # --------------------------- + + class _MockQVariant: + """ + Minimaler Ersatz für QtCore.QVariant. + + Ziel: + - Werte transparent durchreichen + - Typ-Konstanten bereitstellen + - Keine Qt-Abhängigkeiten + """ + + # Typ-Konstanten (symbolisch, Werte egal) + Invalid = 0 + Int = 1 + Double = 2 + String = 3 + Bool = 4 + Date = 5 + DateTime = 6 + + def __init__(self, value: Any = None): + self._value = value + + def value(self) -> Any: + return self._value + + def __repr__(self) -> str: + return f"QVariant({self._value!r})" + + # Optional: automatische Entpackung + def __int__(self): + return int(self._value) + + def __float__(self): + return float(self._value) + + def __str__(self): + return str(self._value) + QVariant = _MockQVariant + + class _MockQHBoxLayout: + def __init__(self, *args, **kwargs): + self._widgets = [] + + def addWidget(self, widget): + self._widgets.append(widget) + + def addLayout(self, layout): + pass + + def addStretch(self, *args, **kwargs): + pass + + def setSpacing(self, *args, **kwargs): + pass + + def setContentsMargins(self, *args, **kwargs): + pass + QHBoxLayout = _MockQHBoxLayout def exec_dialog(dialog: Any) -> Any: return YES - # --------------------------- TEST --------------------------- if __name__ == "__main__": debug_qt_status() diff --git a/modules/Pruefmanager.py b/modules/Pruefmanager.py index 3ea40ec..96bb8b5 100644 --- a/modules/Pruefmanager.py +++ b/modules/Pruefmanager.py @@ -5,7 +5,7 @@ sn_basis/modules/Pruefmanager.py from __future__ import annotations from typing import Optional, Any -from sn_basis.functions import ask_yes_no, info, warning, error +from sn_basis.functions import ask_yes_no, info, warning, error, ask_overwrite_append_cancel_custom from sn_basis.modules.pruef_ergebnis import pruef_ergebnis, PruefAktion print("DEBUG: Pruefmanager DATEI GELADEN:", __file__) @@ -60,6 +60,26 @@ class Pruefmanager: # VERFAHRENS-DB-spezifische Entscheidungen # ------------------------------------------------------------------ def _handle_datei_existiert(self, ergebnis: pruef_ergebnis) -> pruef_ergebnis: + """Handhabt das Szenario, dass die Ziel-Verfahrens-DB bereits existiert. + + Zeigt einen einzigen Dialog mit drei Optionen an: + - **Überschreiben**: Bestehende Layer ersetzen (entspricht YES) + - **Anhängen**: Neue Layer zur Datei hinzufügen (entspricht NO) + - **Abbrechen**: Vorgang beenden (entspricht CANCEL) + + Parameters + ---------- + ergebnis : pruef_ergebnis + Eingabe-Ergebnis mit Dateipfad im ``kontext``-Attribut. + + Returns + ------- + pruef_ergebnis + Ergebnis mit Aktion: + - ``datei_existiert_ueberschreiben`` + - ``datei_existiert_anhaengen`` + - ``datei_existiert_ueberspringen`` (für Cancel-Fall) + """ if self.ui_modus != "qgis": return ergebnis @@ -72,48 +92,34 @@ class Pruefmanager: "Was soll geschehen?\n\n" "• **Überschreiben**: Bestehende Layer ersetzen\n" "• **Anhängen**: Neue Layer hinzufügen\n" - "• **Überspringen**: Nur temporäre Layer erzeugen" + "• **Abbrechen**: Vorgang beenden" ) - # Vereinfacht: Erst Überschreiben? → Dann Anhängen? → Überspringen - if ask_yes_no( - titel, - f"{meldung}\n\n**Überschreiben** (alle Layer ersetzen)?", - default=False, - parent=self.parent - ): + # Einzelner Dialog mit drei Optionen + entscheidung = ask_overwrite_append_cancel_custom( + parent=self.parent, + title=titel, + message=meldung + ) + + if entscheidung == "overwrite": return pruef_ergebnis( ok=True, aktion="datei_existiert_ueberschreiben", kontext=ergebnis.kontext, ) - - if ask_yes_no( - titel, - f"{meldung}\n\n**Anhängen** (neue Layer hinzufügen)?", - default=False, - parent=self.parent - ): + elif entscheidung == "append": return pruef_ergebnis( ok=True, aktion="datei_existiert_anhaengen", kontext=ergebnis.kontext, ) - - if ask_yes_no( - titel, - f"{meldung}\n\n**Überspringen** (nur temporäre Layer)?", - default=True, - parent=self.parent - ): + else: # cancel return pruef_ergebnis( ok=True, aktion="datei_existiert_ueberspringen", kontext=ergebnis.kontext, ) - - return ergebnis - # ------------------------------------------------------------------ # Basis-Entscheidungen (KORREKT: → pruef_ergebnis) # ------------------------------------------------------------------ diff --git a/modules/stilpruefer.py b/modules/stilpruefer.py index db9312a..aa12879 100644 --- a/modules/stilpruefer.py +++ b/modules/stilpruefer.py @@ -6,7 +6,7 @@ Die Anwendung erfolgt später über eine Aktion. from pathlib import Path -from sn_basis.functions import file_exists +from sn_basis.functions.sys_wrapper import file_exists from sn_basis.modules.pruef_ergebnis import pruef_ergebnis