""" sn_basis/functions/qgisqt_wrapper.py – zentrale QGIS/Qt-Abstraktion """ from typing import Optional, Type, Any # --------------------------------------------------------- # Hilfsfunktionen # --------------------------------------------------------- def getattr_safe(obj: Any, name: str, default: Any = None) -> Any: """ Sichere getattr-Variante: - fängt Exceptions beim Attributzugriff ab - liefert default zurück, wenn Attribut fehlt oder fehlschlägt """ try: return getattr(obj, name) except Exception: return default # --------------------------------------------------------- # Qt‑Symbole (werden später dynamisch importiert) # --------------------------------------------------------- QMessageBox: Optional[Type[Any]] = None QFileDialog: Optional[Type[Any]] = None QEventLoop: Optional[Type[Any]] = None QUrl: Optional[Type[Any]] = None QNetworkRequest: Optional[Type[Any]] = None QNetworkReply: Optional[Type[Any]] = None QCoreApplication: Optional[Type[Any]] = None QWidget: Type[Any] QGridLayout: Type[Any] QLabel: Type[Any] QLineEdit: Type[Any] QGroupBox: Type[Any] QVBoxLayout: Type[Any] QPushButton: Type[Any] YES: Optional[Any] = None NO: Optional[Any] = None CANCEL: Optional[Any] = None ICON_QUESTION: Optional[Any] = None def exec_dialog(dialog: Any) -> Any: raise NotImplementedError # --------------------------------------------------------- # QGIS‑Symbole (werden später dynamisch importiert) # --------------------------------------------------------- QgsProject: Optional[Type[Any]] = None QgsVectorLayer: Optional[Type[Any]] = None QgsNetworkAccessManager: Optional[Type[Any]] = None Qgis: Optional[Type[Any]] = None iface: Optional[Any] = None # --------------------------------------------------------- # Qt‑Versionserkennung # --------------------------------------------------------- QT_VERSION = 0 # 0 = Mock, 5 = PyQt5, 6 = PyQt6 # --------------------------------------------------------- # Versuch: PyQt6 importieren # --------------------------------------------------------- try: from PyQt6.QtWidgets import ( #type: ignore QMessageBox as _QMessageBox, QFileDialog as _QFileDialog, QWidget as _QWidget, QGridLayout as _QGridLayout, QLabel as _QLabel, QLineEdit as _QLineEdit, QGroupBox as _QGroupBox, QVBoxLayout as _QVBoxLayout, QPushButton as _QPushButton, ) from PyQt6.QtCore import ( #type: ignore Qt, QEventLoop as _QEventLoop, QUrl as _QUrl, QCoreApplication as _QCoreApplication, ) from PyQt6.QtNetwork import ( #type: ignore QNetworkRequest as _QNetworkRequest, QNetworkReply as _QNetworkReply, ) QMessageBox = _QMessageBox QFileDialog = _QFileDialog QEventLoop = _QEventLoop QUrl = _QUrl QNetworkRequest = _QNetworkRequest QNetworkReply = _QNetworkReply QCoreApplication = _QCoreApplication QWidget = _QWidget QGridLayout = _QGridLayout QLabel = _QLabel QLineEdit = _QLineEdit QGroupBox = _QGroupBox QVBoxLayout = _QVBoxLayout QPushButton = _QPushButton if QMessageBox is not None: YES = QMessageBox.StandardButton.Yes NO = QMessageBox.StandardButton.No CANCEL = QMessageBox.StandardButton.Cancel ICON_QUESTION = QMessageBox.Icon.Question QT_VERSION = 6 def exec_dialog(dialog: Any) -> Any: return dialog.exec() # --------------------------------------------------------- # Versuch: PyQt5 importieren # --------------------------------------------------------- except Exception: try: from PyQt5.QtWidgets import ( QMessageBox as _QMessageBox, QFileDialog as _QFileDialog, QWidget as _QWidget, QGridLayout as _QGridLayout, QLabel as _QLabel, QLineEdit as _QLineEdit, QGroupBox as _QGroupBox, QVBoxLayout as _QVBoxLayout, QPushButton as _QPushButton, ) from PyQt5.QtCore import ( Qt, QEventLoop as _QEventLoop, QUrl as _QUrl, QCoreApplication as _QCoreApplication, ) from PyQt5.QtNetwork import ( QNetworkRequest as _QNetworkRequest, QNetworkReply as _QNetworkReply, ) QMessageBox = _QMessageBox QFileDialog = _QFileDialog QEventLoop = _QEventLoop QUrl = _QUrl QNetworkRequest = _QNetworkRequest QNetworkReply = _QNetworkReply QCoreApplication = _QCoreApplication QWidget = _QWidget QGridLayout = _QGridLayout QLabel = _QLabel QLineEdit = _QLineEdit QGroupBox = _QGroupBox QVBoxLayout = _QVBoxLayout QPushButton = _QPushButton if QMessageBox is not None: YES = QMessageBox.Yes NO = QMessageBox.No CANCEL = QMessageBox.Cancel ICON_QUESTION = QMessageBox.Question QT_VERSION = 5 def exec_dialog(dialog: Any) -> Any: return dialog.exec_() # --------------------------------------------------------- # Mock‑Modus (kein Qt verfügbar) # --------------------------------------------------------- except Exception: QT_VERSION = 0 class FakeEnum(int): """OR‑fähiger Enum‑Ersatz für Mock‑Modus.""" def __new__(cls, value: int): return int.__new__(cls, value) def __or__(self, other: "FakeEnum") -> "FakeEnum": return FakeEnum(int(self) | int(other)) class _MockQMessageBox: Yes = FakeEnum(1) No = FakeEnum(2) Cancel = FakeEnum(4) Question = FakeEnum(8) QMessageBox = _MockQMessageBox class _MockQFileDialog: @staticmethod def getOpenFileName(*args, **kwargs): return ("", "") @staticmethod def getSaveFileName(*args, **kwargs): return ("", "") QFileDialog = _MockQFileDialog class _MockQEventLoop: def exec(self) -> int: return 0 def quit(self) -> None: pass QEventLoop = _MockQEventLoop class _MockQUrl(str): def isValid(self) -> bool: return True QUrl = _MockQUrl class _MockQNetworkRequest: def __init__(self, url: Any): self.url = url QNetworkRequest = _MockQNetworkRequest class _MockQNetworkReply: class NetworkError: NoError = 0 def __init__(self): self._data = b"" def error(self) -> int: return 0 def errorString(self) -> str: return "" def attribute(self, *args, **kwargs) -> Any: return 200 def readAll(self) -> bytes: return self._data def deleteLater(self) -> None: pass QNetworkReply = _MockQNetworkReply YES = FakeEnum(1) NO = FakeEnum(2) CANCEL = FakeEnum(4) ICON_QUESTION = FakeEnum(8) def exec_dialog(dialog: Any) -> Any: return YES class _MockWidget: def __init__(self, *args, **kwargs): pass class _MockLayout: def __init__(self, *args, **kwargs): pass def addWidget(self, *args, **kwargs): pass def addLayout(self, *args, **kwargs): pass def addStretch(self, *args, **kwargs): pass def setLayout(self, *args, **kwargs): pass class _MockLabel: def __init__(self, text: str = ""): self._text = text class _MockLineEdit: def __init__(self, *args, **kwargs): self._text = "" def text(self) -> str: return self._text def setText(self, value: str) -> None: self._text = value class _MockButton: def __init__(self, *args, **kwargs): # einfache Attr für Kompatibilität mit Qt-Signal-Syntax self.clicked = lambda *a, **k: None def connect(self, *args, **kwargs): pass QWidget = _MockWidget QGridLayout = _MockLayout QLabel = _MockLabel QLineEdit = _MockLineEdit QGroupBox = _MockWidget QVBoxLayout = _MockLayout QPushButton = _MockButton # Kein echtes QCoreApplication im Mock QCoreApplication = None # --------------------------------------------------------- # QGIS‑Imports # --------------------------------------------------------- try: from qgis.core import ( QgsProject as _QgsProject, QgsVectorLayer as _QgsVectorLayer, QgsNetworkAccessManager as _QgsNetworkAccessManager, Qgis as _Qgis, ) from qgis.utils import iface as _iface QgsProject = _QgsProject QgsVectorLayer = _QgsVectorLayer QgsNetworkAccessManager = _QgsNetworkAccessManager Qgis = _Qgis iface = _iface QGIS_AVAILABLE = True except Exception: QGIS_AVAILABLE = False class _MockQgsProject: @staticmethod def instance() -> "_MockQgsProject": return _MockQgsProject() def __init__(self): self._variables = {} def read(self) -> bool: return True QgsProject = _MockQgsProject class _MockQgsVectorLayer: def __init__(self, *args, **kwargs): self._valid = True def isValid(self) -> bool: return self._valid def loadNamedStyle(self, path: str): return True, "" def triggerRepaint(self) -> None: pass QgsVectorLayer = _MockQgsVectorLayer class _MockQgsNetworkAccessManager: def head(self, request: Any) -> _MockQNetworkReply: return _MockQNetworkReply() QgsNetworkAccessManager = _MockQgsNetworkAccessManager class _MockQgis: class MessageLevel: Success = 0 Info = 1 Warning = 2 Critical = 3 Qgis = _MockQgis class FakeIface: class FakeMessageBar: def pushMessage(self, title, text, level=0, duration=5): return {"title": title, "text": text, "level": level, "duration": duration} def messageBar(self): return self.FakeMessageBar() def mainWindow(self): return None iface = FakeIface() # --------------------------------------------------------- # Message‑Funktionen # --------------------------------------------------------- def _get_message_bar(): if iface is not None: bar_attr = getattr_safe(iface, "messageBar") if callable(bar_attr): try: return bar_attr() except Exception: pass class _MockMessageBar: def pushMessage(self, title, text, level=0, duration=5): return { "title": title, "text": text, "level": level, "duration": duration, } return _MockMessageBar() def push_message(level, title, text, duration=5, parent=None): bar = _get_message_bar() push = getattr_safe(bar, "pushMessage") if callable(push): return push(title, text, level=level, duration=duration) return None def info(title, text, duration=5): level = Qgis.MessageLevel.Info if Qgis is not None else 1 return push_message(level, title, text, duration) def warning(title, text, duration=5): level = Qgis.MessageLevel.Warning if Qgis is not None else 2 return push_message(level, title, text, duration) def error(title, text, duration=5): level = Qgis.MessageLevel.Critical if Qgis is not None else 3 return push_message(level, title, text, duration) def success(title, text, duration=5): level = Qgis.MessageLevel.Success if Qgis is not None else 0 return push_message(level, title, text, duration) # --------------------------------------------------------- # Dialog‑Interaktionen # --------------------------------------------------------- def ask_yes_no( title: str, message: str, default: bool = False, parent: Any = None, ) -> bool: """ Fragt den Benutzer eine Ja/Nein‑Frage. - In QGIS/Qt: zeigt einen QMessageBox‑Dialog - Im Mock/Test‑Modus: gibt default zurück """ if QMessageBox is None: return default try: buttons = YES | NO result = QMessageBox.question( parent, title, message, buttons, YES if default else NO, ) return result == YES except Exception: return default # --------------------------------------------------------- # Variablen‑Wrapper # --------------------------------------------------------- try: from qgis.core import QgsExpressionContextUtils _HAS_QGIS_VARIABLES = True except Exception: _HAS_QGIS_VARIABLES = False class _MockVariableStore: global_vars: dict[str, str] = {} project_vars: dict[str, str] = {} class QgsExpressionContextUtils: @staticmethod def setGlobalVariable(name: str, value: str) -> None: _MockVariableStore.global_vars[name] = value @staticmethod def globalScope(): class _Scope: def variable(self, name: str) -> str: return _MockVariableStore.global_vars.get(name, "") return _Scope() @staticmethod def setProjectVariable(project: Any, name: str, value: str) -> None: _MockVariableStore.project_vars[name] = value @staticmethod def projectScope(project: Any): class _Scope: def variable(self, name: str) -> str: return _MockVariableStore.project_vars.get(name, "") return _Scope() def get_variable(key: str, scope: str = "project") -> str: var_name = f"sn_{key}" if scope == "project": if QgsProject is not None: projekt = QgsProject.instance() else: projekt = None # type: ignore[assignment] return QgsExpressionContextUtils.projectScope(projekt).variable(var_name) or "" if scope == "global": return QgsExpressionContextUtils.globalScope().variable(var_name) or "" raise ValueError("Scope muss 'project' oder 'global' sein.") def set_variable(key: str, value: str, scope: str = "project") -> None: var_name = f"sn_{key}" if scope == "project": if QgsProject is not None: projekt = QgsProject.instance() else: projekt = None # type: ignore[assignment] QgsExpressionContextUtils.setProjectVariable(projekt, var_name, value) return if scope == "global": QgsExpressionContextUtils.setGlobalVariable(var_name, value) return raise ValueError("Scope muss 'project' oder 'global' sein.") # --------------------------------------------------------- # syswrapper Lazy‑Import # --------------------------------------------------------- def _sys(): from sn_basis.functions import syswrapper return syswrapper # --------------------------------------------------------- # Style‑Funktion # --------------------------------------------------------- def apply_style(layer, style_name: str) -> bool: if layer is None: return False is_valid_attr = getattr_safe(layer, "isValid") if not callable(is_valid_attr) or not is_valid_attr(): return False sys = _sys() base_dir = sys.get_plugin_root() style_path = sys.join_path(base_dir, "styles", style_name) if not sys.file_exists(style_path): return False try: ok, error_msg = layer.loadNamedStyle(style_path) except Exception: return False if not ok: return False try: trigger = getattr_safe(layer, "triggerRepaint") if callable(trigger): trigger() except Exception: pass return True # --------------------------------------------------------- # Layer‑Wrapper # --------------------------------------------------------- def layer_exists(layer) -> bool: if layer is None: return False # Mock/Wrapper-Attribut is_valid_flag = getattr_safe(layer, "is_valid") if is_valid_flag is not None: try: return bool(is_valid_flag) except Exception: return False try: is_valid_attr = getattr_safe(layer, "isValid") if callable(is_valid_attr): return bool(is_valid_attr()) return True except Exception: return False def get_layer_geometry_type(layer) -> str: if layer is None: return "None" geometry_type_attr = getattr_safe(layer, "geometry_type") if geometry_type_attr is not None: return str(geometry_type_attr) try: is_spatial_attr = getattr_safe(layer, "isSpatial") if callable(is_spatial_attr) and not is_spatial_attr(): return "None" geometry_type_qgis = getattr_safe(layer, "geometryType") if callable(geometry_type_qgis): gtype = geometry_type_qgis() if gtype == 0: return "Point" if gtype == 1: return "LineString" if gtype == 2: return "Polygon" return "None" return "None" except Exception: return "None" def get_layer_feature_count(layer) -> int: if layer is None: return 0 feature_count_attr = getattr_safe(layer, "feature_count") if feature_count_attr is not None: try: return int(feature_count_attr) except Exception: return 0 try: is_spatial_attr = getattr_safe(layer, "isSpatial") if callable(is_spatial_attr) and not is_spatial_attr(): return 0 feature_count_qgis = getattr_safe(layer, "featureCount") if callable(feature_count_qgis): return int(feature_count_qgis()) return 0 except Exception: return 0 def is_layer_visible(layer) -> bool: if layer is None: return False visible_attr = getattr_safe(layer, "visible") if visible_attr is not None: try: return bool(visible_attr) except Exception: return False try: is_visible_attr = getattr_safe(layer, "isVisible") if callable(is_visible_attr): return bool(is_visible_attr()) tree_layer_attr = getattr_safe(layer, "treeLayer") if callable(tree_layer_attr): node = tree_layer_attr() else: node = tree_layer_attr if node is not None: node_visible_attr = getattr_safe(node, "isVisible") if callable(node_visible_attr): return bool(node_visible_attr()) return False except Exception: return False def set_layer_visible(layer, visible: bool) -> bool: """ Setzt die Sichtbarkeit eines Layers. Unterstützt: - Mock-/Wrapper-Attribute (layer.visible) - QGIS-LayerTreeNode (treeLayer().setItemVisibilityChecked) - Fallbacks ohne Exception-Wurf Gibt True zurück, wenn die Sichtbarkeit gesetzt werden konnte. """ if layer is None: return False # 1️⃣ Mock / Wrapper-Attribut try: if hasattr(layer, "visible"): layer.visible = bool(visible) return True except Exception: pass # 2️⃣ QGIS: LayerTreeNode try: tree_layer_attr = getattr_safe(layer, "treeLayer") node = tree_layer_attr() if callable(tree_layer_attr) else tree_layer_attr if node is not None: set_visible = getattr_safe(node, "setItemVisibilityChecked") if callable(set_visible): set_visible(bool(visible)) return True except Exception: pass # 3️⃣ QGIS-Fallback: setVisible (selten, aber vorhanden) try: set_visible_attr = getattr_safe(layer, "setVisible") if callable(set_visible_attr): set_visible_attr(bool(visible)) return True except Exception: pass return False def get_layer_type(layer) -> str: if layer is None: return "unknown" layer_type_attr = getattr_safe(layer, "layer_type") if layer_type_attr is not None: return str(layer_type_attr) try: is_spatial_attr = getattr_safe(layer, "isSpatial") if callable(is_spatial_attr): return "vector" if is_spatial_attr() else "table" data_provider_attr = getattr_safe(layer, "dataProvider") raster_type_attr = getattr_safe(layer, "rasterType") if data_provider_attr is not None and raster_type_attr is not None: return "raster" return "unknown" except Exception: return "unknown" def get_layer_crs(layer) -> str: if layer is None: return "None" crs_attr_direct = getattr_safe(layer, "crs") if crs_attr_direct is not None and not callable(crs_attr_direct): # direkter Attributzugriff (z. B. im Mock) return str(crs_attr_direct) try: crs_callable = getattr_safe(layer, "crs") if callable(crs_callable): crs = crs_callable() authid_attr = getattr_safe(crs, "authid") if callable(authid_attr): return authid_attr() or "None" return "None" except Exception: return "None" def get_layer_fields(layer) -> list[str]: if layer is None: return [] # direkter Attributzugriff (Mock / Wrapper) fields_attr_direct = getattr_safe(layer, "fields") if fields_attr_direct is not None and not callable(fields_attr_direct): try: # direkter Iterable oder Mapping von Namen if hasattr(fields_attr_direct, "__iter__") and not isinstance( fields_attr_direct, (str, bytes) ): return list(fields_attr_direct) except Exception: return [] try: fields_callable = getattr_safe(layer, "fields") if callable(fields_callable): fields = fields_callable() # QGIS: QgsFields.names() names_attr = getattr_safe(fields, "names") if callable(names_attr): return list(names_attr()) # Fallback: iterierbar? if hasattr(fields, "__iter__") and not isinstance(fields, (str, bytes)): return list(fields) return [] except Exception: return [] def get_layer_source(layer) -> str: if layer is None: return "None" source_attr_direct = getattr_safe(layer, "source") if source_attr_direct is not None and not callable(source_attr_direct): return str(source_attr_direct) try: source_callable = getattr_safe(layer, "source") if callable(source_callable): return source_callable() or "None" return "None" except Exception: return "None" def is_layer_editable(layer) -> bool: if layer is None: return False editable_attr = getattr_safe(layer, "editable") if editable_attr is not None: try: return bool(editable_attr) except Exception: return False try: editable_callable = getattr_safe(layer, "isEditable") if callable(editable_callable): return bool(editable_callable()) return False except Exception: return False