Files
Plugin_SN_Basis/functions/qgisqt_wrapper.py

881 lines
24 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
"""
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
# ---------------------------------------------------------
# QtSymbole (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
# ---------------------------------------------------------
# QGISSymbole (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
# ---------------------------------------------------------
# QtVersionserkennung
# ---------------------------------------------------------
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_()
# ---------------------------------------------------------
# MockModus (kein Qt verfügbar)
# ---------------------------------------------------------
except Exception:
QT_VERSION = 0
class FakeEnum(int):
"""ORfähiger EnumErsatz für MockModus."""
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
# ---------------------------------------------------------
# QGISImports
# ---------------------------------------------------------
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()
# ---------------------------------------------------------
# MessageFunktionen
# ---------------------------------------------------------
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)
# ---------------------------------------------------------
# DialogInteraktionen
# ---------------------------------------------------------
def ask_yes_no(
title: str,
message: str,
default: bool = False,
parent: Any = None,
) -> bool:
"""
Fragt den Benutzer eine Ja/NeinFrage.
- In QGIS/Qt: zeigt einen QMessageBoxDialog
- Im Mock/TestModus: 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
# ---------------------------------------------------------
# VariablenWrapper
# ---------------------------------------------------------
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 LazyImport
# ---------------------------------------------------------
def _sys():
from sn_basis.functions import syswrapper
return syswrapper
# ---------------------------------------------------------
# StyleFunktion
# ---------------------------------------------------------
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
# ---------------------------------------------------------
# LayerWrapper
# ---------------------------------------------------------
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