forked from AG_QGIS/Plugin_SN_Basis
Wrappe modular aufgebaut, Tests erfolgreich, Menüleiste und Werzeugleiste werden eingetragen (QT6 und QT5)- (Es fehlen noch Fachplugins, um zu prüfen, ob es auch wirklich in QGIS geht)
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
from .ly_existence_wrapper import layer_exists
|
||||
from .ly_geometry_wrapper import (
|
||||
get_layer_geometry_type,
|
||||
get_layer_feature_count,
|
||||
)
|
||||
from .ly_visibility_wrapper import (
|
||||
is_layer_visible,
|
||||
set_layer_visible,
|
||||
)
|
||||
from .ly_metadata_wrapper import (
|
||||
get_layer_type,
|
||||
get_layer_crs,
|
||||
get_layer_fields,
|
||||
get_layer_source,
|
||||
is_layer_editable,
|
||||
)
|
||||
from .ly_style_wrapper import apply_style
|
||||
from .dialog_wrapper import ask_yes_no
|
||||
|
||||
from .message_wrapper import (
|
||||
_get_message_bar,
|
||||
push_message,
|
||||
error,
|
||||
warning,
|
||||
info,
|
||||
success,
|
||||
)
|
||||
|
||||
from .os_wrapper import *
|
||||
from .qgiscore_wrapper import *
|
||||
from .qt_wrapper import *
|
||||
from .settings_logic import *
|
||||
from .sys_wrapper import *
|
||||
from .variable_wrapper import *
|
||||
from .qgisui_wrapper import (
|
||||
get_main_window,
|
||||
add_dock_widget,
|
||||
remove_dock_widget,
|
||||
find_dock_widgets,
|
||||
add_menu,
|
||||
remove_menu,
|
||||
add_toolbar,
|
||||
remove_toolbar)
|
||||
|
||||
41
functions/dialog_wrapper.py
Normal file
41
functions/dialog_wrapper.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""
|
||||
sn_basis/functions/dialog_wrapper.py – Benutzer-Dialoge
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from sn_basis.functions.qt_wrapper import (
|
||||
QMessageBox,
|
||||
YES,
|
||||
NO,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Öffentliche API
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def ask_yes_no(
|
||||
title: str,
|
||||
message: str,
|
||||
default: bool = False,
|
||||
parent: Any = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Fragt den Benutzer eine Ja/Nein-Frage.
|
||||
|
||||
- In Qt: zeigt einen QMessageBox-Dialog
|
||||
- Im Mock-Modus: gibt den Default-Wert zurück
|
||||
"""
|
||||
try:
|
||||
buttons = QMessageBox.Yes | QMessageBox.No
|
||||
result = QMessageBox.question(
|
||||
parent,
|
||||
title,
|
||||
message,
|
||||
buttons,
|
||||
YES if default else NO,
|
||||
)
|
||||
return result == YES
|
||||
except Exception:
|
||||
return default
|
||||
20
functions/ly_existence_wrapper.py
Normal file
20
functions/ly_existence_wrapper.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# sn_basis/functions/ly_existence_wrapper.py
|
||||
|
||||
def layer_exists(layer) -> bool:
|
||||
if layer is None:
|
||||
return False
|
||||
|
||||
is_valid_flag = getattr(layer, "is_valid", None)
|
||||
if is_valid_flag is not None:
|
||||
try:
|
||||
return bool(is_valid_flag)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
try:
|
||||
is_valid = getattr(layer, "isValid", None)
|
||||
if callable(is_valid):
|
||||
return bool(is_valid())
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
57
functions/ly_geometry_wrapper.py
Normal file
57
functions/ly_geometry_wrapper.py
Normal file
@@ -0,0 +1,57 @@
|
||||
# sn_basis/functions/ly_geometry_wrapper.py
|
||||
|
||||
def get_layer_geometry_type(layer) -> str:
|
||||
if layer is None:
|
||||
return "None"
|
||||
|
||||
geometry_type = getattr(layer, "geometry_type", None)
|
||||
if geometry_type is not None:
|
||||
return str(geometry_type)
|
||||
|
||||
try:
|
||||
if callable(getattr(layer, "isSpatial", None)) and not layer.isSpatial():
|
||||
return "None"
|
||||
|
||||
gtype = getattr(layer, "geometryType", None)
|
||||
if callable(gtype):
|
||||
value = gtype()
|
||||
if not isinstance(value, int):
|
||||
return "None"
|
||||
|
||||
return {
|
||||
0: "Point",
|
||||
1: "LineString",
|
||||
2: "Polygon",
|
||||
}.get(value, "None")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return "None"
|
||||
|
||||
|
||||
|
||||
|
||||
def get_layer_feature_count(layer) -> int:
|
||||
if layer is None:
|
||||
return 0
|
||||
|
||||
count = getattr(layer, "feature_count", None)
|
||||
if count is not None:
|
||||
if isinstance(count, int):
|
||||
return count
|
||||
return 0
|
||||
|
||||
try:
|
||||
if callable(getattr(layer, "isSpatial", None)) and not layer.isSpatial():
|
||||
return 0
|
||||
|
||||
fc = getattr(layer, "featureCount", None)
|
||||
if callable(fc):
|
||||
value = fc()
|
||||
if isinstance(value, int):
|
||||
return value
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return 0
|
||||
|
||||
90
functions/ly_metadata_wrapper.py
Normal file
90
functions/ly_metadata_wrapper.py
Normal file
@@ -0,0 +1,90 @@
|
||||
# layer/metadata.py
|
||||
|
||||
def get_layer_type(layer) -> str:
|
||||
if layer is None:
|
||||
return "unknown"
|
||||
|
||||
layer_type = getattr(layer, "layer_type", None)
|
||||
if layer_type is not None:
|
||||
return str(layer_type)
|
||||
|
||||
try:
|
||||
if callable(getattr(layer, "isSpatial", None)):
|
||||
return "vector" if layer.isSpatial() else "table"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return "unknown"
|
||||
|
||||
|
||||
def get_layer_crs(layer) -> str:
|
||||
if layer is None:
|
||||
return "None"
|
||||
|
||||
crs = getattr(layer, "crs", None)
|
||||
if crs is not None and not callable(crs):
|
||||
if isinstance(crs, str):
|
||||
return crs
|
||||
return "None"
|
||||
|
||||
try:
|
||||
crs_obj = layer.crs()
|
||||
authid = getattr(crs_obj, "authid", None)
|
||||
if callable(authid):
|
||||
value = authid()
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return "None"
|
||||
|
||||
|
||||
|
||||
def get_layer_fields(layer) -> list[str]:
|
||||
if layer is None:
|
||||
return []
|
||||
|
||||
fields = getattr(layer, "fields", None)
|
||||
if fields is not None and not callable(fields):
|
||||
return list(fields)
|
||||
|
||||
try:
|
||||
f = layer.fields()
|
||||
if callable(getattr(f, "names", None)):
|
||||
return list(f.names())
|
||||
return list(f)
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def get_layer_source(layer) -> str:
|
||||
if layer is None:
|
||||
return "None"
|
||||
|
||||
source = getattr(layer, "source", None)
|
||||
if source is not None and not callable(source):
|
||||
return str(source)
|
||||
|
||||
try:
|
||||
return layer.source() or "None"
|
||||
except Exception:
|
||||
return "None"
|
||||
|
||||
|
||||
def is_layer_editable(layer) -> bool:
|
||||
if layer is None:
|
||||
return False
|
||||
|
||||
editable = getattr(layer, "editable", None)
|
||||
if editable is not None:
|
||||
return bool(editable)
|
||||
|
||||
try:
|
||||
is_editable = getattr(layer, "isEditable", None)
|
||||
if callable(is_editable):
|
||||
return bool(is_editable())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return False
|
||||
27
functions/ly_style_wrapper.py
Normal file
27
functions/ly_style_wrapper.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# layer/style.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,
|
||||
)
|
||||
|
||||
|
||||
def apply_style(layer, style_name: str) -> bool:
|
||||
if not layer_exists(layer):
|
||||
return False
|
||||
|
||||
style_path = join_path(get_plugin_root(), "styles", style_name)
|
||||
if not file_exists(style_path):
|
||||
return False
|
||||
|
||||
try:
|
||||
ok, _ = layer.loadNamedStyle(style_path)
|
||||
if ok:
|
||||
getattr(layer, "triggerRepaint", lambda: None)()
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return False
|
||||
40
functions/ly_visibility_wrapper.py
Normal file
40
functions/ly_visibility_wrapper.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# sn_basis/functions/ly_visibility_wrapper.py
|
||||
|
||||
def is_layer_visible(layer) -> bool:
|
||||
if layer is None:
|
||||
return False
|
||||
|
||||
visible = getattr(layer, "visible", None)
|
||||
if visible is not None:
|
||||
return bool(visible)
|
||||
|
||||
try:
|
||||
is_visible = getattr(layer, "isVisible", None)
|
||||
if callable(is_visible):
|
||||
return bool(is_visible())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def set_layer_visible(layer, visible: bool) -> bool:
|
||||
if layer is None:
|
||||
return False
|
||||
|
||||
try:
|
||||
if hasattr(layer, "visible"):
|
||||
layer.visible = bool(visible)
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
node = getattr(layer, "treeLayer", lambda: None)()
|
||||
if node and callable(getattr(node, "setItemVisibilityChecked", None)):
|
||||
node.setItemVisibilityChecked(bool(visible))
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return False
|
||||
84
functions/message_wrapper.py
Normal file
84
functions/message_wrapper.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""
|
||||
sn_basis/functions/message_wrapper.py – zentrale MessageBar-Abstraktion
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from sn_basis.functions.qgisui_wrapper import iface
|
||||
from sn_basis.functions.qgiscore_wrapper import Qgis
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Interne Hilfsfunktion
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def _get_message_bar():
|
||||
"""
|
||||
Liefert eine MessageBar-Instanz (QGIS oder Mock).
|
||||
"""
|
||||
try:
|
||||
bar = iface.messageBar()
|
||||
if bar is not None:
|
||||
return bar
|
||||
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()
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Öffentliche API
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def push_message(
|
||||
level: int,
|
||||
title: str,
|
||||
text: str,
|
||||
duration: int = 5,
|
||||
parent: Any = None,
|
||||
):
|
||||
"""
|
||||
Zeigt eine Message in der QGIS-MessageBar an.
|
||||
|
||||
Im Mock-Modus wird ein strukturierter Dict zurückgegeben.
|
||||
"""
|
||||
bar = _get_message_bar()
|
||||
|
||||
try:
|
||||
return bar.pushMessage(
|
||||
title,
|
||||
text,
|
||||
level=level,
|
||||
duration=duration,
|
||||
)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def info(title: str, text: str, duration: int = 5):
|
||||
level = Qgis.MessageLevel.Info
|
||||
return push_message(level, title, text, duration)
|
||||
|
||||
|
||||
def warning(title: str, text: str, duration: int = 5):
|
||||
level = Qgis.MessageLevel.Warning
|
||||
return push_message(level, title, text, duration)
|
||||
|
||||
|
||||
def error(title: str, text: str, duration: int = 5):
|
||||
level = Qgis.MessageLevel.Critical
|
||||
return push_message(level, title, text, duration)
|
||||
|
||||
|
||||
def success(title: str, text: str, duration: int = 5):
|
||||
level = Qgis.MessageLevel.Success
|
||||
return push_message(level, title, text, duration)
|
||||
77
functions/os_wrapper.py
Normal file
77
functions/os_wrapper.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""
|
||||
sn_basis/functions/os_wrapper.py – Betriebssystem-Abstraktion
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import platform
|
||||
from typing import Union
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# OS-Erkennung
|
||||
# ---------------------------------------------------------
|
||||
|
||||
_SYSTEM = platform.system().lower()
|
||||
|
||||
if _SYSTEM.startswith("win"):
|
||||
OS_NAME = "windows"
|
||||
elif _SYSTEM.startswith("darwin"):
|
||||
OS_NAME = "macos"
|
||||
else:
|
||||
OS_NAME = "linux"
|
||||
|
||||
IS_WINDOWS = OS_NAME == "windows"
|
||||
IS_LINUX = OS_NAME == "linux"
|
||||
IS_MACOS = OS_NAME == "macos"
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# OS-Eigenschaften
|
||||
# ---------------------------------------------------------
|
||||
|
||||
PATH_SEPARATOR = "\\" if IS_WINDOWS else "/"
|
||||
LINE_SEPARATOR = "\r\n" if IS_WINDOWS else "\n"
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Pfad-Utilities
|
||||
# ---------------------------------------------------------
|
||||
|
||||
_PathLike = Union[str, Path]
|
||||
|
||||
|
||||
def normalize_path(path: _PathLike) -> Path:
|
||||
"""
|
||||
Normalisiert einen Pfad OS-unabhängig.
|
||||
"""
|
||||
try:
|
||||
return Path(path).expanduser().resolve()
|
||||
except Exception:
|
||||
return Path(path)
|
||||
|
||||
|
||||
def get_home_dir() -> Path:
|
||||
"""
|
||||
Liefert das Home-Verzeichnis des aktuellen Users.
|
||||
"""
|
||||
return Path.home()
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Dateisystem-Eigenschaften
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def is_case_sensitive_fs() -> bool:
|
||||
"""
|
||||
Gibt zurück, ob das Dateisystem case-sensitiv ist.
|
||||
"""
|
||||
# Windows ist immer case-insensitive
|
||||
if IS_WINDOWS:
|
||||
return False
|
||||
|
||||
# macOS meist case-insensitive, aber nicht garantiert
|
||||
if IS_MACOS:
|
||||
return False
|
||||
|
||||
# Linux praktisch immer case-sensitiv
|
||||
return True
|
||||
139
functions/qgiscore_wrapper.py
Normal file
139
functions/qgiscore_wrapper.py
Normal file
@@ -0,0 +1,139 @@
|
||||
"""
|
||||
sn_basis/functions/qgiscore_wrapper.py – zentrale QGIS-Core-Abstraktion
|
||||
"""
|
||||
|
||||
from typing import Type, Any
|
||||
|
||||
from sn_basis.functions.qt_wrapper import (
|
||||
QUrl,
|
||||
QEventLoop,
|
||||
QNetworkRequest,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# QGIS-Symbole (werden dynamisch gesetzt)
|
||||
# ---------------------------------------------------------
|
||||
|
||||
QgsProject: Type[Any]
|
||||
QgsVectorLayer: Type[Any]
|
||||
QgsNetworkAccessManager: Type[Any]
|
||||
Qgis: Type[Any]
|
||||
|
||||
QGIS_AVAILABLE = False
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Versuch: QGIS-Core importieren
|
||||
# ---------------------------------------------------------
|
||||
|
||||
try:
|
||||
from qgis.core import (
|
||||
QgsProject as _QgsProject,
|
||||
QgsVectorLayer as _QgsVectorLayer,
|
||||
QgsNetworkAccessManager as _QgsNetworkAccessManager,
|
||||
Qgis as _Qgis,
|
||||
)
|
||||
|
||||
QgsProject = _QgsProject
|
||||
QgsVectorLayer = _QgsVectorLayer
|
||||
QgsNetworkAccessManager = _QgsNetworkAccessManager
|
||||
Qgis = _Qgis
|
||||
|
||||
QGIS_AVAILABLE = True
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Mock-Modus
|
||||
# ---------------------------------------------------------
|
||||
|
||||
except Exception:
|
||||
QGIS_AVAILABLE = False
|
||||
|
||||
class _MockQgsProject:
|
||||
def __init__(self):
|
||||
self._variables = {}
|
||||
|
||||
@staticmethod
|
||||
def instance() -> "_MockQgsProject":
|
||||
return _MockQgsProject()
|
||||
|
||||
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:
|
||||
@staticmethod
|
||||
def instance():
|
||||
return _MockQgsNetworkAccessManager()
|
||||
|
||||
def head(self, request: Any):
|
||||
return None
|
||||
|
||||
QgsNetworkAccessManager = _MockQgsNetworkAccessManager
|
||||
|
||||
class _MockQgis:
|
||||
class MessageLevel:
|
||||
Success = 0
|
||||
Info = 1
|
||||
Warning = 2
|
||||
Critical = 3
|
||||
|
||||
Qgis = _MockQgis
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Netzwerk
|
||||
# ---------------------------------------------------------
|
||||
|
||||
class NetworkReply:
|
||||
"""
|
||||
Minimaler Wrapper für Netzwerkantworten.
|
||||
"""
|
||||
def __init__(self, error: int):
|
||||
self.error = error
|
||||
|
||||
|
||||
def network_head(url: str) -> NetworkReply | None:
|
||||
"""
|
||||
Führt einen HTTP-HEAD-Request aus.
|
||||
|
||||
Rückgabe:
|
||||
- NetworkReply(error=0) → erreichbar
|
||||
- NetworkReply(error!=0) → nicht erreichbar
|
||||
- None → Netzwerk nicht verfügbar / Fehler beim Request
|
||||
"""
|
||||
|
||||
if not QGIS_AVAILABLE:
|
||||
return None
|
||||
|
||||
if QUrl is None or QNetworkRequest is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
manager = QgsNetworkAccessManager.instance()
|
||||
request = QNetworkRequest(QUrl(url))
|
||||
reply = manager.head(request)
|
||||
|
||||
# synchron warten (kurz)
|
||||
if QEventLoop is not None:
|
||||
loop = QEventLoop()
|
||||
reply.finished.connect(loop.quit)
|
||||
loop.exec()
|
||||
|
||||
return NetworkReply(error=reply.error())
|
||||
except Exception:
|
||||
return None
|
||||
@@ -1,880 +0,0 @@
|
||||
"""
|
||||
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
|
||||
140
functions/qgisui_wrapper.py
Normal file
140
functions/qgisui_wrapper.py
Normal file
@@ -0,0 +1,140 @@
|
||||
"""
|
||||
sn_basis/functions/qgisui_wrapper.py – zentrale QGIS-UI-Abstraktion
|
||||
"""
|
||||
|
||||
from typing import Any, List
|
||||
|
||||
from sn_basis.functions.qt_wrapper import QDockWidget
|
||||
|
||||
|
||||
iface: Any
|
||||
QGIS_UI_AVAILABLE = False
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# iface initialisieren (QGIS oder Mock)
|
||||
# ---------------------------------------------------------
|
||||
|
||||
try:
|
||||
from qgis.utils import iface as _iface
|
||||
iface = _iface
|
||||
QGIS_UI_AVAILABLE = True
|
||||
|
||||
except Exception:
|
||||
|
||||
|
||||
class _MockMessageBar:
|
||||
def pushMessage(self, title, text, level=0, duration=5):
|
||||
return {
|
||||
"title": title,
|
||||
"text": text,
|
||||
"level": level,
|
||||
"duration": duration,
|
||||
}
|
||||
|
||||
class _MockIface:
|
||||
def messageBar(self):
|
||||
return _MockMessageBar()
|
||||
|
||||
def mainWindow(self):
|
||||
return None
|
||||
|
||||
def addDockWidget(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def removeDockWidget(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def addToolBar(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def removeToolBar(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
iface = _MockIface()
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Main Window
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def get_main_window():
|
||||
try:
|
||||
return iface.mainWindow()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Dock-Handling
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def add_dock_widget(area, dock: Any) -> None:
|
||||
try:
|
||||
iface.addDockWidget(area, dock)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def remove_dock_widget(dock: Any) -> None:
|
||||
try:
|
||||
iface.removeDockWidget(dock)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def find_dock_widgets() -> List[Any]:
|
||||
main_window = get_main_window()
|
||||
if not main_window:
|
||||
return []
|
||||
|
||||
try:
|
||||
return main_window.findChildren(QDockWidget)
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Menü-Handling
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def add_menu(menu):
|
||||
main_window = iface.mainWindow()
|
||||
if not main_window:
|
||||
return
|
||||
|
||||
# Nur echte Qt-Menüs an Qt übergeben
|
||||
if hasattr(menu, "menuAction"):
|
||||
main_window.menuBar().addMenu(menu)
|
||||
|
||||
|
||||
|
||||
|
||||
def remove_menu(menu):
|
||||
main_window = iface.mainWindow()
|
||||
if not main_window:
|
||||
return
|
||||
|
||||
if hasattr(menu, "menuAction"):
|
||||
main_window.menuBar().removeAction(menu.menuAction())
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Toolbar-Handling
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def add_toolbar(toolbar: Any) -> None:
|
||||
try:
|
||||
iface.addToolBar(toolbar)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def remove_toolbar(toolbar: Any) -> None:
|
||||
try:
|
||||
iface.removeToolBar(toolbar)
|
||||
except Exception:
|
||||
pass
|
||||
393
functions/qt_wrapper.py
Normal file
393
functions/qt_wrapper.py
Normal file
@@ -0,0 +1,393 @@
|
||||
"""
|
||||
sn_basis/functions/qt_wrapper.py – zentrale Qt-Abstraktion (PyQt5 / PyQt6 / Mock)
|
||||
"""
|
||||
|
||||
from typing import Optional, Type, Any
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Qt-Symbole (werden dynamisch gesetzt)
|
||||
# ---------------------------------------------------------
|
||||
|
||||
QDockWidget: Type[Any]
|
||||
QMessageBox: Type[Any]
|
||||
QFileDialog: Type[Any]
|
||||
QEventLoop: Type[Any]
|
||||
QUrl: Type[Any]
|
||||
QNetworkRequest: Type[Any]
|
||||
QNetworkReply: Type[Any]
|
||||
QCoreApplication: Type[Any]
|
||||
|
||||
QWidget: Type[Any]
|
||||
QGridLayout: Type[Any]
|
||||
QLabel: Type[Any]
|
||||
QLineEdit: Type[Any]
|
||||
QGroupBox: Type[Any]
|
||||
QVBoxLayout: Type[Any]
|
||||
QPushButton: Type[Any]
|
||||
QAction: Type[Any]
|
||||
QMenu: Type[Any]
|
||||
QToolBar: Type[Any]
|
||||
QActionGroup: Type[Any]
|
||||
QTabWidget: type
|
||||
|
||||
|
||||
|
||||
YES: Optional[Any] = None
|
||||
NO: Optional[Any] = None
|
||||
CANCEL: Optional[Any] = None
|
||||
ICON_QUESTION: Optional[Any] = None
|
||||
|
||||
QT_VERSION = 0 # 0 = Mock, 5 = PyQt5, 6 = PyQt6
|
||||
|
||||
|
||||
def exec_dialog(dialog: Any) -> Any:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Versuch: PyQt6
|
||||
# ---------------------------------------------------------
|
||||
|
||||
try:
|
||||
from qgis.PyQt.QtWidgets import ( # type: ignore
|
||||
QMessageBox as _QMessageBox,# type: ignore
|
||||
QFileDialog as _QFileDialog,# type: ignore
|
||||
QWidget as _QWidget,# type: ignore
|
||||
QGridLayout as _QGridLayout,# type: ignore
|
||||
QLabel as _QLabel,# type: ignore
|
||||
QLineEdit as _QLineEdit,# type: ignore
|
||||
QGroupBox as _QGroupBox,# type: ignore
|
||||
QVBoxLayout as _QVBoxLayout,# type: ignore
|
||||
QPushButton as _QPushButton,# type: ignore
|
||||
QAction as _QAction,
|
||||
QMenu as _QMenu,# type: ignore
|
||||
QToolBar as _QToolBar,# type: ignore
|
||||
QActionGroup as _QActionGroup,# type: ignore
|
||||
QDockWidget as _QDockWidget,# type: ignore
|
||||
QTabWidget as _QTabWidget,# type: ignore
|
||||
)
|
||||
|
||||
|
||||
|
||||
from qgis.PyQt.QtCore import ( # type: ignore
|
||||
QEventLoop as _QEventLoop,# type: ignore
|
||||
QUrl as _QUrl,# type: ignore
|
||||
QCoreApplication as _QCoreApplication,# type: ignore
|
||||
)
|
||||
from qgis.PyQt.QtNetwork import ( # type: ignore
|
||||
QNetworkRequest as _QNetworkRequest,# type: ignore
|
||||
QNetworkReply as _QNetworkReply,# type: ignore
|
||||
)
|
||||
QT_VERSION = 6
|
||||
QMessageBox = _QMessageBox
|
||||
QFileDialog = _QFileDialog
|
||||
QEventLoop = _QEventLoop
|
||||
QUrl = _QUrl
|
||||
QNetworkRequest = _QNetworkRequest
|
||||
QNetworkReply = _QNetworkReply
|
||||
QCoreApplication = _QCoreApplication
|
||||
QDockWidget = _QDockWidget
|
||||
QWidget = _QWidget
|
||||
QGridLayout = _QGridLayout
|
||||
QLabel = _QLabel
|
||||
QLineEdit = _QLineEdit
|
||||
QGroupBox = _QGroupBox
|
||||
QVBoxLayout = _QVBoxLayout
|
||||
QPushButton = _QPushButton
|
||||
QAction = _QAction
|
||||
QMenu = _QMenu
|
||||
QToolBar = _QToolBar
|
||||
QActionGroup = _QActionGroup
|
||||
QTabWidget = _QTabWidget
|
||||
|
||||
|
||||
|
||||
YES = QMessageBox.StandardButton.Yes
|
||||
NO = QMessageBox.StandardButton.No
|
||||
CANCEL = QMessageBox.StandardButton.Cancel
|
||||
ICON_QUESTION = QMessageBox.Icon.Question
|
||||
|
||||
|
||||
|
||||
def exec_dialog(dialog: Any) -> Any:
|
||||
return dialog.exec()
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Versuch: PyQt5
|
||||
# ---------------------------------------------------------
|
||||
|
||||
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,
|
||||
QAction as _QAction,
|
||||
QMenu as _QMenu,
|
||||
QToolBar as _QToolBar,
|
||||
QActionGroup as _QActionGroup,
|
||||
QDockWidget as _QDockWidget,
|
||||
QTabWidget as _QTabWidget,
|
||||
|
||||
)
|
||||
from PyQt5.QtCore import (
|
||||
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
|
||||
QDockWidget = _QDockWidget
|
||||
|
||||
|
||||
QWidget = _QWidget
|
||||
QGridLayout = _QGridLayout
|
||||
QLabel = _QLabel
|
||||
QLineEdit = _QLineEdit
|
||||
QGroupBox = _QGroupBox
|
||||
QVBoxLayout = _QVBoxLayout
|
||||
QPushButton = _QPushButton
|
||||
QAction = _QAction
|
||||
QMenu = _QMenu
|
||||
QToolBar = _QToolBar
|
||||
QActionGroup = _QActionGroup
|
||||
QTabWidget = _QTabWidget
|
||||
|
||||
|
||||
|
||||
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
|
||||
# ---------------------------------------------------------
|
||||
|
||||
except Exception:
|
||||
QT_VERSION = 0
|
||||
|
||||
class FakeEnum(int):
|
||||
def __or__(self, other: "FakeEnum") -> "FakeEnum":
|
||||
return FakeEnum(int(self) | int(other))
|
||||
|
||||
YES = FakeEnum(1)
|
||||
NO = FakeEnum(2)
|
||||
CANCEL = FakeEnum(4)
|
||||
ICON_QUESTION = FakeEnum(8)
|
||||
|
||||
class _MockQMessageBox:
|
||||
Yes = YES
|
||||
No = NO
|
||||
Cancel = CANCEL
|
||||
Question = ICON_QUESTION
|
||||
|
||||
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:
|
||||
def error(self) -> int:
|
||||
return 0
|
||||
|
||||
def errorString(self) -> str:
|
||||
return ""
|
||||
|
||||
def readAll(self) -> bytes:
|
||||
return b""
|
||||
|
||||
def deleteLater(self) -> None:
|
||||
pass
|
||||
|
||||
QNetworkReply = _MockQNetworkReply
|
||||
|
||||
class _MockWidget:
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
class _MockLayout:
|
||||
def addWidget(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def addLayout(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def addStretch(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):
|
||||
self.clicked = lambda *a, **k: None
|
||||
|
||||
QWidget = _MockWidget
|
||||
QGridLayout = _MockLayout
|
||||
QLabel = _MockLabel
|
||||
QLineEdit = _MockLineEdit
|
||||
QGroupBox = _MockWidget
|
||||
QVBoxLayout = _MockLayout
|
||||
QPushButton = _MockButton
|
||||
|
||||
class _MockQCoreApplication:
|
||||
pass
|
||||
|
||||
QCoreApplication = _MockQCoreApplication
|
||||
|
||||
|
||||
class _MockQDockWidget(_MockWidget):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._object_name = ""
|
||||
|
||||
def setObjectName(self, name: str) -> None:
|
||||
self._object_name = name
|
||||
|
||||
def objectName(self) -> str:
|
||||
return self._object_name
|
||||
|
||||
def show(self) -> None:
|
||||
pass
|
||||
|
||||
def deleteLater(self) -> None:
|
||||
pass
|
||||
QDockWidget = _MockQDockWidget
|
||||
class _MockAction:
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._checked = False
|
||||
self.triggered = lambda *a, **k: None
|
||||
|
||||
def setToolTip(self, text: str) -> None:
|
||||
pass
|
||||
|
||||
def setCheckable(self, value: bool) -> None:
|
||||
pass
|
||||
|
||||
def setChecked(self, value: bool) -> None:
|
||||
self._checked = value
|
||||
|
||||
|
||||
class _MockMenu:
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._actions = []
|
||||
|
||||
def addAction(self, action):
|
||||
self._actions.append(action)
|
||||
|
||||
def removeAction(self, action):
|
||||
if action in self._actions:
|
||||
self._actions.remove(action)
|
||||
|
||||
def clear(self):
|
||||
self._actions.clear()
|
||||
|
||||
def menuAction(self):
|
||||
return self
|
||||
|
||||
|
||||
class _MockToolBar:
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._actions = []
|
||||
|
||||
def setObjectName(self, name: str) -> None:
|
||||
pass
|
||||
|
||||
def addAction(self, action):
|
||||
self._actions.append(action)
|
||||
|
||||
def removeAction(self, action):
|
||||
if action in self._actions:
|
||||
self._actions.remove(action)
|
||||
|
||||
def clear(self):
|
||||
self._actions.clear()
|
||||
|
||||
|
||||
class _MockActionGroup:
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._actions = []
|
||||
|
||||
def setExclusive(self, value: bool) -> None:
|
||||
pass
|
||||
|
||||
def addAction(self, action):
|
||||
self._actions.append(action)
|
||||
QAction = _MockAction
|
||||
QMenu = _MockMenu
|
||||
QToolBar = _MockToolBar
|
||||
QActionGroup = _MockActionGroup
|
||||
|
||||
|
||||
def exec_dialog(dialog: Any) -> Any:
|
||||
return YES
|
||||
class _MockTabWidget:
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._tabs = []
|
||||
|
||||
def addTab(self, widget, title: str):
|
||||
self._tabs.append((widget, title))
|
||||
QTabWidget = _MockTabWidget
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""
|
||||
sn_basis/funktions/settings_logic.py – Logik zum Lesen und Schreiben der Plugin-Einstellungen
|
||||
über den zentralen qgisqt_wrapper.
|
||||
sn_basis/functions/settings_logic.py – Logik zum Lesen und Schreiben der Plugin-Einstellungen
|
||||
über den zentralen variable_wrapper.
|
||||
"""
|
||||
|
||||
from sn_basis.functions.qgisqt_wrapper import (
|
||||
from sn_basis.functions.variable_wrapper import (
|
||||
get_variable,
|
||||
set_variable,
|
||||
)
|
||||
@@ -27,17 +27,17 @@ class SettingsLogic:
|
||||
"landkreise_proj",
|
||||
]
|
||||
|
||||
def load(self) -> dict:
|
||||
def load(self) -> dict[str, str]:
|
||||
"""
|
||||
Lädt alle Variablen aus dem Projekt.
|
||||
Rückgabe: dict mit allen Werten (leere Strings, wenn nicht gesetzt).
|
||||
"""
|
||||
daten = {}
|
||||
daten: dict[str, str] = {}
|
||||
for key in self.VARIABLEN:
|
||||
daten[key] = get_variable(key, scope="project")
|
||||
return daten
|
||||
|
||||
def save(self, daten: dict):
|
||||
def save(self, daten: dict[str, str]) -> None:
|
||||
"""
|
||||
Speichert alle übergebenen Variablen im Projekt.
|
||||
daten: dict mit key → value
|
||||
|
||||
104
functions/sys_wrapper.py
Normal file
104
functions/sys_wrapper.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""
|
||||
sn_basis/functions/sys_wrapper.py – System- und Pfad-Abstraktion
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Union
|
||||
import sys
|
||||
|
||||
|
||||
_PathLike = Union[str, Path]
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Plugin Root
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def get_plugin_root() -> Path:
|
||||
"""
|
||||
Liefert das Basisverzeichnis des Plugins.
|
||||
"""
|
||||
return Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Pfad-Utilities
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def join_path(*parts: _PathLike) -> Path:
|
||||
"""
|
||||
Verbindet Pfadbestandteile OS-sicher.
|
||||
"""
|
||||
path = Path(parts[0])
|
||||
for part in parts[1:]:
|
||||
path /= part
|
||||
return path
|
||||
|
||||
|
||||
def file_exists(path: _PathLike) -> bool:
|
||||
"""
|
||||
Prüft, ob eine Datei existiert.
|
||||
"""
|
||||
try:
|
||||
return Path(path).exists()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def ensure_dir(path: _PathLike) -> Path:
|
||||
"""
|
||||
Stellt sicher, dass ein Verzeichnis existiert.
|
||||
"""
|
||||
p = Path(path)
|
||||
p.mkdir(parents=True, exist_ok=True)
|
||||
return p
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Datei-IO
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def read_text(path: _PathLike, encoding: str = "utf-8") -> str:
|
||||
"""
|
||||
Liest eine Textdatei.
|
||||
"""
|
||||
try:
|
||||
return Path(path).read_text(encoding=encoding)
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def write_text(
|
||||
path: _PathLike,
|
||||
content: str,
|
||||
encoding: str = "utf-8",
|
||||
) -> bool:
|
||||
"""
|
||||
Schreibt eine Textdatei.
|
||||
"""
|
||||
try:
|
||||
Path(path).write_text(content, encoding=encoding)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
|
||||
def add_to_sys_path(path: Union[str, Path]) -> None:
|
||||
"""
|
||||
Fügt einen Pfad zu sys.path hinzu, falls er noch nicht enthalten ist.
|
||||
"""
|
||||
p = str(path)
|
||||
if p not in sys.path:
|
||||
sys.path.insert(0, p)
|
||||
def getattr_safe(obj, attr, default=None):
|
||||
"""
|
||||
Sicherer Zugriff auf ein Attribut.
|
||||
|
||||
Gibt das Attribut zurück, wenn es existiert,
|
||||
ansonsten den Default-Wert (None, wenn nicht angegeben).
|
||||
"""
|
||||
try:
|
||||
return getattr(obj, attr)
|
||||
except Exception:
|
||||
return default
|
||||
@@ -1,185 +0,0 @@
|
||||
"""
|
||||
snbasis/functions/syswrapper.py – zentrale OS-/Dateisystem-Abstraktion
|
||||
Robust, testfreundlich, mock-fähig.
|
||||
"""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Dateisystem‑Funktionen
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def file_exists(path: str) -> bool:
|
||||
"""Prüft, ob eine Datei existiert."""
|
||||
try:
|
||||
return os.path.exists(path)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def is_file(path: str) -> bool:
|
||||
"""Prüft, ob ein Pfad eine Datei ist."""
|
||||
try:
|
||||
return os.path.isfile(path)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def is_dir(path: str) -> bool:
|
||||
"""Prüft, ob ein Pfad ein Verzeichnis ist."""
|
||||
try:
|
||||
return os.path.isdir(path)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def join_path(*parts) -> str:
|
||||
"""Verbindet Pfadbestandteile OS‑unabhängig."""
|
||||
try:
|
||||
return os.path.join(*parts)
|
||||
except Exception:
|
||||
# Fallback: naive Verkettung
|
||||
return "/".join(str(p) for p in parts)
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Pfad‑ und Systemfunktionen
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def get_temp_dir() -> str:
|
||||
"""Gibt das temporäre Verzeichnis zurück."""
|
||||
try:
|
||||
return tempfile.gettempdir()
|
||||
except Exception:
|
||||
return "/tmp"
|
||||
|
||||
|
||||
def get_plugin_root() -> str:
|
||||
"""
|
||||
Ermittelt den Plugin‑Root‑Pfad.
|
||||
Annahme: syswrapper liegt in sn_basis/funktions/
|
||||
→ also zwei Ebenen hoch.
|
||||
"""
|
||||
try:
|
||||
here = pathlib.Path(__file__).resolve()
|
||||
return str(here.parent.parent)
|
||||
except Exception:
|
||||
# Fallback: aktuelles Arbeitsverzeichnis
|
||||
return os.getcwd()
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Datei‑I/O (optional, aber nützlich)
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def read_file(path: str, mode="r"):
|
||||
"""Liest eine Datei ein. Gibt None zurück, wenn Fehler auftreten."""
|
||||
try:
|
||||
with open(path, mode) as f:
|
||||
return f.read()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def write_file(path: str, data, mode="w"):
|
||||
"""Schreibt Daten in eine Datei. Gibt True/False zurück."""
|
||||
try:
|
||||
with open(path, mode) as f:
|
||||
f.write(data)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Mock‑Modus (optional erweiterbar)
|
||||
# ---------------------------------------------------------
|
||||
|
||||
class FakeFileSystem:
|
||||
"""
|
||||
Minimaler Mock‑Dateisystem‑Ersatz.
|
||||
Wird nicht automatisch aktiviert, aber kann in Tests gepatcht werden.
|
||||
"""
|
||||
files = {}
|
||||
|
||||
@classmethod
|
||||
def add_file(cls, path, content=""):
|
||||
cls.files[path] = content
|
||||
|
||||
@classmethod
|
||||
def exists(cls, path):
|
||||
return path in cls.files
|
||||
|
||||
@classmethod
|
||||
def read(cls, path):
|
||||
return cls.files.get(path, None)
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Betriebssystem‑Erkennung
|
||||
# ---------------------------------------------------------
|
||||
|
||||
import platform
|
||||
|
||||
def get_os() -> str:
|
||||
"""
|
||||
Gibt das Betriebssystem zurück:
|
||||
- 'windows'
|
||||
- 'linux'
|
||||
- 'mac'
|
||||
"""
|
||||
system = platform.system().lower()
|
||||
|
||||
if "windows" in system:
|
||||
return "windows"
|
||||
if "darwin" in system:
|
||||
return "mac"
|
||||
if "linux" in system:
|
||||
return "linux"
|
||||
|
||||
return "unknown"
|
||||
|
||||
|
||||
def is_windows() -> bool:
|
||||
return get_os() == "windows"
|
||||
|
||||
|
||||
def is_linux() -> bool:
|
||||
return get_os() == "linux"
|
||||
|
||||
|
||||
def is_mac() -> bool:
|
||||
return get_os() == "mac"
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Pfad‑Normalisierung
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def normalize_path(path: str) -> str:
|
||||
"""
|
||||
Normalisiert Pfade OS‑unabhängig:
|
||||
- ersetzt Backslashes durch Slashes
|
||||
- entfernt doppelte Slashes
|
||||
- löst relative Pfade auf
|
||||
"""
|
||||
try:
|
||||
p = pathlib.Path(path).resolve()
|
||||
return str(p)
|
||||
except Exception:
|
||||
# Fallback: einfache Normalisierung
|
||||
return path.replace("\\", "/").replace("//", "/")
|
||||
|
||||
def add_to_sys_path(path: str) -> None:
|
||||
"""
|
||||
Fügt einen Pfad sicher zum Python-Importpfad hinzu.
|
||||
"""
|
||||
try:
|
||||
if path not in sys.path:
|
||||
sys.path.insert(0, path)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
115
functions/variable_wrapper.py
Normal file
115
functions/variable_wrapper.py
Normal file
@@ -0,0 +1,115 @@
|
||||
"""
|
||||
variable_wrapper.py – QGIS-Variablen-Abstraktion
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from sn_basis.functions.qgiscore_wrapper import QgsProject
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Versuch: QgsExpressionContextUtils importieren
|
||||
# ---------------------------------------------------------
|
||||
|
||||
try:
|
||||
from qgis.core import QgsExpressionContextUtils
|
||||
|
||||
_HAS_QGIS_VARIABLES = True
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Mock-Modus
|
||||
# ---------------------------------------------------------
|
||||
|
||||
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()
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Öffentliche API
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def get_variable(key: str, scope: str = "project") -> str:
|
||||
"""
|
||||
Liest eine QGIS-Variable.
|
||||
|
||||
:param key: Variablenname ohne Prefix
|
||||
:param scope: 'project' oder 'global'
|
||||
"""
|
||||
var_name = f"sn_{key}"
|
||||
|
||||
if scope == "project":
|
||||
project = QgsProject.instance()
|
||||
return (
|
||||
QgsExpressionContextUtils
|
||||
.projectScope(project)
|
||||
.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:
|
||||
"""
|
||||
Setzt eine QGIS-Variable.
|
||||
|
||||
:param key: Variablenname ohne Prefix
|
||||
:param value: Wert
|
||||
:param scope: 'project' oder 'global'
|
||||
"""
|
||||
var_name = f"sn_{key}"
|
||||
|
||||
if scope == "project":
|
||||
project = QgsProject.instance()
|
||||
QgsExpressionContextUtils.setProjectVariable(
|
||||
project,
|
||||
var_name,
|
||||
value,
|
||||
)
|
||||
return
|
||||
|
||||
if scope == "global":
|
||||
QgsExpressionContextUtils.setGlobalVariable(
|
||||
var_name,
|
||||
value,
|
||||
)
|
||||
return
|
||||
|
||||
raise ValueError("Scope muss 'project' oder 'global' sein.")
|
||||
Reference in New Issue
Block a user