diff --git a/main.py b/main.py index d77f966..66f2b59 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,8 @@ +# sn_plan41/main.py + from qgis.utils import plugins from sn_basis.ui.dockmanager import DockManager -from .ui.dockwidget import DockWidget +from sn_plan41.ui.dockwidget import DockWidget class Plan41: @@ -15,14 +17,17 @@ class Plan41: def initGui(self): basis = plugins.get("sn_basis") - if basis and basis.ui: - self.action = basis.ui.add_action( - self.plugin_name, - self.run, - tooltip=f"Öffnet {self.plugin_name}", - priority=20 - ) - basis.ui.finalize_menu_and_toolbar() + if not basis or not getattr(basis, "ui", None): + return + + self.action = basis.ui.add_action( + self.plugin_name, + self.run, + tooltip=f"Öffnet {self.plugin_name}", + priority=20, + ) + basis.ui.finalize_menu_and_toolbar() + print("Plan41/sn_Basis:initGui called") def unload(self): if self.dockwidget: @@ -32,13 +37,15 @@ class Plan41: if self.action: basis = plugins.get("sn_basis") - if basis and basis.ui: - # Action aus Menü und Toolbar entfernen + if basis and getattr(basis, "ui", None): basis.ui.remove_action(self.action) self.action = None def run(self): - self.dockwidget = DockWidget(self.iface.mainWindow(), subtitle=self.plugin_name) + self.dockwidget = DockWidget( + self.iface.mainWindow(), + subtitle=self.plugin_name, + ) self.dockwidget.setObjectName(self.dock_name) # Action-Referenz im Dock speichern @@ -48,5 +55,5 @@ class Plan41: # Toolbar-Button als aktiv markieren basis = plugins.get("sn_basis") - if basis and basis.ui: + if basis and getattr(basis, "ui", None): basis.ui.set_active_plugin(self.action) diff --git a/pyrightconfig.json b/pyrightconfig.json new file mode 100644 index 0000000..17b70df --- /dev/null +++ b/pyrightconfig.json @@ -0,0 +1,3 @@ +{ + "extraPaths": ["."] +} diff --git a/test/run_tests.py b/test/run_tests.py deleted file mode 100644 index f61f42f..0000000 --- a/test/run_tests.py +++ /dev/null @@ -1,27 +0,0 @@ -import unittest -import os -import sys - -# Plugin-Hauptverzeichnis ermitteln -BASE_DIR = os.path.abspath(os.path.dirname(__file__)) - -# Plugin-Ordner in den Python-Pfad aufnehmen -sys.path.insert(0, BASE_DIR) - -def run(): - # Testverzeichnis - test_dir = os.path.join(BASE_DIR, "tests") - - # Test-Suite automatisch finden - suite = unittest.defaultTestLoader.discover(test_dir) - - # Runner starten - runner = unittest.TextTestRunner(verbosity=2) - result = runner.run(suite) - - # Exit-Code setzen (für CI oder Skripte nützlich) - sys.exit(not result.wasSuccessful()) - - -if __name__ == "__main__": - run() diff --git a/test/test_tab_a.py b/test/test_tab_a.py deleted file mode 100644 index 4524392..0000000 --- a/test/test_tab_a.py +++ /dev/null @@ -1,112 +0,0 @@ -import unittest -import os -import tempfile -import sys - -# Plugin-Ordner in den Python-Pfad aufnehmen -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) - -from qgis.PyQt.QtWidgets import QApplication -from qgis.core import QgsProject, QgsVectorLayer - -from sn_plan41.ui.tabs import tab_a - - -class TestTabA(unittest.TestCase): - - @classmethod - def setUpClass(cls): - """Qt-Anwendung initialisieren.""" - cls.app = QApplication([]) - - def setUp(self): - """Vor jedem Test: Projektvariablen löschen und TabA neu erzeugen.""" - self.project = QgsProject.instance() - self.project.setCustomVariables({}) - - # TabA erzeugen - self.tab = TabA() - - # Temporäre Testdateien - self.tmp_dir = tempfile.gettempdir() - self.test_db = os.path.join(self.tmp_dir, "test_db.gpkg") - self.test_link = os.path.join(self.tmp_dir, "test_link.xlsx") - - # Dummy-Dateien anlegen - with open(self.test_db, "w") as f: - f.write("") - with open(self.test_link, "w") as f: - f.write("") - - # --------------------------------------------------------- - # Verfahrens-DB speichern & wiederherstellen - # --------------------------------------------------------- - def test_save_and_restore_verfahrens_db(self): - self.tab.on_file_changed(self.test_db) - - vars = self.project.customVariables() - self.assertEqual(vars.get("sn_verfahrens_db"), self.test_db) - - tab2 = TabA() - self.assertEqual(tab2.verfahrens_db, self.test_db) - self.assertEqual(tab2.file_widget.filePath(), self.test_db) - - # --------------------------------------------------------- - # Verfahrens-DB löschen - # --------------------------------------------------------- - def test_delete_verfahrens_db(self): - self.tab.on_file_changed(self.test_db) - self.tab.on_file_changed("") - - vars = self.project.customVariables() - self.assertNotIn("sn_verfahrens_db", vars) - self.assertIsNone(self.tab.verfahrens_db) - - # --------------------------------------------------------- - # Linkliste speichern & löschen - # --------------------------------------------------------- - def test_save_and_delete_linkliste(self): - self.tab.on_linkliste_changed(self.test_link) - - vars = self.project.customVariables() - self.assertEqual(vars.get("sn_linkliste"), self.test_link) - - self.tab.on_linkliste_changed("") - - vars = self.project.customVariables() - self.assertNotIn("sn_linkliste", vars) - - # --------------------------------------------------------- - # Layer-Vorauswahl - # --------------------------------------------------------- - def test_preselect_verfahrensgebiet_layer(self): - vg_layer = QgsVectorLayer("Polygon?crs=EPSG:4326", "Verfahrensgebiet", "memory") - other_layer = QgsVectorLayer("Polygon?crs=EPSG:4326", "AndereDaten", "memory") - - self.project.addMapLayer(other_layer) - self.project.addMapLayer(vg_layer) - - tab2 = TabA() - - selected = tab2.layer_combo.currentLayer() - self.assertIsNotNone(selected) - self.assertEqual(selected.name(), "Verfahrensgebiet") - - # --------------------------------------------------------- - # Gespeicherter Layer wird wiederhergestellt - # --------------------------------------------------------- - def test_restore_saved_layer(self): - vg_layer = QgsVectorLayer("Polygon?crs=EPSG:4326", "Verfahrensgebiet", "memory") - self.project.addMapLayer(vg_layer) - - vars = {"sn_verfahrensgebiet_layer": vg_layer.id()} - self.project.setCustomVariables(vars) - - tab2 = TabA() - - selected = tab2.layer_combo.currentLayer() - self.assertEqual(selected.id(), vg_layer.id()) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/__init__.py b/tests/__init__.py similarity index 100% rename from test/__init__.py rename to tests/__init__.py diff --git a/tests/run_tests.py b/tests/run_tests.py new file mode 100644 index 0000000..42e1cdd --- /dev/null +++ b/tests/run_tests.py @@ -0,0 +1,148 @@ +""" +sn_plan41/test/run_tests.py + +Zentraler Test-Runner für sn_plan41. +Wrapper-konform, QGIS-unabhängig, CI- und IDE-fähig. +""" + +import unittest +import datetime +import inspect +import os +import sys +from pathlib import Path + + +# --------------------------------------------------------- +# Plugin-Roots bestimmen +# --------------------------------------------------------- + +THIS_FILE = Path(__file__).resolve() + +# .../plugins/sn_plan41 +SN_PLAN41_ROOT = THIS_FILE.parents[1] +# .../plugins/sn_basis +SN_BASIS_ROOT = SN_PLAN41_ROOT.parent / "sn_basis" + +# --------------------------------------------------------- +# sys.path Bootstrap +# --------------------------------------------------------- + +for path in (SN_PLAN41_ROOT, SN_BASIS_ROOT): + path_str = str(path) + if path_str not in sys.path: + sys.path.insert(0, path_str) + + +# --------------------------------------------------------- +# Farben +# --------------------------------------------------------- + +RED = "\033[91m" +YELLOW = "\033[93m" +GREEN = "\033[92m" +CYAN = "\033[96m" +MAGENTA = "\033[95m" +RESET = "\033[0m" + +GLOBAL_TEST_COUNTER = 0 + + +# --------------------------------------------------------- +# Farbige TestResult-Klasse +# --------------------------------------------------------- + +class ColoredTestResult(unittest.TextTestResult): + + _last_test_class = None + + def startTest(self, test): + global GLOBAL_TEST_COUNTER + GLOBAL_TEST_COUNTER += 1 + self.stream.write(f"{CYAN}[Test {GLOBAL_TEST_COUNTER}]{RESET}\n") + super().startTest(test) + + def startTestClass(self, test): + cls = test.__class__ + file = inspect.getfile(cls) + filename = os.path.basename(file) + + self.stream.write( + f"\n{MAGENTA}{'=' * 70}\n" + f"Starte Testklasse: {filename} → {cls.__name__}\n" + f"{'=' * 70}{RESET}\n" + ) + + def addError(self, test, err): + super().addError(test, err) + self.stream.write(f"{RED}ERROR{RESET}\n") + + def addFailure(self, test, err): + super().addFailure(test, err) + self.stream.write(f"{RED}FAILURE{RESET}\n") + + def addSkip(self, test, reason): + super().addSkip(test, reason) + self.stream.write(f"{YELLOW}SKIPPED{RESET}: {reason}\n") + + def addSuccess(self, test): + super().addSuccess(test) + self.stream.write(f"{GREEN}OK{RESET}\n") + + +# --------------------------------------------------------- +# Farbiger TestRunner +# --------------------------------------------------------- + +class ColoredTestRunner(unittest.TextTestRunner): + + def _makeResult(self): + result = ColoredTestResult( + self.stream, + self.descriptions, + self.verbosity, + ) + + original_start_test = result.startTest + + def patched_start_test(test): + if result._last_test_class != test.__class__: + result.startTestClass(test) + result._last_test_class = test.__class__ + original_start_test(test) + + result.startTest = patched_start_test + return result + + +# --------------------------------------------------------- +# Testlauf starten +# --------------------------------------------------------- + +def main(): + print("\n" + "=" * 70) + print( + f"{CYAN}Testlauf gestartet am: " + f"{datetime.datetime.now():%Y-%m-%d %H:%M:%S}{RESET}" + ) + print("=" * 70 + "\n") + + loader = unittest.TestLoader() + + TEST_ROOT = SN_PLAN41_ROOT / "tests" + + suite = loader.discover( + start_dir=str(TEST_ROOT), + pattern="test_*.py", + top_level_dir=str(SN_PLAN41_ROOT.parent), +) + + + runner = ColoredTestRunner(verbosity=2) + result = runner.run(suite) + + return 0 if result.wasSuccessful() else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test/start_osgeo4w_qgis.bat b/tests/start_osgeo4w_qgis.bat similarity index 100% rename from test/start_osgeo4w_qgis.bat rename to tests/start_osgeo4w_qgis.bat diff --git a/tests/test_tab_a_logic.py b/tests/test_tab_a_logic.py new file mode 100644 index 0000000..eed5bd4 --- /dev/null +++ b/tests/test_tab_a_logic.py @@ -0,0 +1,153 @@ +import unittest +from pathlib import Path +from tempfile import TemporaryDirectory + +from sn_plan41.ui.tab_a_logic import TabALogic # type: ignore +from sn_basis.functions.variable_wrapper import get_variable # type: ignore +from sn_basis.functions.sys_wrapper import file_exists # type: ignore + + +class TestTabALogic(unittest.TestCase): + + # ----------------------------------------------------- + # 1. Verfahrens-DB setzen und laden + # ----------------------------------------------------- + def test_verfahrens_db_set_and_load(self): + logic = TabALogic() + + with TemporaryDirectory() as tmp: + db_path = Path(tmp) / "test.gpkg" + db_path.write_text("") + + logic.set_verfahrens_db(str(db_path)) + + stored = get_variable("verfahrens_db", scope="project") + self.assertEqual(stored, str(db_path)) + + loaded = logic.load_verfahrens_db() + self.assertEqual(loaded, str(db_path)) + + # ----------------------------------------------------- + # 2. Verfahrens-DB löschen + # ----------------------------------------------------- + def test_verfahrens_db_clear(self): + logic = TabALogic() + + logic.set_verfahrens_db(None) + + stored = get_variable("verfahrens_db", scope="project") + self.assertEqual(stored, "") + + # ----------------------------------------------------- + # 3. Neue Verfahrens-DB anlegen + # ----------------------------------------------------- + def test_create_new_verfahrens_db(self): + logic = TabALogic() + + with TemporaryDirectory() as tmp: + db_path = Path(tmp) / "neu.gpkg" + + result = logic.create_new_verfahrens_db(str(db_path)) + + self.assertTrue(result) + self.assertTrue(file_exists(db_path)) + + stored = get_variable("verfahrens_db", scope="project") + self.assertEqual(stored, str(db_path)) + + def test_create_new_verfahrens_db_with_none_path(self): + logic = TabALogic() + result = logic.create_new_verfahrens_db(None) + self.assertFalse(result) + + + + # ----------------------------------------------------- + # 4. Linkliste setzen und laden + # ----------------------------------------------------- + def test_linkliste_set_and_load(self): + logic = TabALogic() + + with TemporaryDirectory() as tmp: + link_path = Path(tmp) / "links.xlsx" + link_path.write_text("dummy") + + logic.set_linkliste(str(link_path)) + + stored = get_variable("linkliste", scope="project") + self.assertEqual(stored, str(link_path)) + + loaded = logic.load_linkliste() + self.assertEqual(loaded, str(link_path)) + + # ----------------------------------------------------- + # 5. Linkliste löschen + # ----------------------------------------------------- + def test_linkliste_clear(self): + logic = TabALogic() + + logic.set_linkliste(None) + + stored = get_variable("linkliste", scope="project") + self.assertEqual(stored, "") + + # ----------------------------------------------------- + # 6. Layer-ID speichern + # ----------------------------------------------------- + def test_verfahrensgebiet_layer_id_storage(self): + logic = TabALogic() + + class MockLayer: + def id(self): + return "layer-123" + + logic.save_verfahrensgebiet_layer(MockLayer()) + + stored = get_variable("verfahrensgebiet_layer", scope="project") + self.assertEqual(stored, "layer-123") + + # ----------------------------------------------------- + # 7. Ungültiger Layer wird ignoriert + # ----------------------------------------------------- + def test_invalid_layer_is_rejected(self): + logic = TabALogic() + + class InvalidLayer: + pass + + logic.save_verfahrensgebiet_layer(InvalidLayer()) + + stored = get_variable("verfahrensgebiet_layer", scope="project") + self.assertEqual(stored, "") + #----------------------------------------------------- + # 8. Layer-ID wirft Exception + #---------------------------------------------------- + def test_layer_id_raises_exception(self): + logic = TabALogic() + + class BadLayer: + def id(self): + raise RuntimeError("boom") + + logic.save_verfahrensgebiet_layer(BadLayer()) + + stored = get_variable("verfahrensgebiet_layer", scope="project") + self.assertEqual(stored, "") + # ----------------------------------------------------- + # 11. Layer ID wird leer zurückgegeben + # ----------------------------------------------------- + def test_layer_id_returns_empty(self): + logic = TabALogic() + + class EmptyLayer: + def id(self): + return "" + + logic.save_verfahrensgebiet_layer(EmptyLayer()) + + stored = get_variable("verfahrensgebiet_layer", scope="project") + self.assertEqual(stored, "") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_tab_a_ui.py b/tests/test_tab_a_ui.py new file mode 100644 index 0000000..4911949 --- /dev/null +++ b/tests/test_tab_a_ui.py @@ -0,0 +1,57 @@ +""" +Smoke-Tests für TabA UI (sn_plan41/ui/tab_a_ui.py) + +Ziel: +- UI kann erstellt werden +- Callbacks crashen nicht +- Keine Qt-Verhaltensprüfung +""" + +import unittest + +from sn_plan41.ui.tab_a_ui import TabA #type:ignore + + +class TestTabAUI(unittest.TestCase): + + # ----------------------------------------------------- + # 1. UI kann erstellt werden + # ----------------------------------------------------- + def test_tab_a_ui_can_be_created(self): + tab = TabA(parent=None,build_ui=False) + + self.assertIsNotNone(tab) + self.assertEqual(tab.tab_title, "Daten") + + # ----------------------------------------------------- + # 2. Toggle-Callbacks crashen nicht + # ----------------------------------------------------- + def test_tab_a_toggle_callbacks_do_not_crash(self): + tab = TabA(parent=None,build_ui=False) + + tab._toggle_group(True) + tab._toggle_group(False) + + tab._toggle_optional(True) + tab._toggle_optional(False) + + # ----------------------------------------------------- + # 3. Datei-Callbacks akzeptieren leere Eingaben + # ----------------------------------------------------- + def test_tab_a_file_callbacks_accept_empty_input(self): + tab = TabA(parent=None,build_ui=False) + + tab._on_verfahrens_db_changed("") + tab._on_linkliste_changed("") + + # ----------------------------------------------------- + # 4. Layer-Callback akzeptiert None + # ----------------------------------------------------- + def test_tab_a_layer_callback_accepts_none(self): + tab = TabA(parent=None,build_ui=False) + + tab._on_layer_changed(None) + + +if __name__ == "__main__": + unittest.main() diff --git a/ui/dockwidget.py b/ui/dockwidget.py index 5ec1750..9f74104 100644 --- a/ui/dockwidget.py +++ b/ui/dockwidget.py @@ -1,7 +1,7 @@ -from sn_basis.ui.tabs.settings_tab import SettingsTab -from sn_plan41.ui.tabs.tab_a import TabA -from sn_plan41.ui.tabs.tab_b import TabB -from sn_basis.ui.base_dockwidget import BaseDockWidget +from sn_basis.ui.tabs.settings_tab import SettingsTab +from sn_plan41.ui.tab_a_ui import TabA +#from sn_plan41.ui.tabs.tab_b import TabB +from sn_basis.ui.base_dockwidget import BaseDockWidget class DockWidget(BaseDockWidget): - tabs = [TabA, TabB, SettingsTab] + tabs = [TabA, SettingsTab] diff --git a/ui/tab_a_logic.py b/ui/tab_a_logic.py new file mode 100644 index 0000000..e930cb5 --- /dev/null +++ b/ui/tab_a_logic.py @@ -0,0 +1,132 @@ +""" +sn_plan41/ui/tab_a_logic.py – Fachlogik für Tab A (Daten) +""" + +from typing import Optional + +from sn_basis.functions.variable_wrapper import (#type: ignore + get_variable, + set_variable, +) +from sn_basis.functions.sys_wrapper import (#type:ignore + file_exists, + write_text, +) +from sn_basis.functions.ly_existence_wrapper import layer_exists#type:ignore +from sn_basis.functions.ly_metadata_wrapper import get_layer_type#type:ignore + + +class TabALogic: + """ + Kapselt die komplette Logik von Tab A: + - Verfahrens-Datenbank + - optionale Linkliste + - Verfahrensgebiet-Layer + """ + + # ------------------------------- + # Verfahrens-Datenbank + # ------------------------------- + + def load_verfahrens_db(self) -> Optional[str]: + """ + Lädt die gespeicherte Verfahrens-Datenbank. + """ + path = get_variable("verfahrens_db", scope="project") + if path and file_exists(path): + return path + return None + + def set_verfahrens_db(self, path: Optional[str]) -> None: + """ + Speichert oder löscht die Verfahrens-Datenbank. + """ + if path: + set_variable("verfahrens_db", path, scope="project") + else: + set_variable("verfahrens_db", "", scope="project") + + def create_new_verfahrens_db(self, path: str) -> bool: + """ + Legt eine neue leere GPKG-Datei an. + """ + if not path: + return False + + try: + write_text(path, "") + except Exception: + return False + + self.set_verfahrens_db(path) + return True + + + # ------------------------------- + # Lokale Linkliste + # ------------------------------- + + def load_linkliste(self) -> Optional[str]: + """ + Lädt die gespeicherte lokale Linkliste. + """ + path = get_variable("linkliste", scope="project") + if path and file_exists(path): + return path + return None + + def set_linkliste(self, path: Optional[str]) -> None: + """ + Speichert oder löscht die lokale Linkliste. + """ + if path: + set_variable("linkliste", path, scope="project") + else: + set_variable("linkliste", "", scope="project") + + # ------------------------------- + # Verfahrensgebiet-Layer + # ------------------------------- + + def save_verfahrensgebiet_layer(self, layer) -> None: + """ + Speichert die ID des Verfahrensgebiet-Layers. + Ungültige Layer werden ignoriert. + """ + if layer is None: + set_variable("verfahrensgebiet_layer", "", scope="project") + return + + if not hasattr(layer, "id") or not callable(layer.id): + set_variable("verfahrensgebiet_layer", "", scope="project") + return + + try: + layer_id = layer.id() + except Exception: + set_variable("verfahrensgebiet_layer", "", scope="project") + return + + if not layer_id: + set_variable("verfahrensgebiet_layer", "", scope="project") + return + + set_variable("verfahrensgebiet_layer", layer_id, scope="project") + + + def load_verfahrensgebiet_layer_id(self) -> Optional[str]: + """ + Lädt die gespeicherte Layer-ID. + """ + value = get_variable("verfahrensgebiet_layer", scope="project") + return value or None + + def is_valid_verfahrensgebiet_layer(self, layer) -> bool: + """ + Prüft, ob ein Layer als Verfahrensgebiet geeignet ist. + """ + if not layer_exists(layer): + return False + + layer_type = get_layer_type(layer) + return layer_type == "vector" diff --git a/ui/tab_a_ui.py b/ui/tab_a_ui.py new file mode 100644 index 0000000..a692f25 --- /dev/null +++ b/ui/tab_a_ui.py @@ -0,0 +1,308 @@ +""" +sn_plan41/ui/tab_a_ui.py – UI für Tab A (Daten) +""" + +from typing import Optional + +from sn_basis.functions.qt_wrapper import ( # type: ignore + QWidget, + QVBoxLayout, + QLabel, + QPushButton, + QToolButton, + QFileDialog, + QMessageBox, + QTabWidget, + ToolButtonTextBesideIcon, + ArrowDown, + ArrowRight, + SizePolicyPreferred, + SizePolicyMaximum, + +) +from sn_basis.functions.qgisui_wrapper import ( # type: ignore + QgsFileWidget, + QgsMapLayerComboBox, + add_dock_widget, +) +from sn_basis.functions.qgiscore_wrapper import ( # type: ignore + QgsProject, + QgsMapLayerProxyModel, +) +from sn_basis.functions.message_wrapper import ( # type: ignore + info, + warning, + error, +) +from sn_basis.functions.dialog_wrapper import ask_yes_no # type: ignore +from sn_basis.functions.sys_wrapper import file_exists # type: ignore + +from sn_plan41.ui.tab_a_logic import TabALogic # type: ignore + + +class TabA(QWidget): + """ + UI-Klasse für Tab A (Daten). + Enthält ausschließlich UI-Code und delegiert Logik an TabALogic. + """ + + + + def __init__(self, parent=None, build_ui: bool=True): + super().__init__(parent) + self.parent=parent + self.tab_title="Daten" + + self.logic = TabALogic() + + self.verfahrens_db: Optional[str] = None + self.lokale_linkliste: Optional[str] = None + + if build_ui: + self._build_ui() + self._restore_state() + + # --------------------------------------------------------- + # UI-Aufbau + # --------------------------------------------------------- + + def _build_ui(self) -> None: + main_layout = QVBoxLayout() + main_layout.setSpacing(4) + main_layout.setContentsMargins(4, 4, 4, 4) + + # ------------------------------- + # Verfahrens-Datenbank + # ------------------------------- + + self.group_button = QToolButton() + self.group_button.setText("Verfahrens-Datenbank") + self.group_button.setCheckable(True) + self.group_button.setChecked(True) + self.group_button.setToolButtonStyle(ToolButtonTextBesideIcon) + self.group_button.setArrowType(ArrowDown) + + self.group_button.setStyleSheet("font-weight: bold;") + self.group_button.toggled.connect(self._toggle_group) + main_layout.addWidget(self.group_button) + + self.group_content = QWidget() + self.group_content.setSizePolicy(SizePolicyPreferred, SizePolicyMaximum) + + + group_layout = QVBoxLayout() + group_layout.setSpacing(2) + group_layout.setContentsMargins(10, 4, 4, 4) + + group_layout.addWidget(QLabel("bestehende Datei auswählen")) + + self.file_widget = QgsFileWidget() + self.file_widget.setStorageMode(QgsFileWidget.GetFile) + self.file_widget.setFilter("Geopackage (*.gpkg)") + self.file_widget.fileChanged.connect(self._on_verfahrens_db_changed) + group_layout.addWidget(self.file_widget) + + group_layout.addWidget(QLabel("-oder-")) + + self.btn_new = QPushButton("Neue Verfahrens-DB anlegen") + self.btn_new.clicked.connect(self._create_new_gpkg) + group_layout.addWidget(self.btn_new) + + self.group_content.setLayout(group_layout) + main_layout.addWidget(self.group_content) + + # ------------------------------- + # Optionale Linkliste + # ------------------------------- + + self.optional_button = QToolButton() + self.optional_button.setText("Optional: Lokale Linkliste") + self.optional_button.setCheckable(True) + self.optional_button.setChecked(False) + self.optional_button.setToolButtonStyle(ToolButtonTextBesideIcon) + self.optional_button.setArrowType(ArrowRight) + + self.optional_button.setStyleSheet("font-weight: bold; margin-top: 6px;") + self.optional_button.toggled.connect(self._toggle_optional) + main_layout.addWidget(self.optional_button) + + self.optional_content = QWidget() + self.optional_content.setSizePolicy(SizePolicyPreferred, SizePolicyMaximum) + + optional_layout = QVBoxLayout() + optional_layout.setSpacing(2) + optional_layout.setContentsMargins(10, 4, 4, 20) + + optional_layout.addWidget(QLabel("(frei lassen für globale Linkliste)")) + + self.linkliste_widget = QgsFileWidget() + self.linkliste_widget.setStorageMode(QgsFileWidget.GetFile) + self.linkliste_widget.setFilter("Excelliste (*.xlsx)") + self.linkliste_widget.fileChanged.connect(self._on_linkliste_changed) + optional_layout.addWidget(self.linkliste_widget) + + self.optional_content.setLayout(optional_layout) + self.optional_content.setVisible(False) + main_layout.addWidget(self.optional_content) + + # ------------------------------- + # Layer-Auswahl + # ------------------------------- + + layer_label = QLabel("Verfahrensgebiet-Layer auswählen") + layer_label.setStyleSheet("font-weight: bold; margin-top: 6px;") + main_layout.addWidget(layer_label) + + self.layer_combo = QgsMapLayerComboBox() + self.layer_combo.setSizePolicy(SizePolicyPreferred, SizePolicyMaximum) + self.layer_combo.setFilters(QgsMapLayerProxyModel.VectorLayer) + self.layer_combo.layerChanged.connect(self._on_layer_changed) + main_layout.addWidget(self.layer_combo) + + main_layout.addStretch(1) + self.setLayout(main_layout) + + # --------------------------------------------------------- + # State Restore + # --------------------------------------------------------- + + def _restore_state(self) -> None: + db = self.logic.load_verfahrens_db() + if db: + self.verfahrens_db = db + self.file_widget.setFilePath(db) + self._update_group_color() + + link = self.logic.load_linkliste() + if link: + self.lokale_linkliste = link + self.linkliste_widget.setFilePath(link) + + layer_id = self.logic.load_verfahrensgebiet_layer_id() + if layer_id: + layer = QgsProject.instance().mapLayer(layer_id) + if layer: + self.layer_combo.setLayer(layer) + + # --------------------------------------------------------- + # UI-Callbacks + # --------------------------------------------------------- + + def _toggle_group(self, checked: bool): + """ + Klappt den Gruppenbereich ein oder aus. + """ + if not hasattr(self, "group_button"): + return + + self.group_button.setArrowType( + ArrowDown if checked else ArrowRight +) + + self.group_content.setVisible(checked) + + + def _toggle_optional(self, checked: bool): + """ + Klappt den optionalen Bereich ein oder aus. + """ + if not hasattr(self, "optional_button"): + return + + self.group_button.setArrowType( + ArrowDown if checked else ArrowRight +) + + self.optional_content.setVisible(checked) + + + def _on_verfahrens_db_changed(self, path: str) -> None: + if not path: + self.verfahrens_db = None + self.logic.set_verfahrens_db(None) + self._update_group_color() + return + + if not path.lower().endswith(".gpkg"): + path += ".gpkg" + self.file_widget.setFilePath(path) + + if not file_exists(path): + warning("Datei nicht gefunden", f"Die Datei existiert nicht:\n{path}") + self.file_widget.setFilePath("") + return + + self.verfahrens_db = path + self.logic.set_verfahrens_db(path) + self._update_group_color() + + def _on_linkliste_changed(self, path: str) -> None: + if not path: + self.lokale_linkliste = None + self.logic.set_linkliste(None) + return + + if not path.lower().endswith(".xlsx"): + path += ".xlsx" + self.linkliste_widget.setFilePath(path) + + if not file_exists(path): + warning("Datei nicht gefunden", f"Die Datei existiert nicht:\n{path}") + self.linkliste_widget.setFilePath("") + return + + self.lokale_linkliste = path + self.logic.set_linkliste(path) + + def _on_layer_changed(self, layer) -> None: + self.logic.save_verfahrensgebiet_layer(layer) + + def _create_new_gpkg(self) -> None: + file_path, _ = QFileDialog.getSaveFileName( + self, + "Neue Verfahrens-Datenbank anlegen", + "", + "Geopackage (*.gpkg)", + ) + + if not file_path: + return + + if not file_path.lower().endswith(".gpkg"): + file_path += ".gpkg" + + if file_exists(file_path): + overwrite = ask_yes_no( + "Datei existiert bereits", + f"Die Datei existiert bereits:\n\n{file_path}\n\nSoll sie überschrieben werden?", + default=False, + parent=self, + ) + if not overwrite: + return + + if not self.logic.create_new_verfahrens_db(file_path): + error("Fehler", "Die Datei konnte nicht angelegt werden.") + return + + self.verfahrens_db = file_path + self.file_widget.setFilePath(file_path) + self._update_group_color() + info("Projekt-DB angelegt", f"Neue Projekt-Datenbank wurde angelegt:\n{file_path}") + + # --------------------------------------------------------- + # UI-Helfer + # --------------------------------------------------------- + + def _update_group_color(self): + """ + Aktualisiert die Darstellung der Gruppenüberschrift. + """ + if not hasattr(self, "group_button"): + return + + if self.verfahrens_db: + self.group_button.setStyleSheet("font-weight: bold;") + else: + self.group_button.setStyleSheet("") + diff --git a/ui/tabs/tab_a.py b/ui/tabs/tab_a.py deleted file mode 100644 index 7480955..0000000 --- a/ui/tabs/tab_a.py +++ /dev/null @@ -1,316 +0,0 @@ -import os - -from qgis.PyQt.QtWidgets import ( - QWidget, QVBoxLayout, QLabel, QMessageBox, QPushButton, - QFileDialog, QToolButton, QSizePolicy -) -from qgis.PyQt.QtCore import Qt -from qgis.gui import QgsFileWidget, QgsMapLayerComboBox -from qgis.core import QgsProject, QgsMapLayerProxyModel - - -class TabA(QWidget): - tab_title = "Daten" - - def __init__(self, parent=None): - super().__init__(parent) - - # Variablen initialisieren - self.verfahrens_db = None - self.lokale_linkliste = None - - # --------------------------------------------------------- - # Hauptlayout - # --------------------------------------------------------- - main_layout = QVBoxLayout() - main_layout.setSpacing(4) - main_layout.setContentsMargins(4, 4, 4, 4) - - # --------------------------------------------------------- - # COLLAPSIBLE GRUPPE: Verfahrens-Datenbank - # --------------------------------------------------------- - self.group_button = QToolButton() - self.group_button.setText("Verfahrens-Datenbank") - self.group_button.setCheckable(True) - self.group_button.setChecked(True) - self.group_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) - self.group_button.setArrowType(Qt.DownArrow) - self.group_button.setStyleSheet("font-weight: bold;") - self.group_button.toggled.connect(self.toggle_group) - main_layout.addWidget(self.group_button, 0) - - # Inhalt der Gruppe - self.group_content = QWidget() - self.group_content.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum) - - group_layout = QVBoxLayout() - group_layout.setSpacing(2) - group_layout.setContentsMargins(10, 4, 4, 4) - - # Hinweis - hinweis1 = QLabel("bestehende Datei auswählen") - group_layout.addWidget(hinweis1) - - # Datei-Auswahl - self.file_widget = QgsFileWidget() - self.file_widget.setStorageMode(QgsFileWidget.GetFile) - self.file_widget.setFilter("Geopackage (*.gpkg)") - self.file_widget.fileChanged.connect(self.on_file_changed) - group_layout.addWidget(self.file_widget) - - # Hinweis "-oder-" - hinweis2 = QLabel("-oder-") - group_layout.addWidget(hinweis2) - - # Button: Neue Datei - self.btn_new = QPushButton("Neue Verfahrens-DB anlegen") - self.btn_new.clicked.connect(self.create_new_gpkg) - group_layout.addWidget(self.btn_new) - - self.group_content.setLayout(group_layout) - main_layout.addWidget(self.group_content, 0) - - # --------------------------------------------------------- - # COLLAPSIBLE Optional-Bereich - # --------------------------------------------------------- - self.optional_button = QToolButton() - self.optional_button.setText("Optional: Lokale Linkliste") - self.optional_button.setCheckable(True) - self.optional_button.setChecked(False) - self.optional_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) - self.optional_button.setArrowType(Qt.RightArrow) - self.optional_button.setStyleSheet("font-weight: bold; margin-top: 6px;") - self.optional_button.toggled.connect(self.toggle_optional) - main_layout.addWidget(self.optional_button, 0) - - # Inhalt optional - self.optional_content = QWidget() - self.optional_content.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum) - - optional_layout = QVBoxLayout() - optional_layout.setSpacing(2) - optional_layout.setContentsMargins(10, 4, 4, 20) - - # Hinweistext - optional_hint = QLabel("(frei lassen für globale Linkliste)") - optional_layout.addWidget(optional_hint) - - # Datei-Auswahl für Linkliste - self.linkliste_widget = QgsFileWidget() - self.linkliste_widget.setStorageMode(QgsFileWidget.GetFile) - self.linkliste_widget.setFilter("Excelliste (*.xlsx)") - self.linkliste_widget.fileChanged.connect(self.on_linkliste_changed) - optional_layout.addWidget(self.linkliste_widget) - main_layout.addWidget(self.optional_content, 0) - - # --------------------------------------------------------- - # Layer-Auswahlfeld - # --------------------------------------------------------- - layer_label = QLabel("Verfahrensgebiet-Layer auswählen") - layer_label.setStyleSheet("font-weight: bold; margin-top: 6px;") - main_layout.addWidget(layer_label) - - self.layer_combo = QgsMapLayerComboBox() - self.layer_combo.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum) - - # ✅ QGIS 3.22–3.46 kompatibel - self.layer_combo.setFilters(QgsMapLayerProxyModel.VectorLayer) - - # Layerwechsel speichern - self.layer_combo.layerChanged.connect(self.on_layer_changed) - - main_layout.addWidget(self.layer_combo) - - self.optional_content.setLayout(optional_layout) - self.optional_content.setVisible(False) - - - # Spacer - main_layout.addStretch(1) - - self.setLayout(main_layout) - - # ✅ gespeicherte Werte wiederherstellen (jetzt existieren die Widgets!) - self.restore_saved_values() - - # ✅ Layer-Vorauswahl durchführen - self.preselect_verfahrensgebiet_layer() - - # --------------------------------------------------------- - # Collapsible Gruppe ein-/ausblenden - # --------------------------------------------------------- - def toggle_group(self, checked): - self.group_button.setArrowType(Qt.DownArrow if checked else Qt.RightArrow) - self.group_content.setVisible(checked) - - def toggle_optional(self, checked): - self.optional_button.setArrowType(Qt.DownArrow if checked else Qt.RightArrow) - self.optional_content.setVisible(checked) - - # --------------------------------------------------------- - # Datei-Auswahl: Verfahrens-DB - # --------------------------------------------------------- - def on_file_changed(self, path: str): - if not path: - self.verfahrens_db = None - - # ✅ Projektvariable löschen - vars = QgsProject.instance().customVariables() - if "sn_verfahrens_db" in vars: - del vars["sn_verfahrens_db"] - QgsProject.instance().setCustomVariables(vars) - - self.update_group_button_color() - return - - if not path.lower().endswith(".gpkg"): - path += ".gpkg" - self.file_widget.setFilePath(path) - - if os.path.exists(path): - self.verfahrens_db = path - else: - self.verfahrens_db = None - QMessageBox.warning(self, "Datei nicht gefunden", f"Die Datei existiert nicht:\n{path}") - self.file_widget.setFilePath("") - - # ✅ speichern - vars = QgsProject.instance().customVariables() - vars["sn_verfahrens_db"] = self.verfahrens_db - QgsProject.instance().setCustomVariables(vars) - - self.update_group_button_color() - - # --------------------------------------------------------- - # Datei-Auswahl: Lokale Linkliste - # --------------------------------------------------------- - def on_linkliste_changed(self, path: str): - if not path: - self.lokale_linkliste = None - - vars = QgsProject.instance().customVariables() - if "sn_linkliste" in vars: - del vars["sn_linkliste"] - QgsProject.instance().setCustomVariables(vars) - - return - - - if not path.lower().endswith(".xlsx"): - path += ".xlsx" - self.linkliste_widget.setFilePath(path) - - if os.path.exists(path): - self.lokale_linkliste = path - else: - self.lokale_linkliste = None - QMessageBox.warning(self, "Datei nicht gefunden", f"Die Datei existiert nicht:\n{path}") - self.linkliste_widget.setFilePath("") - - # ✅ speichern - vars = QgsProject.instance().customVariables() - vars["sn_linkliste"] = self.lokale_linkliste - QgsProject.instance().setCustomVariables(vars) - - # --------------------------------------------------------- - # Layer-Auswahl speichern - # --------------------------------------------------------- - def on_layer_changed(self, layer): - if layer: - vars = QgsProject.instance().customVariables() - vars["sn_verfahrensgebiet_layer"] = layer.id() - QgsProject.instance().setCustomVariables(vars) - - # --------------------------------------------------------- - # Button-Farbe aktualisieren - # --------------------------------------------------------- - def update_group_button_color(self): - if self.verfahrens_db: - self.group_button.setStyleSheet("font-weight: bold; background-color: #c4f7c4;") - else: - self.group_button.setStyleSheet("font-weight: bold;") - - # --------------------------------------------------------- - # Vorauswahl des Layers "Verfahrensgebiet" - # --------------------------------------------------------- - def preselect_verfahrensgebiet_layer(self): - project = QgsProject.instance() - - # ✅ zuerst gespeicherten Layer wiederherstellen - saved_layer_id = project.customVariables().get("sn_verfahrensgebiet_layer", None) - if saved_layer_id: - layer = project.mapLayer(saved_layer_id) - if layer: - self.layer_combo.setLayer(layer) - return - - # ✅ sonst nach Namen suchen - for layer in project.mapLayers().values(): - if "verfahrensgebiet" in layer.name().lower(): - self.layer_combo.setLayer(layer) - return - - # ✅ Fallback: erster Layer - if self.layer_combo.count() > 0: - self.layer_combo.setCurrentIndex(0) - - # --------------------------------------------------------- - # Werte wiederherstellen - # --------------------------------------------------------- - def restore_saved_values(self): - project = QgsProject.instance() - vars = project.customVariables() - - # ✅ Verfahrens-DB - saved_db = vars.get("sn_verfahrens_db", None) - if saved_db and os.path.exists(saved_db): - self.verfahrens_db = saved_db - self.file_widget.setFilePath(saved_db) - self.update_group_button_color() - - # ✅ Linkliste - saved_link = vars.get("sn_linkliste", None) - if saved_link and os.path.exists(saved_link): - self.lokale_linkliste = saved_link - self.linkliste_widget.setFilePath(saved_link) - def create_new_gpkg(self): - """Öffnet einen Save-Dialog und legt eine neue GPKG-Datei an.""" - file_path, _ = QFileDialog.getSaveFileName( - self, - "Neue Verfahrens-Datenbank anlegen", - "", - "Geopackage (*.gpkg);;Alle Dateien (*)" - ) - - if not file_path: - return # Abbruch - - # Automatisch .gpkg anhängen - if not file_path.lower().endswith(".gpkg"): - file_path += ".gpkg" - - # Existiert Datei bereits? - if os.path.exists(file_path): - overwrite = QMessageBox.question( - self, - "Datei existiert bereits", - f"Die Datei existiert bereits:\n\n{file_path}\n\nSoll sie überschrieben werden?", - QMessageBox.Yes | QMessageBox.No, - QMessageBox.No - ) - if overwrite != QMessageBox.Yes: - return - - # Datei anlegen - try: - open(file_path, "w").close() - except Exception as e: - QMessageBox.critical(self, "Fehler", f"Die Datei konnte nicht angelegt werden:\n{e}") - return - - # Datei übernehmen - self.verfahrens_db = file_path - self.file_widget.setFilePath(file_path) - self.update_group_button_color() - - QMessageBox.information(self, "Projekt-DB angelegt", f"Neue Projekt-Datenbank wurde angelegt:\n{file_path}")