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
|
sn_basis/functions/settings_logic.py – Logik zum Lesen und Schreiben der Plugin-Einstellungen
|
||||||
über den zentralen qgisqt_wrapper.
|
über den zentralen variable_wrapper.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from sn_basis.functions.qgisqt_wrapper import (
|
from sn_basis.functions.variable_wrapper import (
|
||||||
get_variable,
|
get_variable,
|
||||||
set_variable,
|
set_variable,
|
||||||
)
|
)
|
||||||
@@ -27,17 +27,17 @@ class SettingsLogic:
|
|||||||
"landkreise_proj",
|
"landkreise_proj",
|
||||||
]
|
]
|
||||||
|
|
||||||
def load(self) -> dict:
|
def load(self) -> dict[str, str]:
|
||||||
"""
|
"""
|
||||||
Lädt alle Variablen aus dem Projekt.
|
Lädt alle Variablen aus dem Projekt.
|
||||||
Rückgabe: dict mit allen Werten (leere Strings, wenn nicht gesetzt).
|
Rückgabe: dict mit allen Werten (leere Strings, wenn nicht gesetzt).
|
||||||
"""
|
"""
|
||||||
daten = {}
|
daten: dict[str, str] = {}
|
||||||
for key in self.VARIABLEN:
|
for key in self.VARIABLEN:
|
||||||
daten[key] = get_variable(key, scope="project")
|
daten[key] = get_variable(key, scope="project")
|
||||||
return daten
|
return daten
|
||||||
|
|
||||||
def save(self, daten: dict):
|
def save(self, daten: dict[str, str]) -> None:
|
||||||
"""
|
"""
|
||||||
Speichert alle übergebenen Variablen im Projekt.
|
Speichert alle übergebenen Variablen im Projekt.
|
||||||
daten: dict mit key → value
|
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.")
|
||||||
29
main.py
29
main.py
@@ -1,13 +1,20 @@
|
|||||||
# sn_basis/main.py
|
# sn_basis/main.py
|
||||||
|
|
||||||
from sn_basis.functions.qgisqt_wrapper import QCoreApplication, getattr_safe
|
|
||||||
from qgis.utils import plugins
|
from qgis.utils import plugins
|
||||||
|
|
||||||
|
from sn_basis.functions.qt_wrapper import QCoreApplication
|
||||||
|
from sn_basis.functions.sys_wrapper import getattr_safe
|
||||||
from sn_basis.ui.navigation import Navigation
|
from sn_basis.ui.navigation import Navigation
|
||||||
|
|
||||||
|
|
||||||
class BasisPlugin:
|
class BasisPlugin:
|
||||||
|
"""
|
||||||
|
Einstiegspunkt des sn_basis-Plugins.
|
||||||
|
Orchestriert UI und Fachmodule – keine UI-Logik.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, iface):
|
def __init__(self, iface):
|
||||||
self.iface = iface
|
# iface wird von QGIS übergeben, aber nicht direkt verwendet
|
||||||
self.ui = None
|
self.ui = None
|
||||||
|
|
||||||
# QCoreApplication kann im Mock-Modus None sein
|
# QCoreApplication kann im Mock-Modus None sein
|
||||||
@@ -21,10 +28,12 @@ class BasisPlugin:
|
|||||||
connect(self.unload)
|
connect(self.unload)
|
||||||
|
|
||||||
def initGui(self):
|
def initGui(self):
|
||||||
# Basis-Navigation neu aufbauen
|
"""
|
||||||
self.ui = Navigation(self.iface)
|
Initialisiert die Basis-Navigation und triggert initGui
|
||||||
|
aller abhängigen sn_-Plugins.
|
||||||
# Alle Fachplugins mit "sn_" prüfen und neu initialisieren
|
"""
|
||||||
|
self.ui = Navigation()
|
||||||
|
self.ui.init_ui()
|
||||||
for name, plugin in plugins.items():
|
for name, plugin in plugins.items():
|
||||||
if name.startswith("sn_") and name != "sn_basis":
|
if name.startswith("sn_") and name != "sn_basis":
|
||||||
try:
|
try:
|
||||||
@@ -33,10 +42,12 @@ class BasisPlugin:
|
|||||||
init_gui()
|
init_gui()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Fehler beim Neuinitialisieren von {name}: {e}")
|
print(f"Fehler beim Neuinitialisieren von {name}: {e}")
|
||||||
|
self.ui.finalize_menu_and_toolbar()
|
||||||
|
|
||||||
def unload(self):
|
def unload(self):
|
||||||
|
"""
|
||||||
|
Räumt UI-Komponenten sauber auf.
|
||||||
|
"""
|
||||||
if self.ui:
|
if self.ui:
|
||||||
remove_all = getattr_safe(self.ui, "remove_all")
|
self.ui.remove_all()
|
||||||
if callable(remove_all):
|
|
||||||
remove_all()
|
|
||||||
self.ui = None
|
self.ui = None
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
"""
|
"""
|
||||||
sn_basis/modulesdateipruefer.py – Prüfung von Dateieingaben für das Plugin.
|
sn_basis/modules/Dateipruefer.py – Prüfung von Dateieingaben für das Plugin.
|
||||||
Verwendet syswrapper und gibt pruef_ergebnis an den Pruefmanager zurück.
|
Verwendet sys_wrapper und gibt pruef_ergebnis an den Pruefmanager zurück.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from sn_basis.functions.syswrapper import (
|
from pathlib import Path
|
||||||
file_exists,
|
|
||||||
is_file,
|
from sn_basis.functions import (
|
||||||
join_path,
|
join_path,
|
||||||
|
file_exists,
|
||||||
)
|
)
|
||||||
|
|
||||||
from sn_basis.modules.Pruefmanager import pruef_ergebnis
|
from sn_basis.modules.Pruefmanager import pruef_ergebnis
|
||||||
@@ -32,13 +33,14 @@ class Dateipruefer:
|
|||||||
self.standarddatei = standarddatei
|
self.standarddatei = standarddatei
|
||||||
self.temporaer_erlaubt = temporaer_erlaubt
|
self.temporaer_erlaubt = temporaer_erlaubt
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# Hilfsfunktion
|
# Hilfsfunktion
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
def _pfad(self, relativer_pfad: str) -> str:
|
def _pfad(self, relativer_pfad: str) -> Path:
|
||||||
"""Erzeugt einen OS‑unabhängigen Pfad relativ zum Basisverzeichnis."""
|
"""
|
||||||
|
Erzeugt einen OS‑unabhängigen Pfad relativ zum Basisverzeichnis.
|
||||||
|
"""
|
||||||
return join_path(self.basis_pfad, relativer_pfad)
|
return join_path(self.basis_pfad, relativer_pfad)
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
@@ -62,12 +64,12 @@ class Dateipruefer:
|
|||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
pfad = self._pfad(self.pfad)
|
pfad = self._pfad(self.pfad)
|
||||||
|
|
||||||
if not file_exists(pfad) or not is_file(pfad):
|
if not file_exists(pfad):
|
||||||
return pruef_ergebnis(
|
return pruef_ergebnis(
|
||||||
ok=False,
|
ok=False,
|
||||||
meldung=f"Die Datei '{self.pfad}' wurde nicht gefunden.",
|
meldung=f"Die Datei '{self.pfad}' wurde nicht gefunden.",
|
||||||
aktion="datei_nicht_gefunden",
|
aktion="datei_nicht_gefunden",
|
||||||
pfad=pfad,
|
kontext=pfad,
|
||||||
)
|
)
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
@@ -77,7 +79,7 @@ class Dateipruefer:
|
|||||||
ok=True,
|
ok=True,
|
||||||
meldung="Datei gefunden.",
|
meldung="Datei gefunden.",
|
||||||
aktion="ok",
|
aktion="ok",
|
||||||
pfad=pfad,
|
kontext=pfad,
|
||||||
)
|
)
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
@@ -96,25 +98,31 @@ class Dateipruefer:
|
|||||||
ok=False,
|
ok=False,
|
||||||
meldung="Das Dateifeld ist leer. Soll ohne Datei fortgefahren werden?",
|
meldung="Das Dateifeld ist leer. Soll ohne Datei fortgefahren werden?",
|
||||||
aktion="leereingabe_erlaubt",
|
aktion="leereingabe_erlaubt",
|
||||||
pfad=None,
|
kontext=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 2. Standarddatei verfügbar → Nutzer fragen, ob sie verwendet werden soll
|
# 2. Standarddatei verfügbar → Nutzer fragen, ob sie verwendet werden soll
|
||||||
if self.standarddatei:
|
if self.standarddatei:
|
||||||
return pruef_ergebnis(
|
return pruef_ergebnis(
|
||||||
ok=False,
|
ok=False,
|
||||||
meldung=f"Es wurde keine Datei angegeben. Soll die Standarddatei '{self.standarddatei}' verwendet werden?",
|
meldung=(
|
||||||
|
f"Es wurde keine Datei angegeben. "
|
||||||
|
f"Soll die Standarddatei '{self.standarddatei}' verwendet werden?"
|
||||||
|
),
|
||||||
aktion="standarddatei_vorschlagen",
|
aktion="standarddatei_vorschlagen",
|
||||||
pfad=self._pfad(self.standarddatei),
|
kontext=self._pfad(self.standarddatei),
|
||||||
)
|
)
|
||||||
|
|
||||||
# 3. Temporäre Datei erlaubt → Nutzer fragen, ob temporär gearbeitet werden soll
|
# 3. Temporäre Datei erlaubt → Nutzer fragen, ob temporär gearbeitet werden soll
|
||||||
if self.temporaer_erlaubt:
|
if self.temporaer_erlaubt:
|
||||||
return pruef_ergebnis(
|
return pruef_ergebnis(
|
||||||
ok=False,
|
ok=False,
|
||||||
meldung="Es wurde keine Datei angegeben. Soll eine temporäre Datei erzeugt werden?",
|
meldung=(
|
||||||
|
"Es wurde keine Datei angegeben. "
|
||||||
|
"Soll eine temporäre Datei erzeugt werden?"
|
||||||
|
),
|
||||||
aktion="temporaer_erlaubt",
|
aktion="temporaer_erlaubt",
|
||||||
pfad=None,
|
kontext=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 4. Leereingabe nicht erlaubt → Fehler
|
# 4. Leereingabe nicht erlaubt → Fehler
|
||||||
@@ -122,5 +130,5 @@ class Dateipruefer:
|
|||||||
ok=False,
|
ok=False,
|
||||||
meldung="Es wurde keine Datei angegeben.",
|
meldung="Es wurde keine Datei angegeben.",
|
||||||
aktion="leereingabe_nicht_erlaubt",
|
aktion="leereingabe_nicht_erlaubt",
|
||||||
pfad=None,
|
kontext=None,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
"""
|
"""
|
||||||
sn_basis/modules/pruefmanager.py – zentrale Verarbeitung von pruef_ergebnis-Objekten.
|
sn_basis/modules/Pruefmanager.py – zentrale Verarbeitung von pruef_ergebnis-Objekten.
|
||||||
Steuert die Nutzerinteraktion über qgisqt_wrapper.
|
Steuert die Nutzerinteraktion über Wrapper.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from sn_basis.functions.qgisqt_wrapper import (
|
from sn_basis.functions import (
|
||||||
ask_yes_no,
|
ask_yes_no,
|
||||||
info,
|
info,
|
||||||
warning,
|
warning,
|
||||||
error,
|
error,
|
||||||
set_layer_visible, # optional, falls implementiert
|
set_layer_visible,
|
||||||
)
|
)
|
||||||
|
|
||||||
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
||||||
@@ -36,6 +36,7 @@ class Pruefmanager:
|
|||||||
return ergebnis
|
return ergebnis
|
||||||
|
|
||||||
aktion = ergebnis.aktion
|
aktion = ergebnis.aktion
|
||||||
|
kontext = ergebnis.kontext
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# Allgemeine Aktionen
|
# Allgemeine Aktionen
|
||||||
@@ -47,7 +48,12 @@ class Pruefmanager:
|
|||||||
|
|
||||||
if aktion == "leereingabe_erlaubt":
|
if aktion == "leereingabe_erlaubt":
|
||||||
if ask_yes_no("Ohne Eingabe fortfahren", ergebnis.meldung):
|
if ask_yes_no("Ohne Eingabe fortfahren", ergebnis.meldung):
|
||||||
return pruef_ergebnis(True, "Ohne Eingabe fortgefahren.", "ok", None)
|
return pruef_ergebnis(
|
||||||
|
ok=True,
|
||||||
|
meldung="Ohne Eingabe fortgefahren.",
|
||||||
|
aktion="ok",
|
||||||
|
kontext=None,
|
||||||
|
)
|
||||||
return ergebnis
|
return ergebnis
|
||||||
|
|
||||||
if aktion == "leereingabe_nicht_erlaubt":
|
if aktion == "leereingabe_nicht_erlaubt":
|
||||||
@@ -56,12 +62,22 @@ class Pruefmanager:
|
|||||||
|
|
||||||
if aktion == "standarddatei_vorschlagen":
|
if aktion == "standarddatei_vorschlagen":
|
||||||
if ask_yes_no("Standarddatei verwenden", ergebnis.meldung):
|
if ask_yes_no("Standarddatei verwenden", ergebnis.meldung):
|
||||||
return pruef_ergebnis(True, "Standarddatei wird verwendet.", "ok", ergebnis.pfad)
|
return pruef_ergebnis(
|
||||||
|
ok=True,
|
||||||
|
meldung="Standarddatei wird verwendet.",
|
||||||
|
aktion="ok",
|
||||||
|
kontext=kontext,
|
||||||
|
)
|
||||||
return ergebnis
|
return ergebnis
|
||||||
|
|
||||||
if aktion == "temporaer_erlaubt":
|
if aktion == "temporaer_erlaubt":
|
||||||
if ask_yes_no("Temporäre Datei erzeugen", ergebnis.meldung):
|
if ask_yes_no("Temporäre Datei erzeugen", ergebnis.meldung):
|
||||||
return pruef_ergebnis(True, "Temporäre Datei soll erzeugt werden.", "temporaer_erzeugen", None)
|
return pruef_ergebnis(
|
||||||
|
ok=True,
|
||||||
|
meldung="Temporäre Datei soll erzeugt werden.",
|
||||||
|
aktion="temporaer_erzeugen",
|
||||||
|
kontext=None,
|
||||||
|
)
|
||||||
return ergebnis
|
return ergebnis
|
||||||
|
|
||||||
if aktion == "datei_nicht_gefunden":
|
if aktion == "datei_nicht_gefunden":
|
||||||
@@ -94,12 +110,18 @@ class Pruefmanager:
|
|||||||
|
|
||||||
if aktion == "layer_unsichtbar":
|
if aktion == "layer_unsichtbar":
|
||||||
if ask_yes_no("Layer einblenden", ergebnis.meldung):
|
if ask_yes_no("Layer einblenden", ergebnis.meldung):
|
||||||
# Falls set_layer_visible implementiert ist
|
if kontext is not None:
|
||||||
try:
|
try:
|
||||||
set_layer_visible(ergebnis.pfad, True)
|
set_layer_visible(kontext, True)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return pruef_ergebnis(True, "Layer wurde eingeblendet.", "ok", ergebnis.pfad)
|
|
||||||
|
return pruef_ergebnis(
|
||||||
|
ok=True,
|
||||||
|
meldung="Layer wurde eingeblendet.",
|
||||||
|
aktion="ok",
|
||||||
|
kontext=kontext,
|
||||||
|
)
|
||||||
return ergebnis
|
return ergebnis
|
||||||
|
|
||||||
if aktion == "falscher_geotyp":
|
if aktion == "falscher_geotyp":
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"""
|
"""
|
||||||
sn_basis/modules/layerpruefer.py – Prüfung von QGIS-Layern.
|
sn_basis/modules/layerpruefer.py – Prüfung von QGIS-Layern.
|
||||||
Verwendet ausschließlich qgisqt_wrapper und gibt pruef_ergebnis zurück.
|
Verwendet ausschließlich Wrapper und gibt pruef_ergebnis zurück.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from sn_basis.functions.qgisqt_wrapper import (
|
from sn_basis.functions import (
|
||||||
layer_exists,
|
layer_exists,
|
||||||
get_layer_geometry_type,
|
get_layer_geometry_type,
|
||||||
get_layer_feature_count,
|
get_layer_feature_count,
|
||||||
@@ -15,7 +15,7 @@ from sn_basis.functions.qgisqt_wrapper import (
|
|||||||
is_layer_editable,
|
is_layer_editable,
|
||||||
)
|
)
|
||||||
|
|
||||||
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis, PruefAktion
|
||||||
|
|
||||||
|
|
||||||
class Layerpruefer:
|
class Layerpruefer:
|
||||||
@@ -57,8 +57,8 @@ class Layerpruefer:
|
|||||||
return pruef_ergebnis(
|
return pruef_ergebnis(
|
||||||
ok=False,
|
ok=False,
|
||||||
meldung="Der Layer existiert nicht oder wurde nicht geladen.",
|
meldung="Der Layer existiert nicht oder wurde nicht geladen.",
|
||||||
aktion="layer_nicht_gefunden", # type: ignore
|
aktion="layer_nicht_gefunden",
|
||||||
pfad=None,
|
kontext=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
@@ -69,8 +69,8 @@ class Layerpruefer:
|
|||||||
return pruef_ergebnis(
|
return pruef_ergebnis(
|
||||||
ok=False,
|
ok=False,
|
||||||
meldung="Der Layer ist unsichtbar. Soll er eingeblendet werden?",
|
meldung="Der Layer ist unsichtbar. Soll er eingeblendet werden?",
|
||||||
aktion="layer_unsichtbar", # type: ignore
|
aktion="layer_unsichtbar",
|
||||||
pfad=self.layer, # Layerobjekt wird übergeben
|
kontext=self.layer, # Layerobjekt als Kontext
|
||||||
)
|
)
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
@@ -80,9 +80,12 @@ class Layerpruefer:
|
|||||||
if self.erwarteter_layertyp and layertyp != self.erwarteter_layertyp:
|
if self.erwarteter_layertyp and layertyp != self.erwarteter_layertyp:
|
||||||
return pruef_ergebnis(
|
return pruef_ergebnis(
|
||||||
ok=False,
|
ok=False,
|
||||||
meldung=f"Der Layer hat den Typ '{layertyp}', erwartet wurde '{self.erwarteter_layertyp}'.",
|
meldung=(
|
||||||
|
f"Der Layer hat den Typ '{layertyp}', "
|
||||||
|
f"erwartet wurde '{self.erwarteter_layertyp}'."
|
||||||
|
),
|
||||||
aktion="falscher_layertyp",
|
aktion="falscher_layertyp",
|
||||||
pfad=None,
|
kontext=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
@@ -92,9 +95,12 @@ class Layerpruefer:
|
|||||||
if self.erwarteter_geotyp and geotyp != self.erwarteter_geotyp:
|
if self.erwarteter_geotyp and geotyp != self.erwarteter_geotyp:
|
||||||
return pruef_ergebnis(
|
return pruef_ergebnis(
|
||||||
ok=False,
|
ok=False,
|
||||||
meldung=f"Der Layer hat den Geometrietyp '{geotyp}', erwartet wurde '{self.erwarteter_geotyp}'.",
|
meldung=(
|
||||||
|
f"Der Layer hat den Geometrietyp '{geotyp}', "
|
||||||
|
f"erwartet wurde '{self.erwarteter_geotyp}'."
|
||||||
|
),
|
||||||
aktion="falscher_geotyp",
|
aktion="falscher_geotyp",
|
||||||
pfad=None,
|
kontext=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
@@ -106,7 +112,7 @@ class Layerpruefer:
|
|||||||
ok=False,
|
ok=False,
|
||||||
meldung="Der Layer enthält keine Objekte.",
|
meldung="Der Layer enthält keine Objekte.",
|
||||||
aktion="layer_leer",
|
aktion="layer_leer",
|
||||||
pfad=None,
|
kontext=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
@@ -116,9 +122,12 @@ class Layerpruefer:
|
|||||||
if self.erwartetes_crs and crs != self.erwartetes_crs:
|
if self.erwartetes_crs and crs != self.erwartetes_crs:
|
||||||
return pruef_ergebnis(
|
return pruef_ergebnis(
|
||||||
ok=False,
|
ok=False,
|
||||||
meldung=f"Der Layer hat das CRS '{crs}', erwartet wurde '{self.erwartetes_crs}'.",
|
meldung=(
|
||||||
|
f"Der Layer hat das CRS '{crs}', "
|
||||||
|
f"erwartet wurde '{self.erwartetes_crs}'."
|
||||||
|
),
|
||||||
aktion="falsches_crs",
|
aktion="falsches_crs",
|
||||||
pfad=None,
|
kontext=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
@@ -130,9 +139,12 @@ class Layerpruefer:
|
|||||||
if fehlende:
|
if fehlende:
|
||||||
return pruef_ergebnis(
|
return pruef_ergebnis(
|
||||||
ok=False,
|
ok=False,
|
||||||
meldung=f"Der Layer enthält nicht alle erforderlichen Felder: {', '.join(fehlende)}",
|
meldung=(
|
||||||
|
"Der Layer enthält nicht alle erforderlichen Felder: "
|
||||||
|
+ ", ".join(fehlende)
|
||||||
|
),
|
||||||
aktion="felder_fehlen",
|
aktion="felder_fehlen",
|
||||||
pfad=None,
|
kontext=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
@@ -144,7 +156,7 @@ class Layerpruefer:
|
|||||||
ok=False,
|
ok=False,
|
||||||
meldung=f"Die Datenquelle '{quelle}' ist nicht erlaubt.",
|
meldung=f"Die Datenquelle '{quelle}' ist nicht erlaubt.",
|
||||||
aktion="datenquelle_unerwartet",
|
aktion="datenquelle_unerwartet",
|
||||||
pfad=None,
|
kontext=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
@@ -156,7 +168,7 @@ class Layerpruefer:
|
|||||||
ok=False,
|
ok=False,
|
||||||
meldung="Der Layer ist nicht editierbar.",
|
meldung="Der Layer ist nicht editierbar.",
|
||||||
aktion="layer_nicht_editierbar",
|
aktion="layer_nicht_editierbar",
|
||||||
pfad=None,
|
kontext=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
@@ -166,5 +178,5 @@ class Layerpruefer:
|
|||||||
ok=True,
|
ok=True,
|
||||||
meldung="Layerprüfung erfolgreich.",
|
meldung="Layerprüfung erfolgreich.",
|
||||||
aktion="ok",
|
aktion="ok",
|
||||||
pfad=None,
|
kontext=None,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,20 +1,17 @@
|
|||||||
"""
|
"""
|
||||||
sn_basis/modules/linkpruefer.py – Prüfung von URLs und lokalen Links.
|
sn_basis/modules/linkpruefer.py – Prüfung von URLs und lokalen Links.
|
||||||
Verwendet syswrapper und qgisqt_wrapper.
|
Verwendet Wrapper und gibt pruef_ergebnis an den Pruefmanager zurück.
|
||||||
Gibt pruef_ergebnis an den Pruefmanager zurück.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from sn_basis.functions.syswrapper import (
|
from pathlib import Path
|
||||||
file_exists,
|
|
||||||
is_file,
|
|
||||||
join_path,
|
|
||||||
)
|
|
||||||
|
|
||||||
from sn_basis.functions.qgisqt_wrapper import (
|
from sn_basis.functions import (
|
||||||
|
file_exists,
|
||||||
|
join_path,
|
||||||
network_head,
|
network_head,
|
||||||
)
|
)
|
||||||
|
|
||||||
from sn_basis.modules.Pruefmanager import pruef_ergebnis
|
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis, PruefAktion
|
||||||
|
|
||||||
|
|
||||||
class Linkpruefer:
|
class Linkpruefer:
|
||||||
@@ -33,14 +30,18 @@ class Linkpruefer:
|
|||||||
# Hilfsfunktionen
|
# Hilfsfunktionen
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
def _pfad(self, relativer_pfad: str) -> str:
|
def _pfad(self, relativer_pfad: str) -> Path:
|
||||||
"""Erzeugt einen OS-unabhängigen Pfad relativ zum Basisverzeichnis."""
|
"""
|
||||||
|
Erzeugt einen OS‑unabhängigen Pfad relativ zum Basisverzeichnis.
|
||||||
|
"""
|
||||||
if not self.basis:
|
if not self.basis:
|
||||||
return relativer_pfad
|
return Path(relativer_pfad)
|
||||||
return join_path(self.basis, relativer_pfad)
|
return join_path(self.basis, relativer_pfad)
|
||||||
|
|
||||||
def _ist_url(self, text: str) -> bool:
|
def _ist_url(self, text: str) -> bool:
|
||||||
"""Einfache URL-Erkennung."""
|
"""
|
||||||
|
Einfache URL-Erkennung.
|
||||||
|
"""
|
||||||
return text.startswith("http://") or text.startswith("https://")
|
return text.startswith("http://") or text.startswith("https://")
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
@@ -58,7 +59,7 @@ class Linkpruefer:
|
|||||||
ok=False,
|
ok=False,
|
||||||
meldung="Es wurde kein Link angegeben.",
|
meldung="Es wurde kein Link angegeben.",
|
||||||
aktion="leer",
|
aktion="leer",
|
||||||
pfad=None,
|
kontext=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
@@ -88,7 +89,7 @@ class Linkpruefer:
|
|||||||
ok=False,
|
ok=False,
|
||||||
meldung=f"Die URL '{url}' konnte nicht geprüft werden.",
|
meldung=f"Die URL '{url}' konnte nicht geprüft werden.",
|
||||||
aktion="netzwerkfehler",
|
aktion="netzwerkfehler",
|
||||||
pfad=url,
|
kontext=url,
|
||||||
)
|
)
|
||||||
|
|
||||||
if reply.error != 0:
|
if reply.error != 0:
|
||||||
@@ -96,14 +97,14 @@ class Linkpruefer:
|
|||||||
ok=False,
|
ok=False,
|
||||||
meldung=f"Die URL '{url}' ist nicht erreichbar.",
|
meldung=f"Die URL '{url}' ist nicht erreichbar.",
|
||||||
aktion="url_nicht_erreichbar",
|
aktion="url_nicht_erreichbar",
|
||||||
pfad=url,
|
kontext=url,
|
||||||
)
|
)
|
||||||
|
|
||||||
return pruef_ergebnis(
|
return pruef_ergebnis(
|
||||||
ok=True,
|
ok=True,
|
||||||
meldung="URL ist erreichbar.",
|
meldung="URL ist erreichbar.",
|
||||||
aktion="ok",
|
aktion="ok",
|
||||||
pfad=url,
|
kontext=url,
|
||||||
)
|
)
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
@@ -122,20 +123,12 @@ class Linkpruefer:
|
|||||||
ok=False,
|
ok=False,
|
||||||
meldung=f"Der Pfad '{eingabe}' wurde nicht gefunden.",
|
meldung=f"Der Pfad '{eingabe}' wurde nicht gefunden.",
|
||||||
aktion="pfad_nicht_gefunden",
|
aktion="pfad_nicht_gefunden",
|
||||||
pfad=pfad,
|
kontext=pfad,
|
||||||
)
|
|
||||||
|
|
||||||
if not is_file(pfad):
|
|
||||||
return pruef_ergebnis(
|
|
||||||
ok=False,
|
|
||||||
meldung=f"Der Pfad '{eingabe}' ist keine Datei.",
|
|
||||||
aktion="kein_dateipfad",
|
|
||||||
pfad=pfad,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return pruef_ergebnis(
|
return pruef_ergebnis(
|
||||||
ok=True,
|
ok=True,
|
||||||
meldung="Dateipfad ist gültig.",
|
meldung="Dateipfad ist gültig.",
|
||||||
aktion="ok",
|
aktion="ok",
|
||||||
pfad=pfad,
|
kontext=pfad,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
"""
|
"""
|
||||||
sn_basis/modules/pruef_ergebnis.py – Ergebnisobjekt für alle Prüfer.
|
sn_basis/modules/pruef_ergebnis.py – Ergebnisobjekt für alle Prüfer.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Optional, Literal
|
from pathlib import Path
|
||||||
|
from typing import Any, Optional, Literal
|
||||||
|
|
||||||
|
|
||||||
# Alle möglichen Aktionen, die ein Prüfer auslösen kann.
|
# Alle möglichen Aktionen, die ein Prüfer auslösen kann.
|
||||||
@@ -31,28 +31,19 @@ PruefAktion = Literal[
|
|||||||
"temporaer_erzeugen",
|
"temporaer_erzeugen",
|
||||||
"stil_nicht_anwendbar",
|
"stil_nicht_anwendbar",
|
||||||
"layer_unsichtbar",
|
"layer_unsichtbar",
|
||||||
|
"layer_nicht_gefunden",
|
||||||
"unbekannt",
|
"unbekannt",
|
||||||
|
"stil_anwendbar",
|
||||||
|
"falsche_endung",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
|
@dataclass(slots=True)
|
||||||
class pruef_ergebnis:
|
class pruef_ergebnis:
|
||||||
"""
|
|
||||||
Reines Datenobjekt, das das Ergebnis einer Prüfung beschreibt.
|
|
||||||
|
|
||||||
ok: True → Prüfung erfolgreich
|
|
||||||
False → Nutzerinteraktion oder Fehler nötig
|
|
||||||
|
|
||||||
meldung: Text, der dem Nutzer angezeigt werden soll
|
|
||||||
|
|
||||||
aktion: Maschinenlesbarer Code, der dem Pruefmanager sagt,
|
|
||||||
wie er weiter verfahren soll
|
|
||||||
|
|
||||||
pfad: Optionaler Pfad oder URL, die geprüft wurde oder
|
|
||||||
verwendet werden soll
|
|
||||||
"""
|
|
||||||
|
|
||||||
ok: bool
|
ok: bool
|
||||||
meldung: str
|
meldung: str
|
||||||
aktion: PruefAktion
|
aktion: PruefAktion
|
||||||
pfad: Optional[str] = None
|
kontext: Optional[Any] = None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,59 +1,75 @@
|
|||||||
"""
|
"""
|
||||||
sn_basis/modules/stilpruefer.py – Prüfung und Anwendung von Layerstilen.
|
sn_basis/modules/stilpruefer.py – Prüfung von Layerstilen.
|
||||||
Verwendet ausschließlich qgisqt_wrapper und gibt pruef_ergebnis zurück.
|
Prüft ausschließlich, ob ein Stilpfad gültig ist.
|
||||||
|
Die Anwendung erfolgt später über eine Aktion.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from sn_basis.functions.qgisqt_wrapper import (
|
from pathlib import Path
|
||||||
apply_style,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
from sn_basis.functions import file_exists
|
||||||
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
||||||
|
|
||||||
|
|
||||||
class Stilpruefer:
|
class Stilpruefer:
|
||||||
"""
|
"""
|
||||||
Prüft, ob ein Stil auf einen Layer angewendet werden kann.
|
Prüft, ob ein Stilpfad gültig ist und angewendet werden kann.
|
||||||
Die eigentliche Nutzerinteraktion übernimmt der Pruefmanager.
|
Keine Seiteneffekte, keine QGIS-Aufrufe.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, layer, stil_pfad: str):
|
def __init__(self):
|
||||||
"""
|
pass
|
||||||
layer: QGIS-Layer oder Mock-Layer
|
|
||||||
stil_pfad: relativer oder absoluter Pfad zum .qml-Stil
|
|
||||||
"""
|
|
||||||
self.layer = layer
|
|
||||||
self.stil_pfad = stil_pfad
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# Hauptfunktion
|
# Hauptfunktion
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
def pruefe(self) -> pruef_ergebnis:
|
def pruefe(self, stil_pfad: str) -> pruef_ergebnis:
|
||||||
"""
|
"""
|
||||||
Versucht, den Stil anzuwenden.
|
Prüft einen Stilpfad.
|
||||||
Rückgabe: pruef_ergebnis
|
Rückgabe: pruef_ergebnis
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Wrapper übernimmt:
|
# -----------------------------------------------------
|
||||||
# - Pfadberechnung
|
# 1. Kein Stil angegeben → OK
|
||||||
# - Existenzprüfung
|
# -----------------------------------------------------
|
||||||
# - loadNamedStyle
|
if not stil_pfad:
|
||||||
# - Fehlerbehandlung
|
|
||||||
# - Mock-Modus
|
|
||||||
erfolg, meldung = apply_style(self.layer, self.stil_pfad)
|
|
||||||
|
|
||||||
if erfolg:
|
|
||||||
return pruef_ergebnis(
|
return pruef_ergebnis(
|
||||||
ok=True,
|
ok=True,
|
||||||
meldung=f"Stil erfolgreich angewendet: {self.stil_pfad}",
|
meldung="Kein Stil angegeben.",
|
||||||
aktion="ok",
|
aktion="ok",
|
||||||
pfad=self.stil_pfad,
|
kontext=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Fehlerfall → Nutzerinteraktion nötig
|
pfad = Path(stil_pfad)
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# 2. Datei existiert nicht
|
||||||
|
# -----------------------------------------------------
|
||||||
|
if not file_exists(pfad):
|
||||||
return pruef_ergebnis(
|
return pruef_ergebnis(
|
||||||
ok=False,
|
ok=False,
|
||||||
meldung=meldung,
|
meldung=f"Die Stil-Datei '{stil_pfad}' wurde nicht gefunden.",
|
||||||
aktion="stil_nicht_anwendbar",
|
aktion="datei_nicht_gefunden",
|
||||||
pfad=self.stil_pfad,
|
kontext=pfad,
|
||||||
|
)
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# 3. Falsche Endung
|
||||||
|
# -----------------------------------------------------
|
||||||
|
if pfad.suffix.lower() != ".qml":
|
||||||
|
return pruef_ergebnis(
|
||||||
|
ok=False,
|
||||||
|
meldung="Die Stil-Datei muss die Endung '.qml' haben.",
|
||||||
|
aktion="falsche_endung",
|
||||||
|
kontext=pfad,
|
||||||
|
)
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# 4. Stil ist gültig → Anwendung später
|
||||||
|
# -----------------------------------------------------
|
||||||
|
return pruef_ergebnis(
|
||||||
|
ok=True,
|
||||||
|
meldung="Stil-Datei ist gültig.",
|
||||||
|
aktion="stil_anwendbar",
|
||||||
|
kontext=pfad,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,18 +11,22 @@ import inspect
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Pre-Bootstrap: Plugin-Root in sys.path eintragen
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
|
THIS_FILE = Path(__file__).resolve()
|
||||||
|
PLUGIN_ROOT = THIS_FILE.parents[2]
|
||||||
|
|
||||||
# Minimaler Bootstrap, um sn_basis importierbar zu machen
|
if str(PLUGIN_ROOT) not in sys.path:
|
||||||
TEST_DIR = os.path.dirname(__file__)
|
sys.path.insert(0, str(PLUGIN_ROOT))
|
||||||
PLUGIN_ROOT = os.path.abspath(os.path.join(TEST_DIR, "..", ".."))
|
|
||||||
|
|
||||||
if PLUGIN_ROOT not in sys.path:
|
from sn_basis.functions import (
|
||||||
sys.path.insert(0, PLUGIN_ROOT)
|
get_plugin_root,
|
||||||
|
add_to_sys_path,
|
||||||
|
)
|
||||||
from sn_basis.functions import syswrapper
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# Bootstrap: Plugin-Root in sys.path eintragen
|
# Bootstrap: Plugin-Root in sys.path eintragen
|
||||||
@@ -33,13 +37,12 @@ def bootstrap():
|
|||||||
Simuliert das QGIS-Plugin-Startverhalten:
|
Simuliert das QGIS-Plugin-Startverhalten:
|
||||||
stellt sicher, dass sn_basis importierbar ist.
|
stellt sicher, dass sn_basis importierbar ist.
|
||||||
"""
|
"""
|
||||||
plugin_root = syswrapper.get_plugin_root()
|
plugin_root = get_plugin_root()
|
||||||
syswrapper.add_to_sys_path(plugin_root)
|
add_to_sys_path(plugin_root)
|
||||||
|
|
||||||
|
|
||||||
bootstrap()
|
bootstrap()
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# Farben
|
# Farben
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
@@ -53,13 +56,15 @@ RESET = "\033[0m"
|
|||||||
|
|
||||||
GLOBAL_TEST_COUNTER = 0
|
GLOBAL_TEST_COUNTER = 0
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# Farbige TestResult-Klasse
|
# Farbige TestResult-Klasse
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
class ColoredTestResult(unittest.TextTestResult):
|
class ColoredTestResult(unittest.TextTestResult):
|
||||||
|
|
||||||
|
_last_test_class: type | None = None
|
||||||
|
|
||||||
|
|
||||||
def startTest(self, test):
|
def startTest(self, test):
|
||||||
global GLOBAL_TEST_COUNTER
|
global GLOBAL_TEST_COUNTER
|
||||||
GLOBAL_TEST_COUNTER += 1
|
GLOBAL_TEST_COUNTER += 1
|
||||||
@@ -93,16 +98,19 @@ class ColoredTestResult(unittest.TextTestResult):
|
|||||||
super().addSuccess(test)
|
super().addSuccess(test)
|
||||||
self.stream.write(f"{GREEN}OK{RESET}\n")
|
self.stream.write(f"{GREEN}OK{RESET}\n")
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# Farbiger TestRunner
|
# Farbiger TestRunner
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
class ColoredTestRunner(unittest.TextTestRunner):
|
class ColoredTestRunner(unittest.TextTestRunner):
|
||||||
resultclass = ColoredTestResult
|
|
||||||
|
|
||||||
def _makeResult(self):
|
def _makeResult(self):
|
||||||
result = super()._makeResult()
|
result = ColoredTestResult(
|
||||||
|
self.stream,
|
||||||
|
self.descriptions,
|
||||||
|
self.verbosity,
|
||||||
|
)
|
||||||
|
|
||||||
original_start_test = result.startTest
|
original_start_test = result.startTest
|
||||||
|
|
||||||
def patched_start_test(test):
|
def patched_start_test(test):
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
from sn_basis.functions import syswrapper
|
from sn_basis.functions import sys_wrapper
|
||||||
syswrapper.add_to_sys_path(syswrapper.get_plugin_root())
|
sys_wrapper.add_to_sys_path(sys_wrapper.get_plugin_root())
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
# sn_basis/test/test_dateipruefer.py
|
# sn_basis/test/test_dateipruefer.py
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from sn_basis.modules.Dateipruefer import Dateipruefer
|
from sn_basis.modules.Dateipruefer import Dateipruefer
|
||||||
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class TestDateipruefer(unittest.TestCase):
|
class TestDateipruefer(unittest.TestCase):
|
||||||
@@ -24,6 +22,7 @@ class TestDateipruefer(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertFalse(result.ok)
|
self.assertFalse(result.ok)
|
||||||
self.assertEqual(result.aktion, "leereingabe_erlaubt")
|
self.assertEqual(result.aktion, "leereingabe_erlaubt")
|
||||||
|
self.assertIsNone(result.kontext)
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# 2. Leere Eingabe nicht erlaubt
|
# 2. Leere Eingabe nicht erlaubt
|
||||||
@@ -38,6 +37,7 @@ class TestDateipruefer(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertFalse(result.ok)
|
self.assertFalse(result.ok)
|
||||||
self.assertEqual(result.aktion, "leereingabe_nicht_erlaubt")
|
self.assertEqual(result.aktion, "leereingabe_nicht_erlaubt")
|
||||||
|
self.assertIsNone(result.kontext)
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# 3. Standarddatei vorschlagen
|
# 3. Standarddatei vorschlagen
|
||||||
@@ -52,7 +52,7 @@ class TestDateipruefer(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertFalse(result.ok)
|
self.assertFalse(result.ok)
|
||||||
self.assertEqual(result.aktion, "standarddatei_vorschlagen")
|
self.assertEqual(result.aktion, "standarddatei_vorschlagen")
|
||||||
self.assertEqual(result.pfad, "/tmp/std.txt")
|
self.assertEqual(result.kontext, Path("/tmp/std.txt"))
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# 4. Temporäre Datei erlaubt
|
# 4. Temporäre Datei erlaubt
|
||||||
@@ -67,11 +67,12 @@ class TestDateipruefer(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertFalse(result.ok)
|
self.assertFalse(result.ok)
|
||||||
self.assertEqual(result.aktion, "temporaer_erlaubt")
|
self.assertEqual(result.aktion, "temporaer_erlaubt")
|
||||||
|
self.assertIsNone(result.kontext)
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# 5. Datei existiert nicht
|
# 5. Datei existiert nicht
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
@patch("sn_basis.functions.syswrapper.file_exists", return_value=False)
|
@patch("sn_basis.modules.Dateipruefer.file_exists", return_value=False)
|
||||||
def test_datei_nicht_gefunden(self, mock_exists):
|
def test_datei_nicht_gefunden(self, mock_exists):
|
||||||
pruefer = Dateipruefer(
|
pruefer = Dateipruefer(
|
||||||
pfad="/tmp/nichtvorhanden.txt"
|
pfad="/tmp/nichtvorhanden.txt"
|
||||||
@@ -81,13 +82,13 @@ class TestDateipruefer(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertFalse(result.ok)
|
self.assertFalse(result.ok)
|
||||||
self.assertEqual(result.aktion, "datei_nicht_gefunden")
|
self.assertEqual(result.aktion, "datei_nicht_gefunden")
|
||||||
|
self.assertEqual(result.kontext, Path("/tmp/nichtvorhanden.txt"))
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# 6. Datei existiert
|
# 6. Datei existiert
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
@patch("sn_basis.functions.syswrapper.file_exists", return_value=True)
|
@patch("sn_basis.modules.Dateipruefer.file_exists", return_value=True)
|
||||||
@patch("sn_basis.functions.syswrapper.is_file", return_value=True)
|
def test_datei_ok(self, mock_exists):
|
||||||
def test_datei_ok(self, mock_isfile, mock_exists):
|
|
||||||
pruefer = Dateipruefer(
|
pruefer = Dateipruefer(
|
||||||
pfad="/tmp/test.txt"
|
pfad="/tmp/test.txt"
|
||||||
)
|
)
|
||||||
@@ -96,7 +97,7 @@ class TestDateipruefer(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertTrue(result.ok)
|
self.assertTrue(result.ok)
|
||||||
self.assertEqual(result.aktion, "ok")
|
self.assertEqual(result.aktion, "ok")
|
||||||
self.assertEqual(result.pfad, "/tmp/test.txt")
|
self.assertEqual(result.kontext, Path("/tmp/test.txt"))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -78,18 +78,19 @@ def mock_is_layer_editable(layer):
|
|||||||
class TestLayerpruefer(unittest.TestCase):
|
class TestLayerpruefer(unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
# Monkeypatching der Wrapper-Funktionen
|
# Monkeypatching der im Layerpruefer verwendeten Wrapper-Funktionen
|
||||||
import sn_basis.functions.qgisqt_wrapper as wrapper
|
import sn_basis.modules.layerpruefer as module
|
||||||
|
|
||||||
|
module.layer_exists = mock_layer_exists
|
||||||
|
module.is_layer_visible = mock_is_layer_visible
|
||||||
|
module.get_layer_type = mock_get_layer_type
|
||||||
|
module.get_layer_geometry_type = mock_get_layer_geometry_type
|
||||||
|
module.get_layer_feature_count = mock_get_layer_feature_count
|
||||||
|
module.get_layer_crs = mock_get_layer_crs
|
||||||
|
module.get_layer_fields = mock_get_layer_fields
|
||||||
|
module.get_layer_source = mock_get_layer_source
|
||||||
|
module.is_layer_editable = mock_is_layer_editable
|
||||||
|
|
||||||
wrapper.layer_exists = mock_layer_exists
|
|
||||||
wrapper.is_layer_visible = mock_is_layer_visible
|
|
||||||
wrapper.get_layer_type = mock_get_layer_type
|
|
||||||
wrapper.get_layer_geometry_type = mock_get_layer_geometry_type
|
|
||||||
wrapper.get_layer_feature_count = mock_get_layer_feature_count
|
|
||||||
wrapper.get_layer_crs = mock_get_layer_crs
|
|
||||||
wrapper.get_layer_fields = mock_get_layer_fields
|
|
||||||
wrapper.get_layer_source = mock_get_layer_source
|
|
||||||
wrapper.is_layer_editable = mock_is_layer_editable
|
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# Tests
|
# Tests
|
||||||
|
|||||||
@@ -1,107 +1,78 @@
|
|||||||
# sn_basis/test/test_linkpruefer.py
|
# sn_basis/test/test_linkpruefer.py
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from sn_basis.modules.linkpruefer import Linkpruefer
|
from sn_basis.modules.linkpruefer import Linkpruefer
|
||||||
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
from sn_basis.functions.qgiscore_wrapper import NetworkReply
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Mock-Ergebnisse für network_head()
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
|
|
||||||
class MockResponseOK:
|
|
||||||
ok = True
|
|
||||||
status = 200
|
|
||||||
error = None
|
|
||||||
|
|
||||||
|
|
||||||
class MockResponseNotFound:
|
|
||||||
ok = False
|
|
||||||
status = 404
|
|
||||||
error = "Not Found"
|
|
||||||
|
|
||||||
|
|
||||||
class MockResponseConnectionError:
|
|
||||||
ok = False
|
|
||||||
status = None
|
|
||||||
error = "Connection refused"
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Testklasse
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
|
|
||||||
class TestLinkpruefer(unittest.TestCase):
|
class TestLinkpruefer(unittest.TestCase):
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# 1. Remote-Link erreichbar
|
# 1. Remote-Link erreichbar
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
@patch("sn_basis.functions.qgisqt_wrapper.network_head")
|
@patch("sn_basis.modules.linkpruefer.network_head")
|
||||||
def test_remote_link_ok(self, mock_head):
|
def test_remote_link_ok(self, mock_head):
|
||||||
mock_head.return_value = MockResponseOK()
|
mock_head.return_value = NetworkReply(error=0)
|
||||||
|
|
||||||
lp = Linkpruefer("http://example.com", "REST")
|
lp = Linkpruefer()
|
||||||
result = lp.pruefe()
|
result = lp.pruefe("http://example.com")
|
||||||
|
|
||||||
self.assertTrue(result.ok)
|
self.assertTrue(result.ok)
|
||||||
self.assertEqual(result.aktion, "ok")
|
self.assertEqual(result.aktion, "ok")
|
||||||
|
self.assertEqual(result.kontext, "http://example.com")
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# 2. Remote-Link nicht erreichbar
|
# 2. Remote-Link nicht erreichbar
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
@patch("sn_basis.functions.qgisqt_wrapper.network_head")
|
@patch("sn_basis.modules.linkpruefer.network_head")
|
||||||
def test_remote_link_error(self, mock_head):
|
def test_remote_link_error(self, mock_head):
|
||||||
mock_head.return_value = MockResponseConnectionError()
|
mock_head.return_value = NetworkReply(error=1)
|
||||||
|
|
||||||
lp = Linkpruefer("http://example.com", "REST")
|
lp = Linkpruefer()
|
||||||
result = lp.pruefe()
|
result = lp.pruefe("http://example.com")
|
||||||
|
|
||||||
self.assertFalse(result.ok)
|
self.assertFalse(result.ok)
|
||||||
self.assertEqual(result.aktion, "url_nicht_erreichbar")
|
self.assertEqual(result.aktion, "url_nicht_erreichbar")
|
||||||
self.assertIn("Connection refused", result.meldung)
|
self.assertEqual(result.kontext, "http://example.com")
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# 3. Remote-Link 404
|
# 3. Netzwerkfehler (None)
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
@patch("sn_basis.functions.qgisqt_wrapper.network_head")
|
@patch("sn_basis.modules.linkpruefer.network_head", return_value=None)
|
||||||
def test_remote_link_404(self, mock_head):
|
def test_remote_link_network_error(self, mock_head):
|
||||||
mock_head.return_value = MockResponseNotFound()
|
lp = Linkpruefer()
|
||||||
|
result = lp.pruefe("http://example.com")
|
||||||
lp = Linkpruefer("http://example.com/missing", "REST")
|
|
||||||
result = lp.pruefe()
|
|
||||||
|
|
||||||
self.assertFalse(result.ok)
|
self.assertFalse(result.ok)
|
||||||
self.assertEqual(result.aktion, "url_nicht_erreichbar")
|
self.assertEqual(result.aktion, "netzwerkfehler")
|
||||||
self.assertIn("404", result.meldung)
|
self.assertEqual(result.kontext, "http://example.com")
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# 4. Lokaler Pfad existiert nicht
|
# 4. Lokaler Pfad existiert nicht
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
@patch("sn_basis.functions.syswrapper.file_exists")
|
@patch("sn_basis.modules.linkpruefer.file_exists", return_value=False)
|
||||||
def test_local_link_not_found(self, mock_exists):
|
def test_local_link_not_found(self, mock_exists):
|
||||||
mock_exists.return_value = False
|
lp = Linkpruefer()
|
||||||
|
result = lp.pruefe("/path/to/missing/file.shp")
|
||||||
lp = Linkpruefer("/path/to/missing/file.shp", "OGR")
|
|
||||||
result = lp.pruefe()
|
|
||||||
|
|
||||||
self.assertFalse(result.ok)
|
self.assertFalse(result.ok)
|
||||||
self.assertEqual(result.aktion, "pfad_nicht_gefunden")
|
self.assertEqual(result.aktion, "pfad_nicht_gefunden")
|
||||||
|
self.assertEqual(result.kontext, Path("/path/to/missing/file.shp"))
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# 5. Lokaler Pfad existiert, aber ungewöhnlich
|
# 5. Lokaler Pfad existiert
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
@patch("sn_basis.functions.syswrapper.file_exists")
|
@patch("sn_basis.modules.linkpruefer.file_exists", return_value=True)
|
||||||
def test_local_link_warning(self, mock_exists):
|
def test_local_link_ok(self, mock_exists):
|
||||||
mock_exists.return_value = True
|
lp = Linkpruefer()
|
||||||
|
result = lp.pruefe("/path/to/file.shp")
|
||||||
lp = Linkpruefer("/path/to/file_without_extension", "OGR")
|
|
||||||
result = lp.pruefe()
|
|
||||||
|
|
||||||
self.assertTrue(result.ok)
|
self.assertTrue(result.ok)
|
||||||
self.assertEqual(result.aktion, "ok")
|
self.assertEqual(result.aktion, "ok")
|
||||||
self.assertIn("ungewöhnlich", result.meldung)
|
self.assertEqual(result.kontext, Path("/path/to/file.shp"))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class TestPruefmanager(unittest.TestCase):
|
|||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# 2. Leere Eingabe erlaubt → Nutzer sagt JA
|
# 2. Leere Eingabe erlaubt → Nutzer sagt JA
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
@patch("sn_basis.functions.qgisqt_wrapper.ask_yes_no", return_value=True)
|
@patch("sn_basis.modules.Pruefmanager.ask_yes_no", return_value=True)
|
||||||
def test_leereingabe_erlaubt_ja(self, mock_ask):
|
def test_leereingabe_erlaubt_ja(self, mock_ask):
|
||||||
ergebnis = pruef_ergebnis(False, "Leer?", "leereingabe_erlaubt", None)
|
ergebnis = pruef_ergebnis(False, "Leer?", "leereingabe_erlaubt", None)
|
||||||
entscheidung = self.manager.verarbeite(ergebnis)
|
entscheidung = self.manager.verarbeite(ergebnis)
|
||||||
@@ -36,7 +36,7 @@ class TestPruefmanager(unittest.TestCase):
|
|||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# 3. Leere Eingabe erlaubt → Nutzer sagt NEIN
|
# 3. Leere Eingabe erlaubt → Nutzer sagt NEIN
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
@patch("sn_basis.functions.qgisqt_wrapper.ask_yes_no", return_value=False)
|
@patch("sn_basis.modules.Pruefmanager.ask_yes_no", return_value=False)
|
||||||
def test_leereingabe_erlaubt_nein(self, mock_ask):
|
def test_leereingabe_erlaubt_nein(self, mock_ask):
|
||||||
ergebnis = pruef_ergebnis(False, "Leer?", "leereingabe_erlaubt", None)
|
ergebnis = pruef_ergebnis(False, "Leer?", "leereingabe_erlaubt", None)
|
||||||
entscheidung = self.manager.verarbeite(ergebnis)
|
entscheidung = self.manager.verarbeite(ergebnis)
|
||||||
@@ -47,21 +47,33 @@ class TestPruefmanager(unittest.TestCase):
|
|||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# 4. Standarddatei vorschlagen → Nutzer sagt JA
|
# 4. Standarddatei vorschlagen → Nutzer sagt JA
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
@patch("sn_basis.functions.qgisqt_wrapper.ask_yes_no", return_value=True)
|
@patch("sn_basis.modules.Pruefmanager.ask_yes_no", return_value=True)
|
||||||
def test_standarddatei_vorschlagen_ja(self, mock_ask):
|
def test_standarddatei_vorschlagen_ja(self, mock_ask):
|
||||||
ergebnis = pruef_ergebnis(False, "Standarddatei verwenden?", "standarddatei_vorschlagen", "/tmp/std.txt")
|
ergebnis = pruef_ergebnis(
|
||||||
|
False,
|
||||||
|
"Standarddatei verwenden?",
|
||||||
|
"standarddatei_vorschlagen",
|
||||||
|
"/tmp/std.txt",
|
||||||
|
)
|
||||||
|
|
||||||
entscheidung = self.manager.verarbeite(ergebnis)
|
entscheidung = self.manager.verarbeite(ergebnis)
|
||||||
|
|
||||||
self.assertTrue(entscheidung.ok)
|
self.assertTrue(entscheidung.ok)
|
||||||
self.assertEqual(entscheidung.aktion, "ok")
|
self.assertEqual(entscheidung.aktion, "ok")
|
||||||
self.assertEqual(entscheidung.pfad, "/tmp/std.txt")
|
self.assertEqual(entscheidung.kontext, "/tmp/std.txt")
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# 5. Standarddatei vorschlagen → Nutzer sagt NEIN
|
# 5. Standarddatei vorschlagen → Nutzer sagt NEIN
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
@patch("sn_basis.functions.qgisqt_wrapper.ask_yes_no", return_value=False)
|
@patch("sn_basis.modules.Pruefmanager.ask_yes_no", return_value=False)
|
||||||
def test_standarddatei_vorschlagen_nein(self, mock_ask):
|
def test_standarddatei_vorschlagen_nein(self, mock_ask):
|
||||||
ergebnis = pruef_ergebnis(False, "Standarddatei verwenden?", "standarddatei_vorschlagen", "/tmp/std.txt")
|
ergebnis = pruef_ergebnis(
|
||||||
|
False,
|
||||||
|
"Standarddatei verwenden?",
|
||||||
|
"standarddatei_vorschlagen",
|
||||||
|
"/tmp/std.txt",
|
||||||
|
)
|
||||||
|
|
||||||
entscheidung = self.manager.verarbeite(ergebnis)
|
entscheidung = self.manager.verarbeite(ergebnis)
|
||||||
|
|
||||||
self.assertFalse(entscheidung.ok)
|
self.assertFalse(entscheidung.ok)
|
||||||
@@ -70,7 +82,7 @@ class TestPruefmanager(unittest.TestCase):
|
|||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# 6. Temporäre Datei erzeugen → Nutzer sagt JA
|
# 6. Temporäre Datei erzeugen → Nutzer sagt JA
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
@patch("sn_basis.functions.qgisqt_wrapper.ask_yes_no", return_value=True)
|
@patch("sn_basis.modules.Pruefmanager.ask_yes_no", return_value=True)
|
||||||
def test_temporaer_erlaubt_ja(self, mock_ask):
|
def test_temporaer_erlaubt_ja(self, mock_ask):
|
||||||
ergebnis = pruef_ergebnis(False, "Temporär?", "temporaer_erlaubt", None)
|
ergebnis = pruef_ergebnis(False, "Temporär?", "temporaer_erlaubt", None)
|
||||||
entscheidung = self.manager.verarbeite(ergebnis)
|
entscheidung = self.manager.verarbeite(ergebnis)
|
||||||
@@ -81,7 +93,7 @@ class TestPruefmanager(unittest.TestCase):
|
|||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# 7. Temporäre Datei erzeugen → Nutzer sagt NEIN
|
# 7. Temporäre Datei erzeugen → Nutzer sagt NEIN
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
@patch("sn_basis.functions.qgisqt_wrapper.ask_yes_no", return_value=False)
|
@patch("sn_basis.modules.Pruefmanager.ask_yes_no", return_value=False)
|
||||||
def test_temporaer_erlaubt_nein(self, mock_ask):
|
def test_temporaer_erlaubt_nein(self, mock_ask):
|
||||||
ergebnis = pruef_ergebnis(False, "Temporär?", "temporaer_erlaubt", None)
|
ergebnis = pruef_ergebnis(False, "Temporär?", "temporaer_erlaubt", None)
|
||||||
entscheidung = self.manager.verarbeite(ergebnis)
|
entscheidung = self.manager.verarbeite(ergebnis)
|
||||||
@@ -92,8 +104,8 @@ class TestPruefmanager(unittest.TestCase):
|
|||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# 8. Layer unsichtbar → Nutzer sagt JA
|
# 8. Layer unsichtbar → Nutzer sagt JA
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
@patch("sn_basis.functions.qgisqt_wrapper.ask_yes_no", return_value=True)
|
@patch("sn_basis.modules.Pruefmanager.ask_yes_no", return_value=True)
|
||||||
@patch("sn_basis.functions.qgisqt_wrapper.set_layer_visible")
|
@patch("sn_basis.modules.Pruefmanager.set_layer_visible")
|
||||||
def test_layer_unsichtbar_ja(self, mock_set, mock_ask):
|
def test_layer_unsichtbar_ja(self, mock_set, mock_ask):
|
||||||
fake_layer = object()
|
fake_layer = object()
|
||||||
ergebnis = pruef_ergebnis(False, "Layer unsichtbar", "layer_unsichtbar", fake_layer)
|
ergebnis = pruef_ergebnis(False, "Layer unsichtbar", "layer_unsichtbar", fake_layer)
|
||||||
@@ -107,7 +119,7 @@ class TestPruefmanager(unittest.TestCase):
|
|||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# 9. Layer unsichtbar → Nutzer sagt NEIN
|
# 9. Layer unsichtbar → Nutzer sagt NEIN
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
@patch("sn_basis.functions.qgisqt_wrapper.ask_yes_no", return_value=False)
|
@patch("sn_basis.modules.Pruefmanager.ask_yes_no", return_value=False)
|
||||||
def test_layer_unsichtbar_nein(self, mock_ask):
|
def test_layer_unsichtbar_nein(self, mock_ask):
|
||||||
fake_layer = object()
|
fake_layer = object()
|
||||||
ergebnis = pruef_ergebnis(False, "Layer unsichtbar", "layer_unsichtbar", fake_layer)
|
ergebnis = pruef_ergebnis(False, "Layer unsichtbar", "layer_unsichtbar", fake_layer)
|
||||||
@@ -120,7 +132,7 @@ class TestPruefmanager(unittest.TestCase):
|
|||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# 10. Fehlerhafte Aktion → Fallback
|
# 10. Fehlerhafte Aktion → Fallback
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
@patch("sn_basis.functions.qgisqt_wrapper.warning")
|
@patch("sn_basis.modules.Pruefmanager.warning")
|
||||||
def test_unbekannte_aktion(self, mock_warn):
|
def test_unbekannte_aktion(self, mock_warn):
|
||||||
ergebnis = pruef_ergebnis(False, "???", "unbekannt", None)
|
ergebnis = pruef_ergebnis(False, "???", "unbekannt", None)
|
||||||
entscheidung = self.manager.verarbeite(ergebnis)
|
entscheidung = self.manager.verarbeite(ergebnis)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class TestSettingsLogic(unittest.TestCase):
|
|||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# Test: load() liest alle Variablen über get_variable()
|
# Test: load() liest alle Variablen über get_variable()
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
@patch("sn_basis.functions.qgisqt_wrapper.get_variable")
|
@patch("sn_basis.functions.settings_logic.get_variable")
|
||||||
def test_load(self, mock_get):
|
def test_load(self, mock_get):
|
||||||
# Mock-Rückgabe für jede Variable
|
# Mock-Rückgabe für jede Variable
|
||||||
mock_get.side_effect = lambda key, scope="project": f"wert_{key}"
|
mock_get.side_effect = lambda key, scope="project": f"wert_{key}"
|
||||||
@@ -30,7 +30,7 @@ class TestSettingsLogic(unittest.TestCase):
|
|||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# Test: save() ruft set_variable() nur für bekannte Keys auf
|
# Test: save() ruft set_variable() nur für bekannte Keys auf
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
@patch("sn_basis.functions.qgisqt_wrapper.set_variable")
|
@patch("sn_basis.functions.settings_logic.set_variable")
|
||||||
def test_save(self, mock_set):
|
def test_save(self, mock_set):
|
||||||
logic = SettingsLogic()
|
logic = SettingsLogic()
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
import unittest
|
import unittest
|
||||||
import tempfile
|
import tempfile
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from sn_basis.modules.stilpruefer import Stilpruefer
|
from sn_basis.modules.stilpruefer import Stilpruefer
|
||||||
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
|
||||||
|
|
||||||
|
|
||||||
class TestStilpruefer(unittest.TestCase):
|
class TestStilpruefer(unittest.TestCase):
|
||||||
@@ -23,13 +23,13 @@ class TestStilpruefer(unittest.TestCase):
|
|||||||
self.assertTrue(result.ok)
|
self.assertTrue(result.ok)
|
||||||
self.assertEqual(result.aktion, "ok")
|
self.assertEqual(result.aktion, "ok")
|
||||||
self.assertIn("Kein Stil angegeben", result.meldung)
|
self.assertIn("Kein Stil angegeben", result.meldung)
|
||||||
|
self.assertIsNone(result.kontext)
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# 2. Datei existiert und ist .qml
|
# 2. Datei existiert und ist .qml
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
@patch("sn_basis.functions.syswrapper.file_exists", return_value=True)
|
@patch("sn_basis.modules.stilpruefer.file_exists", return_value=True)
|
||||||
@patch("sn_basis.functions.syswrapper.is_file", return_value=True)
|
def test_datei_existiert_mit_qml(self, mock_exists):
|
||||||
def test_datei_existiert_mit_qml(self, mock_isfile, mock_exists):
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".qml", delete=False) as tmp:
|
with tempfile.NamedTemporaryFile(suffix=".qml", delete=False) as tmp:
|
||||||
tmp_path = tmp.name
|
tmp_path = tmp.name
|
||||||
|
|
||||||
@@ -37,8 +37,8 @@ class TestStilpruefer(unittest.TestCase):
|
|||||||
result = self.pruefer.pruefe(tmp_path)
|
result = self.pruefer.pruefe(tmp_path)
|
||||||
|
|
||||||
self.assertTrue(result.ok)
|
self.assertTrue(result.ok)
|
||||||
self.assertEqual(result.aktion, "ok")
|
self.assertEqual(result.aktion, "stil_anwendbar")
|
||||||
self.assertEqual(result.pfad, tmp_path)
|
self.assertEqual(result.kontext, Path(tmp_path))
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
os.remove(tmp_path)
|
os.remove(tmp_path)
|
||||||
@@ -46,9 +46,8 @@ class TestStilpruefer(unittest.TestCase):
|
|||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# 3. Datei existiert, aber falsche Endung
|
# 3. Datei existiert, aber falsche Endung
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
@patch("sn_basis.functions.syswrapper.file_exists", return_value=True)
|
@patch("sn_basis.modules.stilpruefer.file_exists", return_value=True)
|
||||||
@patch("sn_basis.functions.syswrapper.is_file", return_value=True)
|
def test_datei_existiert_falsche_endung(self, mock_exists):
|
||||||
def test_datei_existiert_falsche_endung(self, mock_isfile, mock_exists):
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as tmp:
|
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as tmp:
|
||||||
tmp_path = tmp.name
|
tmp_path = tmp.name
|
||||||
|
|
||||||
@@ -58,6 +57,7 @@ class TestStilpruefer(unittest.TestCase):
|
|||||||
self.assertFalse(result.ok)
|
self.assertFalse(result.ok)
|
||||||
self.assertEqual(result.aktion, "falsche_endung")
|
self.assertEqual(result.aktion, "falsche_endung")
|
||||||
self.assertIn(".qml", result.meldung)
|
self.assertIn(".qml", result.meldung)
|
||||||
|
self.assertEqual(result.kontext, Path(tmp_path))
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
os.remove(tmp_path)
|
os.remove(tmp_path)
|
||||||
@@ -65,7 +65,7 @@ class TestStilpruefer(unittest.TestCase):
|
|||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# 4. Datei existiert nicht
|
# 4. Datei existiert nicht
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
@patch("sn_basis.functions.syswrapper.file_exists", return_value=False)
|
@patch("sn_basis.modules.stilpruefer.file_exists", return_value=False)
|
||||||
def test_datei_existiert_nicht(self, mock_exists):
|
def test_datei_existiert_nicht(self, mock_exists):
|
||||||
fake_path = "/tmp/nichtvorhanden.qml"
|
fake_path = "/tmp/nichtvorhanden.qml"
|
||||||
|
|
||||||
@@ -74,6 +74,7 @@ class TestStilpruefer(unittest.TestCase):
|
|||||||
self.assertFalse(result.ok)
|
self.assertFalse(result.ok)
|
||||||
self.assertEqual(result.aktion, "datei_nicht_gefunden")
|
self.assertEqual(result.aktion, "datei_nicht_gefunden")
|
||||||
self.assertIn("nicht gefunden", result.meldung)
|
self.assertIn("nicht gefunden", result.meldung)
|
||||||
|
self.assertEqual(result.kontext, Path(fake_path))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -1,164 +0,0 @@
|
|||||||
# sn_basis/test/test_wrapper.py
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
import os
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
# Wrapper importieren
|
|
||||||
import sn_basis.functions.syswrapper as syswrapper
|
|
||||||
import sn_basis.functions.qgisqt_wrapper as qgisqt
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Mock-Layer für qgisqt_wrapper
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
class MockLayer:
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
exists=True,
|
|
||||||
visible=True,
|
|
||||||
layer_type="vector",
|
|
||||||
geometry_type="Polygon",
|
|
||||||
feature_count=10,
|
|
||||||
crs="EPSG:25833",
|
|
||||||
fields=None,
|
|
||||||
source="/tmp/test.shp",
|
|
||||||
editable=True,
|
|
||||||
):
|
|
||||||
self.exists = exists
|
|
||||||
self.visible = visible
|
|
||||||
self.layer_type = layer_type
|
|
||||||
self.geometry_type = geometry_type
|
|
||||||
self.feature_count = feature_count
|
|
||||||
self.crs = crs
|
|
||||||
self.fields = fields or []
|
|
||||||
self.source = source
|
|
||||||
self.editable = editable
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Monkeypatching für qgisqt_wrapper
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
def mock_layer_exists(layer):
|
|
||||||
return layer is not None and layer.exists
|
|
||||||
|
|
||||||
|
|
||||||
def mock_is_layer_visible(layer):
|
|
||||||
return layer.visible
|
|
||||||
|
|
||||||
|
|
||||||
def mock_get_layer_type(layer):
|
|
||||||
return layer.layer_type
|
|
||||||
|
|
||||||
|
|
||||||
def mock_get_layer_geometry_type(layer):
|
|
||||||
return layer.geometry_type
|
|
||||||
|
|
||||||
|
|
||||||
def mock_get_layer_feature_count(layer):
|
|
||||||
return layer.feature_count
|
|
||||||
|
|
||||||
|
|
||||||
def mock_get_layer_crs(layer):
|
|
||||||
return layer.crs
|
|
||||||
|
|
||||||
|
|
||||||
def mock_get_layer_fields(layer):
|
|
||||||
return layer.fields
|
|
||||||
|
|
||||||
|
|
||||||
def mock_get_layer_source(layer):
|
|
||||||
return layer.source
|
|
||||||
|
|
||||||
|
|
||||||
def mock_is_layer_editable(layer):
|
|
||||||
return layer.editable
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Testklasse
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
class TestWrapper(unittest.TestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
# qgisqt_wrapper monkeypatchen
|
|
||||||
qgisqt.layer_exists = mock_layer_exists
|
|
||||||
qgisqt.is_layer_visible = mock_is_layer_visible
|
|
||||||
qgisqt.get_layer_type = mock_get_layer_type
|
|
||||||
qgisqt.get_layer_geometry_type = mock_get_layer_geometry_type
|
|
||||||
qgisqt.get_layer_feature_count = mock_get_layer_feature_count
|
|
||||||
qgisqt.get_layer_crs = mock_get_layer_crs
|
|
||||||
qgisqt.get_layer_fields = mock_get_layer_fields
|
|
||||||
qgisqt.get_layer_source = mock_get_layer_source
|
|
||||||
qgisqt.is_layer_editable = mock_is_layer_editable
|
|
||||||
|
|
||||||
# -----------------------------------------------------
|
|
||||||
# syswrapper Tests
|
|
||||||
# -----------------------------------------------------
|
|
||||||
|
|
||||||
def test_syswrapper_file_exists(self):
|
|
||||||
with tempfile.NamedTemporaryFile(delete=True) as tmp:
|
|
||||||
self.assertTrue(syswrapper.file_exists(tmp.name))
|
|
||||||
self.assertFalse(syswrapper.file_exists("/path/does/not/exist"))
|
|
||||||
|
|
||||||
def test_syswrapper_is_file(self):
|
|
||||||
with tempfile.NamedTemporaryFile(delete=True) as tmp:
|
|
||||||
self.assertTrue(syswrapper.is_file(tmp.name))
|
|
||||||
self.assertFalse(syswrapper.is_file("/path/does/not/exist"))
|
|
||||||
|
|
||||||
def test_syswrapper_join_path(self):
|
|
||||||
result = syswrapper.join_path("/tmp", "test.txt")
|
|
||||||
self.assertEqual(result, "/tmp/test.txt")
|
|
||||||
|
|
||||||
# -----------------------------------------------------
|
|
||||||
# qgisqt_wrapper Tests (Mock-Modus)
|
|
||||||
# -----------------------------------------------------
|
|
||||||
|
|
||||||
def test_qgisqt_layer_exists(self):
|
|
||||||
layer = MockLayer(exists=True)
|
|
||||||
self.assertTrue(qgisqt.layer_exists(layer))
|
|
||||||
|
|
||||||
layer = MockLayer(exists=False)
|
|
||||||
self.assertFalse(qgisqt.layer_exists(layer))
|
|
||||||
|
|
||||||
def test_qgisqt_layer_visible(self):
|
|
||||||
layer = MockLayer(visible=True)
|
|
||||||
self.assertTrue(qgisqt.is_layer_visible(layer))
|
|
||||||
|
|
||||||
layer = MockLayer(visible=False)
|
|
||||||
self.assertFalse(qgisqt.is_layer_visible(layer))
|
|
||||||
|
|
||||||
def test_qgisqt_layer_type(self):
|
|
||||||
layer = MockLayer(layer_type="vector")
|
|
||||||
self.assertEqual(qgisqt.get_layer_type(layer), "vector")
|
|
||||||
|
|
||||||
def test_qgisqt_geometry_type(self):
|
|
||||||
layer = MockLayer(geometry_type="Polygon")
|
|
||||||
self.assertEqual(qgisqt.get_layer_geometry_type(layer), "Polygon")
|
|
||||||
|
|
||||||
def test_qgisqt_feature_count(self):
|
|
||||||
layer = MockLayer(feature_count=12)
|
|
||||||
self.assertEqual(qgisqt.get_layer_feature_count(layer), 12)
|
|
||||||
|
|
||||||
def test_qgisqt_crs(self):
|
|
||||||
layer = MockLayer(crs="EPSG:4326")
|
|
||||||
self.assertEqual(qgisqt.get_layer_crs(layer), "EPSG:4326")
|
|
||||||
|
|
||||||
def test_qgisqt_fields(self):
|
|
||||||
layer = MockLayer(fields=["id", "name"])
|
|
||||||
self.assertEqual(qgisqt.get_layer_fields(layer), ["id", "name"])
|
|
||||||
|
|
||||||
def test_qgisqt_source(self):
|
|
||||||
layer = MockLayer(source="/tmp/test.shp")
|
|
||||||
self.assertEqual(qgisqt.get_layer_source(layer), "/tmp/test.shp")
|
|
||||||
|
|
||||||
def test_qgisqt_editable(self):
|
|
||||||
layer = MockLayer(editable=True)
|
|
||||||
self.assertTrue(qgisqt.is_layer_editable(layer))
|
|
||||||
|
|
||||||
layer = MockLayer(editable=False)
|
|
||||||
self.assertFalse(qgisqt.is_layer_editable(layer))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
@@ -1,12 +1,17 @@
|
|||||||
# sn_basis/ui/base_dockwidget.py
|
"""
|
||||||
|
sn_basis/ui/base_dockwidget.py
|
||||||
|
|
||||||
from qgis.PyQt.QtWidgets import QDockWidget, QTabWidget
|
Basis-Dockwidget für alle LNO-Module.
|
||||||
from sn_basis.functions.qgisqt_wrapper import warning, error
|
"""
|
||||||
|
|
||||||
|
from sn_basis.functions.qt_wrapper import QDockWidget, QTabWidget
|
||||||
|
from sn_basis.functions.message_wrapper import warning, error
|
||||||
|
|
||||||
|
|
||||||
class BaseDockWidget(QDockWidget):
|
class BaseDockWidget(QDockWidget):
|
||||||
"""
|
"""
|
||||||
Basis-Dockwidget für alle LNO-Module.
|
Basis-Dockwidget für alle LNO-Module.
|
||||||
|
|
||||||
- Titel wird automatisch aus base_title + subtitle erzeugt
|
- Titel wird automatisch aus base_title + subtitle erzeugt
|
||||||
- Tabs werden dynamisch aus der Klassenvariable 'tabs' erzeugt
|
- Tabs werden dynamisch aus der Klassenvariable 'tabs' erzeugt
|
||||||
- Die zugehörige Toolbar-Action wird beim Schließen zurückgesetzt
|
- Die zugehörige Toolbar-Action wird beim Schließen zurückgesetzt
|
||||||
@@ -23,19 +28,15 @@ class BaseDockWidget(QDockWidget):
|
|||||||
# Titel setzen
|
# Titel setzen
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
try:
|
try:
|
||||||
title = self.base_title if not subtitle else f"{self.base_title} | {subtitle}"
|
title = (
|
||||||
|
self.base_title
|
||||||
|
if not subtitle
|
||||||
|
else f"{self.base_title} | {subtitle}"
|
||||||
|
)
|
||||||
self.setWindowTitle(title)
|
self.setWindowTitle(title)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
warning("Titel konnte nicht gesetzt werden", str(e))
|
warning("Titel konnte nicht gesetzt werden", str(e))
|
||||||
|
|
||||||
# -----------------------------------------------------
|
|
||||||
# Dock-Features
|
|
||||||
# -----------------------------------------------------
|
|
||||||
try:
|
|
||||||
self.setFeatures(QDockWidget.DockWidgetFeature.DockWidgetClosable)
|
|
||||||
except Exception as e:
|
|
||||||
warning("Dock-Features konnten nicht gesetzt werden", str(e))
|
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# Tabs erzeugen
|
# Tabs erzeugen
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
@@ -45,15 +46,25 @@ class BaseDockWidget(QDockWidget):
|
|||||||
for tab_class in self.tabs:
|
for tab_class in self.tabs:
|
||||||
try:
|
try:
|
||||||
tab_instance = tab_class()
|
tab_instance = tab_class()
|
||||||
tab_title = getattr(tab_class, "tab_title", tab_class.__name__)
|
tab_title = getattr(
|
||||||
|
tab_class,
|
||||||
|
"tab_title",
|
||||||
|
tab_class.__name__,
|
||||||
|
)
|
||||||
tab_widget.addTab(tab_instance, tab_title)
|
tab_widget.addTab(tab_instance, tab_title)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error("Tab konnte nicht geladen werden", f"{tab_class}: {e}")
|
error(
|
||||||
|
"Tab konnte nicht geladen werden",
|
||||||
|
f"{tab_class}: {e}",
|
||||||
|
)
|
||||||
|
|
||||||
self.setWidget(tab_widget)
|
self.setWidget(tab_widget)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error("Tab-Widget konnte nicht initialisiert werden", str(e))
|
error(
|
||||||
|
"Tab-Widget konnte nicht initialisiert werden",
|
||||||
|
str(e),
|
||||||
|
)
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# Dock schließen
|
# Dock schließen
|
||||||
@@ -68,6 +79,9 @@ class BaseDockWidget(QDockWidget):
|
|||||||
if self.action:
|
if self.action:
|
||||||
self.action.setChecked(False)
|
self.action.setChecked(False)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
warning("Toolbar-Status konnte nicht zurückgesetzt werden", str(e))
|
warning(
|
||||||
|
"Toolbar-Status konnte nicht zurückgesetzt werden",
|
||||||
|
str(e),
|
||||||
|
)
|
||||||
|
|
||||||
super().closeEvent(event)
|
super().closeEvent(event)
|
||||||
|
|||||||
@@ -1,53 +1,69 @@
|
|||||||
# sn_basis/ui/dockmanager.py
|
"""
|
||||||
|
sn_basis/ui/dockmanager.py
|
||||||
|
|
||||||
from qgis.PyQt.QtCore import Qt
|
Verwaltet das Anzeigen und Ersetzen von DockWidgets.
|
||||||
from qgis.PyQt.QtWidgets import QDockWidget
|
Stellt sicher, dass immer nur ein sn_basis-Dock gleichzeitig sichtbar ist.
|
||||||
from qgis.utils import iface
|
"""
|
||||||
|
|
||||||
from sn_basis.functions.qgisqt_wrapper import warning, error
|
from typing import Any
|
||||||
|
|
||||||
|
from sn_basis.functions import (
|
||||||
|
add_dock_widget,
|
||||||
|
remove_dock_widget,
|
||||||
|
find_dock_widgets,
|
||||||
|
warning,
|
||||||
|
error,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DockManager:
|
class DockManager:
|
||||||
"""
|
"""
|
||||||
Verwaltet das Anzeigen und Ersetzen von DockWidgets.
|
Verwaltet das Anzeigen und Ersetzen von DockWidgets.
|
||||||
Stellt sicher, dass immer nur ein LNO-Dock gleichzeitig sichtbar ist.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
default_area = Qt.DockWidgetArea.RightDockWidgetArea
|
|
||||||
dock_prefix = "sn_dock_"
|
dock_prefix = "sn_dock_"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def show(cls, dock_widget, area=None):
|
def show(cls, dock_widget: Any, area=None) -> None:
|
||||||
"""
|
"""
|
||||||
Zeigt ein DockWidget an und entfernt vorher alle anderen
|
Zeigt ein DockWidget an und entfernt vorher alle anderen
|
||||||
LNO-Docks (erkennbar am Prefix 'sn_dock_').
|
sn_basis-Docks (erkennbar am Prefix 'sn_dock_').
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if dock_widget is None:
|
if dock_widget is None:
|
||||||
error("Dock konnte nicht angezeigt werden", "Dock-Widget ist None.")
|
error("Dock konnte nicht angezeigt werden", "Dock-Widget ist None.")
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
area = area or cls.default_area
|
# Sicherstellen, dass das Dock einen Namen hat
|
||||||
|
|
||||||
# Prüfen, ob das Dock einen gültigen Namen hat
|
|
||||||
if not dock_widget.objectName():
|
if not dock_widget.objectName():
|
||||||
dock_widget.setObjectName(f"{cls.dock_prefix}{id(dock_widget)}")
|
dock_widget.setObjectName(f"{cls.dock_prefix}{id(dock_widget)}")
|
||||||
|
|
||||||
# Bestehende Plugin-Docks schließen
|
# Vorhandene Plugin-Docks entfernen
|
||||||
try:
|
try:
|
||||||
for widget in iface.mainWindow().findChildren(QDockWidget):
|
for widget in find_dock_widgets():
|
||||||
if widget is not dock_widget and widget.objectName().startswith(cls.dock_prefix):
|
if (
|
||||||
iface.removeDockWidget(widget)
|
widget is not dock_widget
|
||||||
|
and widget.objectName().startswith(cls.dock_prefix)
|
||||||
|
):
|
||||||
|
remove_dock_widget(widget)
|
||||||
widget.deleteLater()
|
widget.deleteLater()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
warning("Vorherige Docks konnten nicht entfernt werden", str(e))
|
warning(
|
||||||
|
"Vorherige Docks konnten nicht entfernt werden",
|
||||||
|
str(e),
|
||||||
|
)
|
||||||
|
|
||||||
# Neues Dock anzeigen
|
# Neues Dock anzeigen
|
||||||
try:
|
try:
|
||||||
iface.addDockWidget(area, dock_widget)
|
add_dock_widget(area, dock_widget)
|
||||||
dock_widget.show()
|
dock_widget.show()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error("Dock konnte nicht angezeigt werden", str(e))
|
error(
|
||||||
|
"Dock konnte nicht angezeigt werden",
|
||||||
|
str(e),
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error("DockManager-Fehler", str(e))
|
error("DockManager-Fehler", str(e))
|
||||||
|
|
||||||
|
|||||||
121
ui/navigation.py
121
ui/navigation.py
@@ -1,84 +1,115 @@
|
|||||||
#sn_basis/ui/navigation.py
|
"""
|
||||||
from qgis.PyQt.QtWidgets import QAction, QMenu, QToolBar, QActionGroup
|
sn_basis/ui/navigation.py
|
||||||
|
|
||||||
|
Zentrale Navigation (Menü + Toolbar) für sn_basis.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Any, List, Tuple
|
||||||
|
|
||||||
|
from sn_basis.functions.qt_wrapper import (
|
||||||
|
QAction,
|
||||||
|
QMenu,
|
||||||
|
QToolBar,
|
||||||
|
QActionGroup,
|
||||||
|
)
|
||||||
|
from sn_basis.functions import (
|
||||||
|
get_main_window,
|
||||||
|
add_toolbar,
|
||||||
|
remove_toolbar,
|
||||||
|
add_menu,
|
||||||
|
remove_menu,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Navigation:
|
class Navigation:
|
||||||
def __init__(self, iface):
|
def __init__(self):
|
||||||
self.iface = iface
|
|
||||||
self.actions = []
|
self.actions = []
|
||||||
|
self.menu = None
|
||||||
|
self.toolbar = None
|
||||||
|
self.plugin_group = None
|
||||||
|
|
||||||
# Menü und Toolbar einmalig anlegen
|
|
||||||
self.menu = QMenu("LNO Sachsen", iface.mainWindow())
|
|
||||||
iface.mainWindow().menuBar().addMenu(self.menu)
|
|
||||||
|
|
||||||
self.toolbar = QToolBar("LNO Sachsen")
|
|
||||||
|
def init_ui(self):
|
||||||
|
print(">>> Navigation.init_ui() CALLED")
|
||||||
|
|
||||||
|
main_window = get_main_window()
|
||||||
|
if not main_window:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.menu = QMenu("LNO Sachsen", main_window)
|
||||||
|
add_menu(self.menu)
|
||||||
|
|
||||||
|
self.toolbar = QToolBar("LNO Sachsen", main_window)
|
||||||
self.toolbar.setObjectName("LnoSachsenToolbar")
|
self.toolbar.setObjectName("LnoSachsenToolbar")
|
||||||
iface.addToolBar(self.toolbar)
|
add_toolbar(self.toolbar)
|
||||||
|
|
||||||
# Gruppe für exklusive Auswahl (nur ein Plugin aktiv)
|
test_action = QAction("TEST ACTION", main_window)
|
||||||
self.plugin_group = QActionGroup(iface.mainWindow())
|
self.menu.addAction(test_action)
|
||||||
self.plugin_group.setExclusive(True)
|
self.toolbar.addAction(test_action)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------
|
||||||
|
# Actions
|
||||||
|
# -----------------------------------------------------
|
||||||
|
|
||||||
def add_action(self, text, callback, tooltip="", priority=100):
|
def add_action(self, text, callback, tooltip="", priority=100):
|
||||||
action = QAction(text, self.iface.mainWindow())
|
if not self.plugin_group:
|
||||||
|
return None
|
||||||
|
|
||||||
|
action = QAction(text, get_main_window())
|
||||||
action.setToolTip(tooltip)
|
action.setToolTip(tooltip)
|
||||||
action.setCheckable(True) # Button kann aktiv sein
|
action.setCheckable(True)
|
||||||
action.triggered.connect(callback)
|
action.triggered.connect(callback)
|
||||||
|
|
||||||
# Action in Gruppe aufnehmen
|
|
||||||
self.plugin_group.addAction(action)
|
self.plugin_group.addAction(action)
|
||||||
|
|
||||||
# Action mit Priority speichern
|
|
||||||
self.actions.append((priority, action))
|
self.actions.append((priority, action))
|
||||||
return action
|
return action
|
||||||
|
|
||||||
def finalize_menu_and_toolbar(self):
|
def finalize_menu_and_toolbar(self):
|
||||||
# Sortieren nach Priority
|
if not self.menu or not self.toolbar:
|
||||||
|
return
|
||||||
|
|
||||||
self.actions.sort(key=lambda x: x[0])
|
self.actions.sort(key=lambda x: x[0])
|
||||||
|
|
||||||
# Menüeinträge
|
|
||||||
self.menu.clear()
|
self.menu.clear()
|
||||||
|
self.toolbar.clear()
|
||||||
|
|
||||||
for _, action in self.actions:
|
for _, action in self.actions:
|
||||||
self.menu.addAction(action)
|
self.menu.addAction(action)
|
||||||
|
|
||||||
# Toolbar-Einträge
|
|
||||||
self.toolbar.clear()
|
|
||||||
for _, action in self.actions:
|
|
||||||
self.toolbar.addAction(action)
|
self.toolbar.addAction(action)
|
||||||
|
|
||||||
def set_active_plugin(self, active_action):
|
def set_active_plugin(self, active_action):
|
||||||
# Alle zurücksetzen, dann aktives Plugin markieren
|
|
||||||
for _, action in self.actions:
|
for _, action in self.actions:
|
||||||
action.setChecked(False)
|
action.setChecked(False)
|
||||||
if active_action:
|
if active_action:
|
||||||
active_action.setChecked(True)
|
active_action.setChecked(True)
|
||||||
|
|
||||||
def remove_all(self):
|
# -----------------------------------------------------
|
||||||
"""Alles entfernen beim Entladen des Basisplugins"""
|
# Cleanup
|
||||||
# Menü entfernen
|
# -----------------------------------------------------
|
||||||
if self.menu:
|
|
||||||
self.iface.mainWindow().menuBar().removeAction(self.menu.menuAction())
|
|
||||||
self.menu = None
|
|
||||||
|
|
||||||
# Toolbar entfernen
|
|
||||||
if self.toolbar:
|
|
||||||
self.iface.mainWindow().removeToolBar(self.toolbar)
|
|
||||||
self.toolbar = None
|
|
||||||
|
|
||||||
# Actions zurücksetzen
|
|
||||||
self.actions.clear()
|
|
||||||
|
|
||||||
# Gruppe leeren
|
|
||||||
self.plugin_group = None
|
|
||||||
|
|
||||||
def remove_action(self, action):
|
def remove_action(self, action):
|
||||||
"""Entfernt eine einzelne Action aus Menü und Toolbar"""
|
|
||||||
if not action:
|
if not action:
|
||||||
return
|
return
|
||||||
# Menüeintrag entfernen
|
|
||||||
if self.menu:
|
if self.menu:
|
||||||
self.menu.removeAction(action)
|
self.menu.removeAction(action)
|
||||||
# Toolbar-Eintrag entfernen
|
|
||||||
if self.toolbar:
|
if self.toolbar:
|
||||||
self.toolbar.removeAction(action)
|
self.toolbar.removeAction(action)
|
||||||
# Aus der internen Liste löschen
|
|
||||||
self.actions = [(p, a) for p, a in self.actions if a != action]
|
self.actions = [(p, a) for p, a in self.actions if a != action]
|
||||||
|
|
||||||
|
def remove_all(self):
|
||||||
|
if self.menu:
|
||||||
|
remove_menu(self.menu)
|
||||||
|
self.menu = None
|
||||||
|
|
||||||
|
if self.toolbar:
|
||||||
|
remove_toolbar(self.toolbar)
|
||||||
|
self.toolbar = None
|
||||||
|
|
||||||
|
self.actions.clear()
|
||||||
|
self.plugin_group = None
|
||||||
|
|
||||||
|
|||||||
@@ -1,129 +1,87 @@
|
|||||||
# sn_basis/ui/tabs/settings_tab.py
|
"""
|
||||||
|
sn_basis/ui/base_dockwidget.py
|
||||||
|
|
||||||
from sn_basis.functions.qgisqt_wrapper import (
|
Basis-Dockwidget für alle LNO-Module.
|
||||||
QWidget, QGridLayout, QLabel, QLineEdit,
|
"""
|
||||||
QGroupBox, QVBoxLayout, QPushButton,
|
|
||||||
info, warning, error
|
|
||||||
)
|
|
||||||
|
|
||||||
from sn_basis.functions.settings_logic import SettingsLogic
|
from sn_basis.functions.qt_wrapper import QDockWidget, QTabWidget
|
||||||
|
from sn_basis.functions.message_wrapper import warning, error
|
||||||
|
|
||||||
|
|
||||||
class SettingsTab(QWidget):
|
class BaseDockWidget(QDockWidget):
|
||||||
"""
|
"""
|
||||||
Tab für benutzer- und projektspezifische Einstellungen.
|
Basis-Dockwidget für alle LNO-Module.
|
||||||
Nutzt SettingsLogic für das Laden/Speichern und den Wrapper für Meldungen.
|
|
||||||
|
- Titel wird automatisch aus base_title + subtitle erzeugt
|
||||||
|
- Tabs werden dynamisch aus der Klassenvariable 'tabs' erzeugt
|
||||||
|
- Die zugehörige Toolbar-Action wird beim Schließen zurückgesetzt
|
||||||
"""
|
"""
|
||||||
|
|
||||||
tab_title = "Projekteigenschaften"
|
base_title = "LNO Sachsen"
|
||||||
|
tabs = [] # Liste von Tab-Klassen
|
||||||
|
action = None # Referenz auf die Toolbar-Action
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None, subtitle=""):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.logic = SettingsLogic()
|
|
||||||
|
|
||||||
main_layout = QVBoxLayout()
|
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# Definition der Felder
|
# Titel setzen
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
self.user_fields = {
|
|
||||||
"amt": "Amt:",
|
|
||||||
"behoerde": "Behörde:",
|
|
||||||
"landkreis_user": "Landkreis:",
|
|
||||||
"sachgebiet": "Sachgebiet:"
|
|
||||||
}
|
|
||||||
|
|
||||||
self.project_fields = {
|
|
||||||
"bezeichnung": "Bezeichnung:",
|
|
||||||
"verfahrensnummer": "Verfahrensnummer:",
|
|
||||||
"gemeinden": "Gemeinde(n):",
|
|
||||||
"landkreise_proj": "Landkreis(e):"
|
|
||||||
}
|
|
||||||
|
|
||||||
# -----------------------------------------------------
|
|
||||||
# Benutzer-Felder
|
|
||||||
# -----------------------------------------------------
|
|
||||||
user_group = QGroupBox("Benutzerspezifische Festlegungen")
|
|
||||||
user_layout = QGridLayout()
|
|
||||||
self.user_inputs = {}
|
|
||||||
|
|
||||||
for row, (key, label) in enumerate(self.user_fields.items()):
|
|
||||||
line_edit = QLineEdit()
|
|
||||||
self.user_inputs[key] = line_edit
|
|
||||||
user_layout.addWidget(QLabel(label), row, 0)
|
|
||||||
user_layout.addWidget(line_edit, row, 1)
|
|
||||||
|
|
||||||
user_group.setLayout(user_layout)
|
|
||||||
|
|
||||||
# -----------------------------------------------------
|
|
||||||
# Projekt-Felder
|
|
||||||
# -----------------------------------------------------
|
|
||||||
project_group = QGroupBox("Projektspezifische Festlegungen")
|
|
||||||
project_layout = QGridLayout()
|
|
||||||
self.project_inputs = {}
|
|
||||||
|
|
||||||
for row, (key, label) in enumerate(self.project_fields.items()):
|
|
||||||
line_edit = QLineEdit()
|
|
||||||
self.project_inputs[key] = line_edit
|
|
||||||
project_layout.addWidget(QLabel(label), row, 0)
|
|
||||||
project_layout.addWidget(line_edit, row, 1)
|
|
||||||
|
|
||||||
project_group.setLayout(project_layout)
|
|
||||||
|
|
||||||
# -----------------------------------------------------
|
|
||||||
# Speichern-Button
|
|
||||||
# -----------------------------------------------------
|
|
||||||
save_button = QPushButton("Speichern")
|
|
||||||
save_button.clicked.connect(self.save_data)
|
|
||||||
|
|
||||||
# -----------------------------------------------------
|
|
||||||
# Layout zusammenfügen
|
|
||||||
# -----------------------------------------------------
|
|
||||||
main_layout.addWidget(user_group)
|
|
||||||
main_layout.addWidget(project_group)
|
|
||||||
main_layout.addStretch()
|
|
||||||
main_layout.addWidget(save_button)
|
|
||||||
|
|
||||||
self.setLayout(main_layout)
|
|
||||||
|
|
||||||
# Daten laden
|
|
||||||
self.load_data()
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Speichern
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
|
|
||||||
def save_data(self):
|
|
||||||
"""
|
|
||||||
Speichert alle Eingaben über SettingsLogic.
|
|
||||||
Fehler werden über den Wrapper gemeldet.
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
fields = {
|
title = (
|
||||||
key: widget.text()
|
self.base_title
|
||||||
for key, widget in {**self.user_inputs, **self.project_inputs}.items()
|
if not subtitle
|
||||||
}
|
else f"{self.base_title} | {subtitle}"
|
||||||
|
)
|
||||||
|
self.setWindowTitle(title)
|
||||||
|
except Exception as e:
|
||||||
|
warning("Titel konnte nicht gesetzt werden", str(e))
|
||||||
|
|
||||||
self.logic.save(fields)
|
# -----------------------------------------------------
|
||||||
info("Gespeichert", "Die Einstellungen wurden erfolgreich gespeichert.")
|
# Tabs erzeugen
|
||||||
|
# -----------------------------------------------------
|
||||||
|
try:
|
||||||
|
tab_widget = QTabWidget()
|
||||||
|
|
||||||
|
for tab_class in self.tabs:
|
||||||
|
try:
|
||||||
|
tab_instance = tab_class()
|
||||||
|
tab_title = getattr(
|
||||||
|
tab_class,
|
||||||
|
"tab_title",
|
||||||
|
tab_class.__name__,
|
||||||
|
)
|
||||||
|
tab_widget.addTab(tab_instance, tab_title)
|
||||||
|
except Exception as e:
|
||||||
|
error(
|
||||||
|
"Tab konnte nicht geladen werden",
|
||||||
|
f"{tab_class}: {e}",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.setWidget(tab_widget)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error("Fehler beim Speichern", str(e))
|
error(
|
||||||
|
"Tab-Widget konnte nicht initialisiert werden",
|
||||||
|
str(e),
|
||||||
|
)
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# Laden
|
# Dock schließen
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
def load_data(self):
|
def closeEvent(self, event):
|
||||||
"""
|
"""
|
||||||
Lädt gespeicherte Einstellungen und füllt die Felder.
|
Wird aufgerufen, wenn das Dock geschlossen wird.
|
||||||
Fehler werden über den Wrapper gemeldet.
|
Setzt die zugehörige Toolbar-Action zurück.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
data = self.logic.load()
|
if self.action:
|
||||||
|
self.action.setChecked(False)
|
||||||
for key, widget in {**self.user_inputs, **self.project_inputs}.items():
|
|
||||||
widget.setText(data.get(key, ""))
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
warning("Einstellungen konnten nicht geladen werden", str(e))
|
warning(
|
||||||
|
"Toolbar-Status konnte nicht zurückgesetzt werden",
|
||||||
|
str(e),
|
||||||
|
)
|
||||||
|
|
||||||
|
super().closeEvent(event)
|
||||||
|
|||||||
Reference in New Issue
Block a user