Files
Plugin_SN_Basis/functions/qgisqt_wrapper.py

881 lines
24 KiB
Python
Raw Normal View History

"""
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