diff --git a/functions/__init__.py b/functions/__init__.py index e69de29..cfbfc4a 100644 --- a/functions/__init__.py +++ b/functions/__init__.py @@ -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) diff --git a/functions/dialog_wrapper.py b/functions/dialog_wrapper.py new file mode 100644 index 0000000..43e7654 --- /dev/null +++ b/functions/dialog_wrapper.py @@ -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 diff --git a/functions/ly_existence_wrapper.py b/functions/ly_existence_wrapper.py new file mode 100644 index 0000000..d39e000 --- /dev/null +++ b/functions/ly_existence_wrapper.py @@ -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 diff --git a/functions/ly_geometry_wrapper.py b/functions/ly_geometry_wrapper.py new file mode 100644 index 0000000..97c2b6a --- /dev/null +++ b/functions/ly_geometry_wrapper.py @@ -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 + diff --git a/functions/ly_metadata_wrapper.py b/functions/ly_metadata_wrapper.py new file mode 100644 index 0000000..48c4d19 --- /dev/null +++ b/functions/ly_metadata_wrapper.py @@ -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 diff --git a/functions/ly_style_wrapper.py b/functions/ly_style_wrapper.py new file mode 100644 index 0000000..71d48e6 --- /dev/null +++ b/functions/ly_style_wrapper.py @@ -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 diff --git a/functions/ly_visibility_wrapper.py b/functions/ly_visibility_wrapper.py new file mode 100644 index 0000000..b37a4ce --- /dev/null +++ b/functions/ly_visibility_wrapper.py @@ -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 diff --git a/functions/message_wrapper.py b/functions/message_wrapper.py new file mode 100644 index 0000000..c6f75f3 --- /dev/null +++ b/functions/message_wrapper.py @@ -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) diff --git a/functions/os_wrapper.py b/functions/os_wrapper.py new file mode 100644 index 0000000..6bd6d10 --- /dev/null +++ b/functions/os_wrapper.py @@ -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 diff --git a/functions/qgiscore_wrapper.py b/functions/qgiscore_wrapper.py new file mode 100644 index 0000000..77e8ae3 --- /dev/null +++ b/functions/qgiscore_wrapper.py @@ -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 diff --git a/functions/qgisqt_wrapper.py b/functions/qgisqt_wrapper.py deleted file mode 100644 index a8cd723..0000000 --- a/functions/qgisqt_wrapper.py +++ /dev/null @@ -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 diff --git a/functions/qgisui_wrapper.py b/functions/qgisui_wrapper.py new file mode 100644 index 0000000..56e5775 --- /dev/null +++ b/functions/qgisui_wrapper.py @@ -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 diff --git a/functions/qt_wrapper.py b/functions/qt_wrapper.py new file mode 100644 index 0000000..08de053 --- /dev/null +++ b/functions/qt_wrapper.py @@ -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 + diff --git a/functions/settings_logic.py b/functions/settings_logic.py index 77d049c..73543ce 100644 --- a/functions/settings_logic.py +++ b/functions/settings_logic.py @@ -1,9 +1,9 @@ """ -sn_basis/funktions/settings_logic.py – Logik zum Lesen und Schreiben der Plugin-Einstellungen -über den zentralen qgisqt_wrapper. +sn_basis/functions/settings_logic.py – Logik zum Lesen und Schreiben der Plugin-Einstellungen +über den zentralen variable_wrapper. """ -from sn_basis.functions.qgisqt_wrapper import ( +from sn_basis.functions.variable_wrapper import ( get_variable, set_variable, ) @@ -27,17 +27,17 @@ class SettingsLogic: "landkreise_proj", ] - def load(self) -> dict: + def load(self) -> dict[str, str]: """ Lädt alle Variablen aus dem Projekt. Rückgabe: dict mit allen Werten (leere Strings, wenn nicht gesetzt). """ - daten = {} + daten: dict[str, str] = {} for key in self.VARIABLEN: daten[key] = get_variable(key, scope="project") return daten - def save(self, daten: dict): + def save(self, daten: dict[str, str]) -> None: """ Speichert alle übergebenen Variablen im Projekt. daten: dict mit key → value diff --git a/functions/sys_wrapper.py b/functions/sys_wrapper.py new file mode 100644 index 0000000..75b1899 --- /dev/null +++ b/functions/sys_wrapper.py @@ -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 diff --git a/functions/syswrapper.py b/functions/syswrapper.py deleted file mode 100644 index 2ab5a6b..0000000 --- a/functions/syswrapper.py +++ /dev/null @@ -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 - diff --git a/functions/variable_wrapper.py b/functions/variable_wrapper.py new file mode 100644 index 0000000..416e864 --- /dev/null +++ b/functions/variable_wrapper.py @@ -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.") diff --git a/main.py b/main.py index d71c114..414820f 100644 --- a/main.py +++ b/main.py @@ -1,13 +1,20 @@ # sn_basis/main.py -from sn_basis.functions.qgisqt_wrapper import QCoreApplication, getattr_safe 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 class BasisPlugin: + """ + Einstiegspunkt des sn_basis-Plugins. + Orchestriert UI und Fachmodule – keine UI-Logik. + """ + def __init__(self, iface): - self.iface = iface + # iface wird von QGIS übergeben, aber nicht direkt verwendet self.ui = None # QCoreApplication kann im Mock-Modus None sein @@ -21,10 +28,12 @@ class BasisPlugin: connect(self.unload) def initGui(self): - # Basis-Navigation neu aufbauen - self.ui = Navigation(self.iface) - - # Alle Fachplugins mit "sn_" prüfen und neu initialisieren + """ + Initialisiert die Basis-Navigation und triggert initGui + aller abhängigen sn_-Plugins. + """ + self.ui = Navigation() + self.ui.init_ui() for name, plugin in plugins.items(): if name.startswith("sn_") and name != "sn_basis": try: @@ -33,10 +42,12 @@ class BasisPlugin: init_gui() except Exception as e: print(f"Fehler beim Neuinitialisieren von {name}: {e}") + self.ui.finalize_menu_and_toolbar() def unload(self): + """ + Räumt UI-Komponenten sauber auf. + """ if self.ui: - remove_all = getattr_safe(self.ui, "remove_all") - if callable(remove_all): - remove_all() + self.ui.remove_all() self.ui = None diff --git a/modules/Dateipruefer.py b/modules/Dateipruefer.py index a2fd17e..5564654 100644 --- a/modules/Dateipruefer.py +++ b/modules/Dateipruefer.py @@ -1,12 +1,13 @@ """ -sn_basis/modulesdateipruefer.py – Prüfung von Dateieingaben für das Plugin. -Verwendet syswrapper und gibt pruef_ergebnis an den Pruefmanager zurück. +sn_basis/modules/Dateipruefer.py – Prüfung von Dateieingaben für das Plugin. +Verwendet sys_wrapper und gibt pruef_ergebnis an den Pruefmanager zurück. """ -from sn_basis.functions.syswrapper import ( - file_exists, - is_file, +from pathlib import Path + +from sn_basis.functions import ( join_path, + file_exists, ) from sn_basis.modules.Pruefmanager import pruef_ergebnis @@ -32,13 +33,14 @@ class Dateipruefer: self.standarddatei = standarddatei self.temporaer_erlaubt = temporaer_erlaubt - # --------------------------------------------------------- # Hilfsfunktion # --------------------------------------------------------- - def _pfad(self, relativer_pfad: str) -> str: - """Erzeugt einen OS‑unabhängigen Pfad relativ zum Basisverzeichnis.""" + def _pfad(self, relativer_pfad: str) -> Path: + """ + Erzeugt einen OS‑unabhängigen Pfad relativ zum Basisverzeichnis. + """ return join_path(self.basis_pfad, relativer_pfad) # --------------------------------------------------------- @@ -62,12 +64,12 @@ class Dateipruefer: # ----------------------------------------------------- pfad = self._pfad(self.pfad) - if not file_exists(pfad) or not is_file(pfad): + if not file_exists(pfad): return pruef_ergebnis( ok=False, meldung=f"Die Datei '{self.pfad}' wurde nicht gefunden.", aktion="datei_nicht_gefunden", - pfad=pfad, + kontext=pfad, ) # ----------------------------------------------------- @@ -77,7 +79,7 @@ class Dateipruefer: ok=True, meldung="Datei gefunden.", aktion="ok", - pfad=pfad, + kontext=pfad, ) # --------------------------------------------------------- @@ -96,25 +98,31 @@ class Dateipruefer: ok=False, meldung="Das Dateifeld ist leer. Soll ohne Datei fortgefahren werden?", aktion="leereingabe_erlaubt", - pfad=None, + kontext=None, ) # 2. Standarddatei verfügbar → Nutzer fragen, ob sie verwendet werden soll if self.standarddatei: return pruef_ergebnis( 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", - pfad=self._pfad(self.standarddatei), + kontext=self._pfad(self.standarddatei), ) # 3. Temporäre Datei erlaubt → Nutzer fragen, ob temporär gearbeitet werden soll if self.temporaer_erlaubt: return pruef_ergebnis( 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", - pfad=None, + kontext=None, ) # 4. Leereingabe nicht erlaubt → Fehler @@ -122,5 +130,5 @@ class Dateipruefer: ok=False, meldung="Es wurde keine Datei angegeben.", aktion="leereingabe_nicht_erlaubt", - pfad=None, + kontext=None, ) diff --git a/modules/Pruefmanager.py b/modules/Pruefmanager.py index 5eea743..4a0a85c 100644 --- a/modules/Pruefmanager.py +++ b/modules/Pruefmanager.py @@ -1,14 +1,14 @@ """ -sn_basis/modules/pruefmanager.py – zentrale Verarbeitung von pruef_ergebnis-Objekten. -Steuert die Nutzerinteraktion über qgisqt_wrapper. +sn_basis/modules/Pruefmanager.py – zentrale Verarbeitung von pruef_ergebnis-Objekten. +Steuert die Nutzerinteraktion über Wrapper. """ -from sn_basis.functions.qgisqt_wrapper import ( +from sn_basis.functions import ( ask_yes_no, info, warning, error, - set_layer_visible, # optional, falls implementiert + set_layer_visible, ) from sn_basis.modules.pruef_ergebnis import pruef_ergebnis @@ -36,6 +36,7 @@ class Pruefmanager: return ergebnis aktion = ergebnis.aktion + kontext = ergebnis.kontext # ----------------------------------------------------- # Allgemeine Aktionen @@ -47,7 +48,12 @@ class Pruefmanager: if aktion == "leereingabe_erlaubt": 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 if aktion == "leereingabe_nicht_erlaubt": @@ -56,12 +62,22 @@ class Pruefmanager: if aktion == "standarddatei_vorschlagen": 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 if aktion == "temporaer_erlaubt": 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 if aktion == "datei_nicht_gefunden": @@ -94,12 +110,18 @@ class Pruefmanager: if aktion == "layer_unsichtbar": if ask_yes_no("Layer einblenden", ergebnis.meldung): - # Falls set_layer_visible implementiert ist - try: - set_layer_visible(ergebnis.pfad, True) - except Exception: - pass - return pruef_ergebnis(True, "Layer wurde eingeblendet.", "ok", ergebnis.pfad) + if kontext is not None: + try: + set_layer_visible(kontext, True) + except Exception: + pass + + return pruef_ergebnis( + ok=True, + meldung="Layer wurde eingeblendet.", + aktion="ok", + kontext=kontext, + ) return ergebnis if aktion == "falscher_geotyp": diff --git a/modules/layerpruefer.py b/modules/layerpruefer.py index b0d5c56..164f9cf 100644 --- a/modules/layerpruefer.py +++ b/modules/layerpruefer.py @@ -1,9 +1,9 @@ """ 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, get_layer_geometry_type, get_layer_feature_count, @@ -15,7 +15,7 @@ from sn_basis.functions.qgisqt_wrapper import ( is_layer_editable, ) -from sn_basis.modules.pruef_ergebnis import pruef_ergebnis +from sn_basis.modules.pruef_ergebnis import pruef_ergebnis, PruefAktion class Layerpruefer: @@ -57,8 +57,8 @@ class Layerpruefer: return pruef_ergebnis( ok=False, meldung="Der Layer existiert nicht oder wurde nicht geladen.", - aktion="layer_nicht_gefunden", # type: ignore - pfad=None, + aktion="layer_nicht_gefunden", + kontext=None, ) # ----------------------------------------------------- @@ -69,8 +69,8 @@ class Layerpruefer: return pruef_ergebnis( ok=False, meldung="Der Layer ist unsichtbar. Soll er eingeblendet werden?", - aktion="layer_unsichtbar", # type: ignore - pfad=self.layer, # Layerobjekt wird übergeben + aktion="layer_unsichtbar", + kontext=self.layer, # Layerobjekt als Kontext ) # ----------------------------------------------------- @@ -80,9 +80,12 @@ class Layerpruefer: if self.erwarteter_layertyp and layertyp != self.erwarteter_layertyp: return pruef_ergebnis( 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", - pfad=None, + kontext=None, ) # ----------------------------------------------------- @@ -92,9 +95,12 @@ class Layerpruefer: if self.erwarteter_geotyp and geotyp != self.erwarteter_geotyp: return pruef_ergebnis( 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", - pfad=None, + kontext=None, ) # ----------------------------------------------------- @@ -106,7 +112,7 @@ class Layerpruefer: ok=False, meldung="Der Layer enthält keine Objekte.", aktion="layer_leer", - pfad=None, + kontext=None, ) # ----------------------------------------------------- @@ -116,9 +122,12 @@ class Layerpruefer: if self.erwartetes_crs and crs != self.erwartetes_crs: return pruef_ergebnis( 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", - pfad=None, + kontext=None, ) # ----------------------------------------------------- @@ -130,9 +139,12 @@ class Layerpruefer: if fehlende: return pruef_ergebnis( 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", - pfad=None, + kontext=None, ) # ----------------------------------------------------- @@ -144,7 +156,7 @@ class Layerpruefer: ok=False, meldung=f"Die Datenquelle '{quelle}' ist nicht erlaubt.", aktion="datenquelle_unerwartet", - pfad=None, + kontext=None, ) # ----------------------------------------------------- @@ -156,7 +168,7 @@ class Layerpruefer: ok=False, meldung="Der Layer ist nicht editierbar.", aktion="layer_nicht_editierbar", - pfad=None, + kontext=None, ) # ----------------------------------------------------- @@ -166,5 +178,5 @@ class Layerpruefer: ok=True, meldung="Layerprüfung erfolgreich.", aktion="ok", - pfad=None, + kontext=None, ) diff --git a/modules/linkpruefer.py b/modules/linkpruefer.py index 0b15889..6f59306 100644 --- a/modules/linkpruefer.py +++ b/modules/linkpruefer.py @@ -1,20 +1,17 @@ """ sn_basis/modules/linkpruefer.py – Prüfung von URLs und lokalen Links. -Verwendet syswrapper und qgisqt_wrapper. -Gibt pruef_ergebnis an den Pruefmanager zurück. +Verwendet Wrapper und gibt pruef_ergebnis an den Pruefmanager zurück. """ -from sn_basis.functions.syswrapper import ( - file_exists, - is_file, - join_path, -) +from pathlib import Path -from sn_basis.functions.qgisqt_wrapper import ( +from sn_basis.functions import ( + file_exists, + join_path, network_head, ) -from sn_basis.modules.Pruefmanager import pruef_ergebnis +from sn_basis.modules.pruef_ergebnis import pruef_ergebnis, PruefAktion class Linkpruefer: @@ -33,14 +30,18 @@ class Linkpruefer: # Hilfsfunktionen # --------------------------------------------------------- - def _pfad(self, relativer_pfad: str) -> str: - """Erzeugt einen OS-unabhängigen Pfad relativ zum Basisverzeichnis.""" + def _pfad(self, relativer_pfad: str) -> Path: + """ + Erzeugt einen OS‑unabhängigen Pfad relativ zum Basisverzeichnis. + """ if not self.basis: - return relativer_pfad + return Path(relativer_pfad) return join_path(self.basis, relativer_pfad) def _ist_url(self, text: str) -> bool: - """Einfache URL-Erkennung.""" + """ + Einfache URL-Erkennung. + """ return text.startswith("http://") or text.startswith("https://") # --------------------------------------------------------- @@ -58,7 +59,7 @@ class Linkpruefer: ok=False, meldung="Es wurde kein Link angegeben.", aktion="leer", - pfad=None, + kontext=None, ) # ----------------------------------------------------- @@ -88,7 +89,7 @@ class Linkpruefer: ok=False, meldung=f"Die URL '{url}' konnte nicht geprüft werden.", aktion="netzwerkfehler", - pfad=url, + kontext=url, ) if reply.error != 0: @@ -96,14 +97,14 @@ class Linkpruefer: ok=False, meldung=f"Die URL '{url}' ist nicht erreichbar.", aktion="url_nicht_erreichbar", - pfad=url, + kontext=url, ) return pruef_ergebnis( ok=True, meldung="URL ist erreichbar.", aktion="ok", - pfad=url, + kontext=url, ) # --------------------------------------------------------- @@ -122,20 +123,12 @@ class Linkpruefer: ok=False, meldung=f"Der Pfad '{eingabe}' wurde nicht gefunden.", aktion="pfad_nicht_gefunden", - pfad=pfad, - ) - - if not is_file(pfad): - return pruef_ergebnis( - ok=False, - meldung=f"Der Pfad '{eingabe}' ist keine Datei.", - aktion="kein_dateipfad", - pfad=pfad, + kontext=pfad, ) return pruef_ergebnis( ok=True, meldung="Dateipfad ist gültig.", aktion="ok", - pfad=pfad, + kontext=pfad, ) diff --git a/modules/pruef_ergebnis.py b/modules/pruef_ergebnis.py index e15d8ad..084f314 100644 --- a/modules/pruef_ergebnis.py +++ b/modules/pruef_ergebnis.py @@ -1,10 +1,10 @@ """ sn_basis/modules/pruef_ergebnis.py – Ergebnisobjekt für alle Prüfer. - """ 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. @@ -31,28 +31,19 @@ PruefAktion = Literal[ "temporaer_erzeugen", "stil_nicht_anwendbar", "layer_unsichtbar", + "layer_nicht_gefunden", "unbekannt", + "stil_anwendbar", + "falsche_endung", ] -@dataclass + +@dataclass(slots=True) 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 meldung: str aktion: PruefAktion - pfad: Optional[str] = None + kontext: Optional[Any] = None + + diff --git a/modules/stilpruefer.py b/modules/stilpruefer.py index 43734f2..db9312a 100644 --- a/modules/stilpruefer.py +++ b/modules/stilpruefer.py @@ -1,59 +1,75 @@ """ -sn_basis/modules/stilpruefer.py – Prüfung und Anwendung von Layerstilen. -Verwendet ausschließlich qgisqt_wrapper und gibt pruef_ergebnis zurück. +sn_basis/modules/stilpruefer.py – Prüfung von Layerstilen. +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 ( - apply_style, -) +from pathlib import Path +from sn_basis.functions import file_exists from sn_basis.modules.pruef_ergebnis import pruef_ergebnis class Stilpruefer: """ - Prüft, ob ein Stil auf einen Layer angewendet werden kann. - Die eigentliche Nutzerinteraktion übernimmt der Pruefmanager. + Prüft, ob ein Stilpfad gültig ist und angewendet werden kann. + Keine Seiteneffekte, keine QGIS-Aufrufe. """ - def __init__(self, layer, stil_pfad: str): - """ - layer: QGIS-Layer oder Mock-Layer - stil_pfad: relativer oder absoluter Pfad zum .qml-Stil - """ - self.layer = layer - self.stil_pfad = stil_pfad + def __init__(self): + pass # --------------------------------------------------------- # 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 """ - # Wrapper übernimmt: - # - Pfadberechnung - # - Existenzprüfung - # - loadNamedStyle - # - Fehlerbehandlung - # - Mock-Modus - erfolg, meldung = apply_style(self.layer, self.stil_pfad) - - if erfolg: + # ----------------------------------------------------- + # 1. Kein Stil angegeben → OK + # ----------------------------------------------------- + if not stil_pfad: return pruef_ergebnis( ok=True, - meldung=f"Stil erfolgreich angewendet: {self.stil_pfad}", + meldung="Kein Stil angegeben.", 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( + ok=False, + meldung=f"Die Stil-Datei '{stil_pfad}' wurde nicht gefunden.", + aktion="datei_nicht_gefunden", + 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=False, - meldung=meldung, - aktion="stil_nicht_anwendbar", - pfad=self.stil_pfad, + ok=True, + meldung="Stil-Datei ist gültig.", + aktion="stil_anwendbar", + kontext=pfad, ) diff --git a/test/run_tests.py b/test/run_tests.py index 7c2b03a..7518bd5 100644 --- a/test/run_tests.py +++ b/test/run_tests.py @@ -11,18 +11,22 @@ import inspect import os 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 -TEST_DIR = os.path.dirname(__file__) -PLUGIN_ROOT = os.path.abspath(os.path.join(TEST_DIR, "..", "..")) +if str(PLUGIN_ROOT) not in sys.path: + sys.path.insert(0, str(PLUGIN_ROOT)) -if PLUGIN_ROOT not in sys.path: - sys.path.insert(0, PLUGIN_ROOT) - - -from sn_basis.functions import syswrapper +from sn_basis.functions import ( + get_plugin_root, + add_to_sys_path, +) # --------------------------------------------------------- # Bootstrap: Plugin-Root in sys.path eintragen @@ -33,13 +37,12 @@ def bootstrap(): Simuliert das QGIS-Plugin-Startverhalten: stellt sicher, dass sn_basis importierbar ist. """ - plugin_root = syswrapper.get_plugin_root() - syswrapper.add_to_sys_path(plugin_root) + plugin_root = get_plugin_root() + add_to_sys_path(plugin_root) bootstrap() - # --------------------------------------------------------- # Farben # --------------------------------------------------------- @@ -53,12 +56,14 @@ RESET = "\033[0m" GLOBAL_TEST_COUNTER = 0 - # --------------------------------------------------------- # Farbige TestResult-Klasse # --------------------------------------------------------- class ColoredTestResult(unittest.TextTestResult): + + _last_test_class: type | None = None + def startTest(self, test): global GLOBAL_TEST_COUNTER @@ -93,16 +98,19 @@ class ColoredTestResult(unittest.TextTestResult): super().addSuccess(test) self.stream.write(f"{GREEN}OK{RESET}\n") - # --------------------------------------------------------- # Farbiger TestRunner # --------------------------------------------------------- class ColoredTestRunner(unittest.TextTestRunner): - resultclass = ColoredTestResult def _makeResult(self): - result = super()._makeResult() + result = ColoredTestResult( + self.stream, + self.descriptions, + self.verbosity, + ) + original_start_test = result.startTest def patched_start_test(test): @@ -127,7 +135,7 @@ def main(): f"{datetime.datetime.now():%Y-%m-%d %H:%M:%S}{RESET}" ) print("=" * 70 + "\n") - + loader = unittest.TestLoader() suite = loader.discover( diff --git a/test/test_bootstrap.py b/test/test_bootstrap.py index 10db7cd..f87d84d 100644 --- a/test/test_bootstrap.py +++ b/test/test_bootstrap.py @@ -1,2 +1,2 @@ -from sn_basis.functions import syswrapper -syswrapper.add_to_sys_path(syswrapper.get_plugin_root()) +from sn_basis.functions import sys_wrapper +sys_wrapper.add_to_sys_path(sys_wrapper.get_plugin_root()) diff --git a/test/test_dateipruefer.py b/test/test_dateipruefer.py index d61ac70..84cd127 100644 --- a/test/test_dateipruefer.py +++ b/test/test_dateipruefer.py @@ -1,12 +1,10 @@ # sn_basis/test/test_dateipruefer.py import unittest +from pathlib import Path from unittest.mock import patch from sn_basis.modules.Dateipruefer import Dateipruefer -from sn_basis.modules.pruef_ergebnis import pruef_ergebnis - - class TestDateipruefer(unittest.TestCase): @@ -24,6 +22,7 @@ class TestDateipruefer(unittest.TestCase): self.assertFalse(result.ok) self.assertEqual(result.aktion, "leereingabe_erlaubt") + self.assertIsNone(result.kontext) # ----------------------------------------------------- # 2. Leere Eingabe nicht erlaubt @@ -38,6 +37,7 @@ class TestDateipruefer(unittest.TestCase): self.assertFalse(result.ok) self.assertEqual(result.aktion, "leereingabe_nicht_erlaubt") + self.assertIsNone(result.kontext) # ----------------------------------------------------- # 3. Standarddatei vorschlagen @@ -52,7 +52,7 @@ class TestDateipruefer(unittest.TestCase): self.assertFalse(result.ok) 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 @@ -67,11 +67,12 @@ class TestDateipruefer(unittest.TestCase): self.assertFalse(result.ok) self.assertEqual(result.aktion, "temporaer_erlaubt") + self.assertIsNone(result.kontext) # ----------------------------------------------------- # 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): pruefer = Dateipruefer( pfad="/tmp/nichtvorhanden.txt" @@ -81,13 +82,13 @@ class TestDateipruefer(unittest.TestCase): self.assertFalse(result.ok) self.assertEqual(result.aktion, "datei_nicht_gefunden") + self.assertEqual(result.kontext, Path("/tmp/nichtvorhanden.txt")) # ----------------------------------------------------- # 6. Datei existiert # ----------------------------------------------------- - @patch("sn_basis.functions.syswrapper.file_exists", return_value=True) - @patch("sn_basis.functions.syswrapper.is_file", return_value=True) - def test_datei_ok(self, mock_isfile, mock_exists): + @patch("sn_basis.modules.Dateipruefer.file_exists", return_value=True) + def test_datei_ok(self, mock_exists): pruefer = Dateipruefer( pfad="/tmp/test.txt" ) @@ -96,7 +97,7 @@ class TestDateipruefer(unittest.TestCase): self.assertTrue(result.ok) self.assertEqual(result.aktion, "ok") - self.assertEqual(result.pfad, "/tmp/test.txt") + self.assertEqual(result.kontext, Path("/tmp/test.txt")) if __name__ == "__main__": diff --git a/test/test_layerpruefer.py b/test/test_layerpruefer.py index 46bde78..9bff1ad 100644 --- a/test/test_layerpruefer.py +++ b/test/test_layerpruefer.py @@ -78,18 +78,19 @@ def mock_is_layer_editable(layer): class TestLayerpruefer(unittest.TestCase): def setUp(self): - # Monkeypatching der Wrapper-Funktionen - import sn_basis.functions.qgisqt_wrapper as wrapper + # Monkeypatching der im Layerpruefer verwendeten Wrapper-Funktionen + 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 diff --git a/test/test_linkpruefer.py b/test/test_linkpruefer.py index 89ea0e3..07c0fff 100644 --- a/test/test_linkpruefer.py +++ b/test/test_linkpruefer.py @@ -1,107 +1,78 @@ # sn_basis/test/test_linkpruefer.py import unittest +from pathlib import Path from unittest.mock import patch 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): # ----------------------------------------------------- # 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): - mock_head.return_value = MockResponseOK() + mock_head.return_value = NetworkReply(error=0) - lp = Linkpruefer("http://example.com", "REST") - result = lp.pruefe() + lp = Linkpruefer() + result = lp.pruefe("http://example.com") self.assertTrue(result.ok) self.assertEqual(result.aktion, "ok") + self.assertEqual(result.kontext, "http://example.com") # ----------------------------------------------------- # 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): - mock_head.return_value = MockResponseConnectionError() + mock_head.return_value = NetworkReply(error=1) - lp = Linkpruefer("http://example.com", "REST") - result = lp.pruefe() + lp = Linkpruefer() + result = lp.pruefe("http://example.com") self.assertFalse(result.ok) 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") - def test_remote_link_404(self, mock_head): - mock_head.return_value = MockResponseNotFound() - - lp = Linkpruefer("http://example.com/missing", "REST") - result = lp.pruefe() + @patch("sn_basis.modules.linkpruefer.network_head", return_value=None) + def test_remote_link_network_error(self, mock_head): + lp = Linkpruefer() + result = lp.pruefe("http://example.com") self.assertFalse(result.ok) - self.assertEqual(result.aktion, "url_nicht_erreichbar") - self.assertIn("404", result.meldung) + self.assertEqual(result.aktion, "netzwerkfehler") + self.assertEqual(result.kontext, "http://example.com") # ----------------------------------------------------- # 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): - mock_exists.return_value = False - - lp = Linkpruefer("/path/to/missing/file.shp", "OGR") - result = lp.pruefe() + lp = Linkpruefer() + result = lp.pruefe("/path/to/missing/file.shp") self.assertFalse(result.ok) 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") - def test_local_link_warning(self, mock_exists): - mock_exists.return_value = True - - lp = Linkpruefer("/path/to/file_without_extension", "OGR") - result = lp.pruefe() + @patch("sn_basis.modules.linkpruefer.file_exists", return_value=True) + def test_local_link_ok(self, mock_exists): + lp = Linkpruefer() + result = lp.pruefe("/path/to/file.shp") self.assertTrue(result.ok) self.assertEqual(result.aktion, "ok") - self.assertIn("ungewöhnlich", result.meldung) + self.assertEqual(result.kontext, Path("/path/to/file.shp")) if __name__ == "__main__": diff --git a/test/test_pruefmanager.py b/test/test_pruefmanager.py index 3a0cfe6..ef8d95b 100644 --- a/test/test_pruefmanager.py +++ b/test/test_pruefmanager.py @@ -25,7 +25,7 @@ class TestPruefmanager(unittest.TestCase): # ----------------------------------------------------- # 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): ergebnis = pruef_ergebnis(False, "Leer?", "leereingabe_erlaubt", None) entscheidung = self.manager.verarbeite(ergebnis) @@ -36,7 +36,7 @@ class TestPruefmanager(unittest.TestCase): # ----------------------------------------------------- # 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): ergebnis = pruef_ergebnis(False, "Leer?", "leereingabe_erlaubt", None) entscheidung = self.manager.verarbeite(ergebnis) @@ -47,21 +47,33 @@ class TestPruefmanager(unittest.TestCase): # ----------------------------------------------------- # 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): - 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) self.assertTrue(entscheidung.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 # ----------------------------------------------------- - @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): - 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) self.assertFalse(entscheidung.ok) @@ -70,7 +82,7 @@ class TestPruefmanager(unittest.TestCase): # ----------------------------------------------------- # 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): ergebnis = pruef_ergebnis(False, "Temporär?", "temporaer_erlaubt", None) entscheidung = self.manager.verarbeite(ergebnis) @@ -81,7 +93,7 @@ class TestPruefmanager(unittest.TestCase): # ----------------------------------------------------- # 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): ergebnis = pruef_ergebnis(False, "Temporär?", "temporaer_erlaubt", None) entscheidung = self.manager.verarbeite(ergebnis) @@ -92,8 +104,8 @@ class TestPruefmanager(unittest.TestCase): # ----------------------------------------------------- # 8. Layer unsichtbar → Nutzer sagt JA # ----------------------------------------------------- - @patch("sn_basis.functions.qgisqt_wrapper.ask_yes_no", return_value=True) - @patch("sn_basis.functions.qgisqt_wrapper.set_layer_visible") + @patch("sn_basis.modules.Pruefmanager.ask_yes_no", return_value=True) + @patch("sn_basis.modules.Pruefmanager.set_layer_visible") def test_layer_unsichtbar_ja(self, mock_set, mock_ask): fake_layer = object() ergebnis = pruef_ergebnis(False, "Layer unsichtbar", "layer_unsichtbar", fake_layer) @@ -107,7 +119,7 @@ class TestPruefmanager(unittest.TestCase): # ----------------------------------------------------- # 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): fake_layer = object() ergebnis = pruef_ergebnis(False, "Layer unsichtbar", "layer_unsichtbar", fake_layer) @@ -120,7 +132,7 @@ class TestPruefmanager(unittest.TestCase): # ----------------------------------------------------- # 10. Fehlerhafte Aktion → Fallback # ----------------------------------------------------- - @patch("sn_basis.functions.qgisqt_wrapper.warning") + @patch("sn_basis.modules.Pruefmanager.warning") def test_unbekannte_aktion(self, mock_warn): ergebnis = pruef_ergebnis(False, "???", "unbekannt", None) entscheidung = self.manager.verarbeite(ergebnis) diff --git a/test/test_settings_logic.py b/test/test_settings_logic.py index b360bb1..6296e9f 100644 --- a/test/test_settings_logic.py +++ b/test/test_settings_logic.py @@ -11,7 +11,7 @@ class TestSettingsLogic(unittest.TestCase): # ----------------------------------------------------- # 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): # Mock-Rückgabe für jede Variable 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 # ----------------------------------------------------- - @patch("sn_basis.functions.qgisqt_wrapper.set_variable") + @patch("sn_basis.functions.settings_logic.set_variable") def test_save(self, mock_set): logic = SettingsLogic() diff --git a/test/test_stilpruefer.py b/test/test_stilpruefer.py index 28ec3e8..06c2fca 100644 --- a/test/test_stilpruefer.py +++ b/test/test_stilpruefer.py @@ -3,10 +3,10 @@ import unittest import tempfile import os +from pathlib import Path from unittest.mock import patch from sn_basis.modules.stilpruefer import Stilpruefer -from sn_basis.modules.pruef_ergebnis import pruef_ergebnis class TestStilpruefer(unittest.TestCase): @@ -23,13 +23,13 @@ class TestStilpruefer(unittest.TestCase): self.assertTrue(result.ok) self.assertEqual(result.aktion, "ok") self.assertIn("Kein Stil angegeben", result.meldung) + self.assertIsNone(result.kontext) # ----------------------------------------------------- # 2. Datei existiert und ist .qml # ----------------------------------------------------- - @patch("sn_basis.functions.syswrapper.file_exists", return_value=True) - @patch("sn_basis.functions.syswrapper.is_file", return_value=True) - def test_datei_existiert_mit_qml(self, mock_isfile, mock_exists): + @patch("sn_basis.modules.stilpruefer.file_exists", return_value=True) + def test_datei_existiert_mit_qml(self, mock_exists): with tempfile.NamedTemporaryFile(suffix=".qml", delete=False) as tmp: tmp_path = tmp.name @@ -37,8 +37,8 @@ class TestStilpruefer(unittest.TestCase): result = self.pruefer.pruefe(tmp_path) self.assertTrue(result.ok) - self.assertEqual(result.aktion, "ok") - self.assertEqual(result.pfad, tmp_path) + self.assertEqual(result.aktion, "stil_anwendbar") + self.assertEqual(result.kontext, Path(tmp_path)) finally: os.remove(tmp_path) @@ -46,9 +46,8 @@ class TestStilpruefer(unittest.TestCase): # ----------------------------------------------------- # 3. Datei existiert, aber falsche Endung # ----------------------------------------------------- - @patch("sn_basis.functions.syswrapper.file_exists", return_value=True) - @patch("sn_basis.functions.syswrapper.is_file", return_value=True) - def test_datei_existiert_falsche_endung(self, mock_isfile, mock_exists): + @patch("sn_basis.modules.stilpruefer.file_exists", return_value=True) + def test_datei_existiert_falsche_endung(self, mock_exists): with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as tmp: tmp_path = tmp.name @@ -58,6 +57,7 @@ class TestStilpruefer(unittest.TestCase): self.assertFalse(result.ok) self.assertEqual(result.aktion, "falsche_endung") self.assertIn(".qml", result.meldung) + self.assertEqual(result.kontext, Path(tmp_path)) finally: os.remove(tmp_path) @@ -65,7 +65,7 @@ class TestStilpruefer(unittest.TestCase): # ----------------------------------------------------- # 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): fake_path = "/tmp/nichtvorhanden.qml" @@ -74,6 +74,7 @@ class TestStilpruefer(unittest.TestCase): self.assertFalse(result.ok) self.assertEqual(result.aktion, "datei_nicht_gefunden") self.assertIn("nicht gefunden", result.meldung) + self.assertEqual(result.kontext, Path(fake_path)) if __name__ == "__main__": diff --git a/test/test_wrapper.py b/test/test_wrapper.py deleted file mode 100644 index f57b5bb..0000000 --- a/test/test_wrapper.py +++ /dev/null @@ -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() diff --git a/ui/base_dockwidget.py b/ui/base_dockwidget.py index 9ee75af..e0ce1f6 100644 --- a/ui/base_dockwidget.py +++ b/ui/base_dockwidget.py @@ -1,12 +1,17 @@ -# sn_basis/ui/base_dockwidget.py +""" +sn_basis/ui/base_dockwidget.py -from qgis.PyQt.QtWidgets import QDockWidget, QTabWidget -from sn_basis.functions.qgisqt_wrapper import warning, error +Basis-Dockwidget für alle LNO-Module. +""" + +from sn_basis.functions.qt_wrapper import QDockWidget, QTabWidget +from sn_basis.functions.message_wrapper import warning, error class BaseDockWidget(QDockWidget): """ Basis-Dockwidget für alle LNO-Module. + - 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 @@ -23,19 +28,15 @@ class BaseDockWidget(QDockWidget): # Titel setzen # ----------------------------------------------------- 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) except Exception as 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 # ----------------------------------------------------- @@ -45,15 +46,25 @@ class BaseDockWidget(QDockWidget): for tab_class in self.tabs: try: 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) 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) 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 @@ -68,6 +79,9 @@ class BaseDockWidget(QDockWidget): if self.action: self.action.setChecked(False) 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) diff --git a/ui/dockmanager.py b/ui/dockmanager.py index 8830e60..e8f0393 100644 --- a/ui/dockmanager.py +++ b/ui/dockmanager.py @@ -1,53 +1,69 @@ -# sn_basis/ui/dockmanager.py +""" +sn_basis/ui/dockmanager.py -from qgis.PyQt.QtCore import Qt -from qgis.PyQt.QtWidgets import QDockWidget -from qgis.utils import iface +Verwaltet das Anzeigen und Ersetzen von DockWidgets. +Stellt sicher, dass immer nur ein sn_basis-Dock gleichzeitig sichtbar ist. +""" -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: """ 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_" @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 - LNO-Docks (erkennbar am Prefix 'sn_dock_'). + sn_basis-Docks (erkennbar am Prefix 'sn_dock_'). """ + if dock_widget is None: error("Dock konnte nicht angezeigt werden", "Dock-Widget ist None.") return try: - area = area or cls.default_area - - # Prüfen, ob das Dock einen gültigen Namen hat + # Sicherstellen, dass das Dock einen Namen hat if not dock_widget.objectName(): dock_widget.setObjectName(f"{cls.dock_prefix}{id(dock_widget)}") - # Bestehende Plugin-Docks schließen + # Vorhandene Plugin-Docks entfernen try: - for widget in iface.mainWindow().findChildren(QDockWidget): - if widget is not dock_widget and widget.objectName().startswith(cls.dock_prefix): - iface.removeDockWidget(widget) + for widget in find_dock_widgets(): + if ( + widget is not dock_widget + and widget.objectName().startswith(cls.dock_prefix) + ): + remove_dock_widget(widget) widget.deleteLater() 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 try: - iface.addDockWidget(area, dock_widget) + add_dock_widget(area, dock_widget) dock_widget.show() except Exception as e: - error("Dock konnte nicht angezeigt werden", str(e)) + error( + "Dock konnte nicht angezeigt werden", + str(e), + ) except Exception as e: error("DockManager-Fehler", str(e)) + diff --git a/ui/navigation.py b/ui/navigation.py index 36c786c..c621b7c 100644 --- a/ui/navigation.py +++ b/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: - def __init__(self, iface): - self.iface = iface + def __init__(self): self.actions = [] - - # Menü und Toolbar einmalig anlegen - self.menu = QMenu("LNO Sachsen", iface.mainWindow()) - iface.mainWindow().menuBar().addMenu(self.menu) + self.menu = None + self.toolbar = None + self.plugin_group = None + - 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") - iface.addToolBar(self.toolbar) + add_toolbar(self.toolbar) - # Gruppe für exklusive Auswahl (nur ein Plugin aktiv) - self.plugin_group = QActionGroup(iface.mainWindow()) - self.plugin_group.setExclusive(True) + test_action = QAction("TEST ACTION", main_window) + self.menu.addAction(test_action) + self.toolbar.addAction(test_action) + + + + # ----------------------------------------------------- + # Actions + # ----------------------------------------------------- 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.setCheckable(True) # Button kann aktiv sein + action.setCheckable(True) action.triggered.connect(callback) - # Action in Gruppe aufnehmen self.plugin_group.addAction(action) - - # Action mit Priority speichern self.actions.append((priority, action)) return action - + 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]) - # Menüeinträge self.menu.clear() + self.toolbar.clear() + for _, action in self.actions: self.menu.addAction(action) - - # Toolbar-Einträge - self.toolbar.clear() - for _, action in self.actions: self.toolbar.addAction(action) def set_active_plugin(self, active_action): - # Alle zurücksetzen, dann aktives Plugin markieren for _, action in self.actions: action.setChecked(False) if active_action: active_action.setChecked(True) - def remove_all(self): - """Alles entfernen beim Entladen des Basisplugins""" - # 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 + # ----------------------------------------------------- + # Cleanup + # ----------------------------------------------------- def remove_action(self, action): - """Entfernt eine einzelne Action aus Menü und Toolbar""" if not action: return - # Menüeintrag entfernen + if self.menu: self.menu.removeAction(action) - # Toolbar-Eintrag entfernen if self.toolbar: self.toolbar.removeAction(action) - # Aus der internen Liste löschen + 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 + diff --git a/ui/tabs/settings_tab.py b/ui/tabs/settings_tab.py index ae164f3..e0ce1f6 100644 --- a/ui/tabs/settings_tab.py +++ b/ui/tabs/settings_tab.py @@ -1,129 +1,87 @@ -# sn_basis/ui/tabs/settings_tab.py +""" +sn_basis/ui/base_dockwidget.py -from sn_basis.functions.qgisqt_wrapper import ( - QWidget, QGridLayout, QLabel, QLineEdit, - QGroupBox, QVBoxLayout, QPushButton, - info, warning, error -) +Basis-Dockwidget für alle LNO-Module. +""" -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. - Nutzt SettingsLogic für das Laden/Speichern und den Wrapper für Meldungen. + Basis-Dockwidget für alle LNO-Module. + + - 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) - 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: - fields = { - key: widget.text() - for key, widget in {**self.user_inputs, **self.project_inputs}.items() - } + title = ( + self.base_title + 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: - 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. - Fehler werden über den Wrapper gemeldet. + Wird aufgerufen, wenn das Dock geschlossen wird. + Setzt die zugehörige Toolbar-Action zurück. """ try: - data = self.logic.load() - - for key, widget in {**self.user_inputs, **self.project_inputs}.items(): - widget.setText(data.get(key, "")) - + if self.action: + self.action.setChecked(False) 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)