From 0900c6d3b010f90c8f2f7634c895bb51b69e4093 Mon Sep 17 00:00:00 2001 From: daniel Date: Fri, 10 Apr 2026 22:56:34 +0200 Subject: [PATCH] =?UTF-8?q?neues=20Layout=20"Baufreigabe"=20hinzugef=C3=BC?= =?UTF-8?q?gt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/Dateipruefer.py | 22 +- modules/Pruefmanager.py | 45 ++-- modules/print_layout_baufreigabe.py | 365 ++++++++++++++++++++++++++ modules/print_logic.py | 392 +++++++++++++++++++++++++++- modules/stilpruefer.py | 9 +- ui/tabs/print_tab.py | 195 +++++++++++++- 6 files changed, 975 insertions(+), 53 deletions(-) create mode 100644 modules/print_layout_baufreigabe.py diff --git a/modules/Dateipruefer.py b/modules/Dateipruefer.py index 552c558..84cfb62 100644 --- a/modules/Dateipruefer.py +++ b/modules/Dateipruefer.py @@ -46,7 +46,7 @@ class Dateipruefer: standarddatei: Optional[str] = None, temporaer_erlaubt: bool = False, *, - verfahrens_db_modus: bool = True, # 🆕 Verfahrens-DB-spezifische Logik + verfahrens_db_modus: bool = False, ) -> None: """ Parameters @@ -90,6 +90,8 @@ class Dateipruefer: def _pfad(self, relativer_pfad: str) -> Path: """Erzeugt OS-unabhängigen Pfad relativ zum Basisverzeichnis.""" + if relativer_pfad.startswith(("/", "\\")): + return Path(relativer_pfad) return join_path(self.basis_pfad, relativer_pfad) @@ -152,6 +154,22 @@ class Dateipruefer: #Excel-dateien erkennen dateityp = self.erkenne_dateityp(pfad) + if not self.verfahrens_db_modus: + if not file_exists(pfad): + return pruef_ergebnis( + ok=False, + meldung=f"Datei '{self.pfad}' wurde nicht gefunden.", + aktion="datei_nicht_gefunden", + kontext=pfad, + ) + + return pruef_ergebnis( + ok=True, + meldung="Datei ist gültig.", + aktion="ok", + kontext=pfad, + ) + if dateityp == "excel": if not file_exists(pfad): return pruef_ergebnis( @@ -176,7 +194,7 @@ class Dateipruefer: ) # 🆕 2.c: Ungültiger GPKG-Pfad? - if not self.verfahrens_db_modus or not self._ist_gueltiger_gpkg_pfad(pfad): + if not self._ist_gueltiger_gpkg_pfad(pfad): return pruef_ergebnis( ok=False, meldung=f"Der Pfad '{self.pfad}' ist kein gültiger GPKG-Pfad.", diff --git a/modules/Pruefmanager.py b/modules/Pruefmanager.py index 1bb131f..1b1fdb1 100644 --- a/modules/Pruefmanager.py +++ b/modules/Pruefmanager.py @@ -6,8 +6,8 @@ from __future__ import annotations from typing import Optional, Any from sn_basis.functions import ask_yes_no, info, warning, error, ask_overwrite_append_cancel_custom +from sn_basis.functions.ly_visibility_wrapper import set_layer_visible from sn_basis.modules.pruef_ergebnis import pruef_ergebnis, PruefAktion -print("DEBUG: Pruefmanager DATEI GELADEN:", __file__) class Pruefmanager: def __init__(self, ui_modus: str = "qgis", parent: Optional[Any] = None) -> None: @@ -146,10 +146,7 @@ class Pruefmanager: # ------------------------------------------------------------------ def _handle_basic_decision(self, ergebnis: pruef_ergebnis) -> pruef_ergebnis: """Basis-Entscheidung für einfache Ja/Nein-Fragen.""" - print(f"DEBUG _handle_basic_decision: aktion='{ergebnis.aktion}', ui_modus='{self.ui_modus}'") - if self.ui_modus != "qgis": - print("DEBUG: Nicht QGIS → ergebnis unverändert") return ergebnis title_map = { @@ -163,27 +160,32 @@ class Pruefmanager: meldung = ergebnis.meldung or "" try: - print(f"DEBUG ask_yes_no: title='{title}', meldung='{meldung[:50]}...'") yes = ask_yes_no(title, meldung, default=False, parent=self.parent) - print(f"DEBUG ask_yes_no: yes={yes}") - except Exception as e: - print(f"DEBUG ask_yes_no Exception: {e}") + except Exception: return ergebnis if not yes: - print("DEBUG: Nutzer sagte Nein → ok=False") return ergebnis # Nutzer sagte Ja if ergebnis.aktion == "temporaer_erlaubt": - print("DEBUG: temporaer_erlaubt bestätigt → ok=True") return pruef_ergebnis( ok=True, - aktion="temporaer_erlaubt", + aktion="temporaer_erzeugen", kontext=ergebnis.kontext ) - print("DEBUG: Andere Aktion bestätigt → ok=True, aktion='ok'") + if ergebnis.aktion == "layer_unsichtbar": + try: + set_layer_visible(ergebnis.kontext, True) + except Exception: + return ergebnis + return pruef_ergebnis( + ok=True, + aktion="ok", + kontext=ergebnis.kontext, + ) + return pruef_ergebnis( ok=True, aktion="ok", @@ -194,48 +196,31 @@ class Pruefmanager: # Hauptlogik: verarbeite() (KORRIGIERT!) # ------------------------------------------------------------------ def verarbeite(self, ergebnis: pruef_ergebnis) -> pruef_ergebnis: - print("🔥 verarbeite() START") - print("DEBUG Pruefmanager:", ergebnis.ok, ergebnis.aktion) - print("DEBUG ergebnis.aktion TYPE:", type(ergebnis.aktion), repr(ergebnis.aktion)) - # 1. Erfolg → direkt weiter - print("🔍 Schritt 1: Prüfe ergebnis.ok =", ergebnis.ok) if ergebnis.ok: - print("✅ Schritt 1: ok=True → return") return ergebnis # 2. VERFAHRENS-DB: Bestehende Datei - print("🔍 Schritt 2: Prüfe datei_existiert =", ergebnis.aktion == "datei_existiert") if ergebnis.aktion == "datei_existiert": - print("✅ Schritt 2: _handle_datei_existiert") return self._handle_datei_existiert(ergebnis) # 3. Basis interaktive Aktionen - print("🔍 Schritt 3: Definiere interactive_actions") interactive_actions = { "leereingabe_erlaubt", "standarddatei_vorschlagen", "temporaer_erlaubt", "layer_unsichtbar", } - print("DEBUG interactive_actions:", repr(interactive_actions)) - print("DEBUG ergebnis.aktion in interactive_actions?", ergebnis.aktion in interactive_actions) - if ergebnis.aktion in interactive_actions: - print("✅ Schritt 3: Interaktive Aktion → _handle_basic_decision") - decision = self._handle_basic_decision(ergebnis) - print(f"DEBUG: _handle_basic_decision Ergebnis: ok={decision.ok}, aktion='{decision.aktion}'") - return decision + return self._handle_basic_decision(ergebnis) # 4. Fehler behandeln - print("❌ Schritt 4: FEHLER BEHANDELN") self.report_error( thema=ergebnis.aktion or "pruefung", meldung=ergebnis.meldung or "", aktion=ergebnis.aktion, kontext=ergebnis.kontext, ) - print("🔥 verarbeite() ENDE mit ok=False") return ergebnis def _ask_use_or_replace_pufferlayer(self) -> str: diff --git a/modules/print_layout_baufreigabe.py b/modules/print_layout_baufreigabe.py new file mode 100644 index 0000000..ecdce44 --- /dev/null +++ b/modules/print_layout_baufreigabe.py @@ -0,0 +1,365 @@ +""" +sn_basis/modules/print_layout_baufreigabe.py - Layout-Erstellung fuer Baufreigabe-Vorlagen. + +Baufreigabe-Layouts verwenden immer feste DIN A3/A4 Seitengroessen im Querformat, +ein 120 mm breites Kartenschild (statt 180 mm) und erbenerben die Standard-Logik +fuer Hauptkarte, Quellenangabe und Legende vom PrintLayout. + +Atlas-Integration erfolgt nach Layouterstellung durch externe Konfiguration +in der PrintLogic. +""" +from __future__ import annotations + +import math +from typing import Any + +from sn_basis.functions.qt_wrapper import QFont +from sn_basis.functions.qgiscore_wrapper import ( + QgsLayoutItem, + QgsLayoutItemLabel, + QgsLayoutItemLegend, + QgsLayoutItemMap, + QgsLayoutPoint, + QgsLayoutSize, + QgsPrintLayout, + QgsProject, + QgsUnitTypes, +) +from sn_basis.functions.qgisui_wrapper import open_layout_designer +from sn_basis.modules.print_layout import PrintLayout + + +MM = QgsUnitTypes.LayoutMillimeters + + +class PrintLayoutBaufreigabe(PrintLayout): + """Erzeugt QGIS-Einzelblatt-Layouts mit fester DIN A3/A4 Querformat-Groesse und 120mm Kartenschild.""" + + _FRAME_STROKE_WIDTH_MM = 0.5 + _KARTENSCHILD_WIDTH_MM = 120.0 # Unterschied zu StandardLayout: 120mm statt 180mm + _KARTENSCHILD_HEIGHT_MM = 92.0 + _GRID_INTERVAL_MAP_UNITS = 500.0 + + # DIN-Formate: nur A3 und A4 im Querformat + _DIN_A3_LANDSCAPE = (420, 297) # Breite x Hoehe (Querformat) + _DIN_A4_LANDSCAPE = (297, 210) # Querformat + + _TEXT_FORMAT_TEMPLATES = { + "Format_1": {"font_family": "Arial", "font_size": 11, "font_style": "normal"}, + "Format_2": {"font_family": "Arial", "font_size": 13, "font_style": "bold"}, + "Format_3": {"font_family": "Arial", "font_size": 16, "font_style": "bold"}, + } + + _OBJECT_TEMPLATES = { + "hauptkarte": {"id": "Hauptkarte", "object_name": "Hauptkarte"}, + "quellenangabe": { + "id": "Quellenangabe", + "object_name": "Quellenangabe", + "size_mm": (120.0, 100.0), # Breite angepasst auf 120mm Kartenschild + "format": "Format_1", + }, + "textfeld": { + "id": "Textfeld", + "object_name": "Textfeld", + "size_mm": (60.0, 8.0), + "format": "Format_1", + "html_mode": True, + "text": "", + }, + "legende": { + "id": "Legende", + "object_name": "Legende", + "size_mm": (120.0, 60.0), # Breite angepasst auf 120mm Kartenschild + }, + } + + _QUELLENANGABE_TEXT = ( + "Quelle Geobasisdaten: GeoSN, " + "dl-de/by-2-0
" + "Quelle Fachdaten: Darstellung auf der Grundlage von Daten und mit Erlaubnis des " + "Saechsischen Landesamtes fuer Umwelt, Landwirtschaft und Geologie
" + "Basemap:" + "© GeoBasis-DE / BKG ([%year($now)%]) " + "CC BY 4.0 " + "mit teilweise angepasster Signatur
" + ) + + def __init__(self, project: Any | None = None) -> None: + super().__init__(project) + + def create_baufreigabe_layout( + self, + name: str, + din_format: str, + map_width_mm: float, + map_height_mm: float, + extent: Any, + plotmassstab: float, + thema: str = "", + fit_extent_to_map: bool = True, + ) -> Any: + """Erstellt ein Baufreigabe-Layout mit fester DIN A3/A4 Querformat-Groesse. + + Args: + name: Name des Layouts + din_format: "DIN A3" oder "DIN A4" + map_width_mm: Breite der Hauptkarte + map_height_mm: Hoehe der Hauptkarte + extent: QgsRectangle der Kartenausdehnung + plotmassstab: Plotmassstab (z.B. 5000 fuer 1:5000) + thema: Name des Visibility-Presets (optional) + fit_extent_to_map: Ob das Extent an die Kartengroe angepasst wird + + Returns: + QgsPrintLayout objektInstance (wird im Layout-Dialog geoeffnet) + + Raises: + ValueError: Falls Name bereits existiert oder Format ungueltig ist + """ + # Seitenformat auswaehlen (nur A3/A4 im Querformat) + if din_format == "DIN A3": + page_width_mm, page_height_mm = self._DIN_A3_LANDSCAPE + elif din_format == "DIN A4": + page_width_mm, page_height_mm = self._DIN_A4_LANDSCAPE + else: + raise ValueError(f"Baufreigabe: Format {din_format} ist nicht unterstuetzt. Nur DIN A3/A4 Querformat.") + + layout_manager = self.project.layoutManager() + existing_layout = layout_manager.layoutByName(name) + if existing_layout is not None: + raise ValueError(f"Eine Vorlage mit der Bezeichnung '{name}' existiert bereits.") + + layout = QgsPrintLayout(self.project) + layout.initializeDefaults() + layout.setName(name) + self._current_layout = layout + + page = layout.pageCollection().page(0) + page.setPageSize(QgsLayoutSize(page_width_mm, page_height_mm, MM)) + + map_left_mm = 10.0 + map_top_mm = 10.0 + map_right_mm = map_left_mm + map_width_mm + map_bottom_mm = map_top_mm + map_height_mm + kartenfenster_untere_rechte_ecke_mm = (map_right_mm, map_bottom_mm) + + hauptkarte = self._create_map_item( + layout, + "hauptkarte", + map_left_mm, + map_top_mm, + map_width_mm, + map_height_mm, + ) + + fitted_extent = extent + if fit_extent_to_map: + fitted_extent = self._fit_extent_to_map_item(extent, map_width_mm, map_height_mm) + if fitted_extent is not None and hasattr(fitted_extent, "isNull") and callable(fitted_extent.isNull) and not fitted_extent.isNull(): + try: + hauptkarte.setExtent(fitted_extent) + except Exception: + pass + + if isinstance(plotmassstab, (int, float)) and math.isfinite(plotmassstab) and plotmassstab > 0: + try: + hauptkarte.setScale(plotmassstab) + except Exception: + pass + + set_frame_enabled = getattr(hauptkarte, "setFrameEnabled", None) + if callable(set_frame_enabled): + try: + set_frame_enabled(True) + except Exception: + pass + + set_frame_stroke_width = getattr(hauptkarte, "setFrameStrokeWidth", None) + if callable(set_frame_stroke_width): + try: + self._set_item_frame_stroke_width(hauptkarte, self._FRAME_STROKE_WIDTH_MM) + except Exception: + pass + + if thema and thema != "aktuell": + follow_theme = getattr(hauptkarte, "setFollowVisibilityPreset", None) + set_theme_name = getattr(hauptkarte, "setFollowVisibilityPresetName", None) + if callable(follow_theme): + try: + follow_theme(True) + except Exception: + pass + if callable(set_theme_name): + try: + set_theme_name(thema) + except Exception: + pass + + set_keep_layer_set = getattr(hauptkarte, "setKeepLayerSet", None) + if callable(set_keep_layer_set): + try: + set_keep_layer_set(True) + except Exception: + pass + + set_refresh_strategy = getattr(hauptkarte, "setRefreshStrategy", None) + if callable(set_refresh_strategy): + refresh_cache = ( + getattr(hauptkarte, "RefreshLaterOnly", None) + or getattr(hauptkarte, "RefreshWhenRequested", None) + or 0 + ) + try: + set_refresh_strategy(refresh_cache) + except Exception: + pass + + self._configure_hauptkarte_grids(hauptkarte) + + self._setup_layout_objects_baufreigabe( + kartenfenster_untere_rechte_ecke_mm, + hauptkarte, + map_top_mm, + map_height_mm, + ) + + layout_manager.addLayout(layout) + self._current_layout = None + open_layout_designer(layout) + return layout + + def _setup_layout_objects_baufreigabe( + self, + kartenfenster_untere_rechte_ecke_mm: tuple, + hauptkarte: Any, + map_top_mm: float, + map_height_mm: float, + ) -> None: + """Erstellt Legende, Quellenangabe und Kartenschild fuer Baufreigabe-Layout. + + Angepasst fuer 120mm Kartenschild statt 180mm, mit proportional skalierten Textfeldern. + """ + if self._current_layout is None: + raise RuntimeError("Kein Layout aktiv.") + + def _coords_rel_untere_rechte_ecke(rel_x_mm: float, rel_y_mm: float) -> tuple[float, float]: + return ( + kartenfenster_untere_rechte_ecke_mm[0] + rel_x_mm, + kartenfenster_untere_rechte_ecke_mm[1] + rel_y_mm, + ) + + # Legende: 120mm Breite (angepasst), hoehe adaptive wie Standard + legende_x = kartenfenster_untere_rechte_ecke_mm[0] + 10.0 + legende_height_mm = max(map_height_mm - 122.0, 20.0) + legende_y = map_top_mm + legende = self._create_legend_item( + self._current_layout, + "legende", + hauptkarte, + legende_x, + legende_y, + width_mm=self._KARTENSCHILD_WIDTH_MM, + height_mm=legende_height_mm, + object_name="Legende", + ) + + legende_hoehe_mm = self._configure_legend_size( + legende, + width_limit_mm=self._KARTENSCHILD_WIDTH_MM, + column_count=1, + equal_column_width=False, + ) + + # Quellenangabe: 120mm Breite + quellenangabe_x, quellenangabe_y = _coords_rel_untere_rechte_ecke(10.0, -102.0) + quellenangabe_hoehe_mm = 20.0 + quellenangabe_obere_y = quellenangabe_y - quellenangabe_hoehe_mm + legende_untere_y = legende_y + legende_hoehe_mm + + if legende_untere_y > quellenangabe_obere_y: + self._configure_legend_size( + legende, + width_limit_mm=self._KARTENSCHILD_WIDTH_MM, + column_count=2, + equal_column_width=True, + ) + + self.add_text_label( + self._QUELLENANGABE_TEXT, + quellenangabe_x, + quellenangabe_y, + width_mm=self._KARTENSCHILD_WIDTH_MM, + height_mm=20.0, + text_format="Format_1", + html_mode=True, + expression_enabled=True, + object_name="Quellenangabe", + ) + + # Kartenschild: 120mm Breite (angepasst) + kartenschild_x, kartenschild_y = _coords_rel_untere_rechte_ecke(10.0, 0.0) + self._create_rectangle_box_item( + self._current_layout, + "Kartenschild", + kartenschild_x, + kartenschild_y, + self._KARTENSCHILD_WIDTH_MM, + self._KARTENSCHILD_HEIGHT_MM, + stroke_width_mm=self._FRAME_STROKE_WIDTH_MM, + ) + + # Kartenschild-Linien: 6 Trennlinien (Positionen gleich) + kartenschild_line_x, _ = _coords_rel_untere_rechte_ecke(10.0, 0.0) + kartenschild_line_offsets_y = (-10.0, -20.0, -30.0, -46.0, -62.0, -82.0) + for index, offset_y in enumerate(kartenschild_line_offsets_y, start=1): + _, line_y = _coords_rel_untere_rechte_ecke(0.0, offset_y) + self._create_rectangle_box_item( + self._current_layout, + f"Kartenschild_Linie_{index}", + kartenschild_line_x, + line_y, + self._KARTENSCHILD_WIDTH_MM, + 0.0, + stroke_width_mm=0.1, + ) + + # Textfelder im Kartenschild: proportional skaliert auf 120mm + # Skalierungsfaktor: 120/180 = 0.667 fuer horizontale Abstaende/Breiten + scale_x = self._KARTENSCHILD_WIDTH_MM / 180.0 + base_x = 16.0 * scale_x # ~10.7mm statt 16mm + label_width_scaled = 101.0 * scale_x # ~67.3mm statt 101mm + + labels = [ + ("[% @sn_amt %]", base_x, -84.0, label_width_scaled, 4.0, "Format_1", "Herausgeber"), + ("[% @sn_bezeichnung %]", base_x, -75.0, label_width_scaled, 4.0, "Format_1", "Bezeichnung_Flurbereinigung"), + ("[% @sn_name %]", base_x, -64.0, label_width_scaled, 5.0, "Format_2", "Name_Flurbereinigung"), + ("[% @sn_gemeinden %]", base_x, -56.0, label_width_scaled, 4.0, "Format_1", "Stadt_Gemeinde"), + ("[% @sn_landkreise_proj %]", base_x, -48.0, label_width_scaled, 4.0, "Format_1", "Landkreis"), + ("[% @sn_kartenname %]", base_x, -32.0, label_width_scaled - 30, 12.0, "Format_3", "Kartenname"), + ("Gefertigt: ", base_x, -23.0, 40.0 * scale_x, 4.0, "Format_1", "Gefertigt"), + ("Geprueft: ", base_x + 40.0 * scale_x + 5.0, -23.0, 40.0 * scale_x, 4.0, "Format_1", "Geprueft"), + ( + "Massstab: 1 : [%round(map_get(item_variables('Hauptkarte'),'map_scale'),0)%]", + base_x, + -13.0, + 40.0 * scale_x, + 4.0, + "Format_1", + "Massstab", + ), + ("Ausgabedatum: [%format_date(now(), 'dd.MM.yyyy')%]", base_x + 40.0 * scale_x + 5.0, -13.0, 40.0 * scale_x, 4.0, "Format_1", "Ausgabedatum"), + ("Aktenzeichen: ", base_x, -3.0, label_width_scaled, 4.0, "Format_1", "Aktenzeichen"), + ] + + for text, rx, ry, w, h, fmt, obj_name in labels: + x, y = _coords_rel_untere_rechte_ecke(rx, ry) + self.add_text_label( + text, + x, + y, + width_mm=w, + height_mm=h, + text_format=fmt, + expression_enabled=True, + object_name=obj_name, + ) diff --git a/modules/print_logic.py b/modules/print_logic.py index c203158..4624f7b 100644 --- a/modules/print_logic.py +++ b/modules/print_logic.py @@ -4,7 +4,7 @@ sn_basis/modules/print_logic.py - Fachlogik fuer den Druck-Tab (nur Einzelblatt) from __future__ import annotations import importlib -from typing import Any, cast +from typing import Any, cast, TypedDict from sn_basis.functions.qgiscore_wrapper import QgsProject, get_layer_extent from sn_basis.functions.qgisui_wrapper import iface @@ -12,6 +12,48 @@ from sn_basis.functions.variable_wrapper import get_variable, set_variable from sn_basis.modules.layerpruefer import Layerpruefer from sn_basis.modules.Pruefmanager import Pruefmanager from sn_basis.modules.print_layout import PrintLayout +from sn_basis.modules.print_layout_baufreigabe import PrintLayoutBaufreigabe + + +class AtlasConfig(TypedDict, total=False): + """Strukturierte Konfiguration fuer Atlas-Optionen. + + Alle Felder sind optional (total=False) und haben vordefinierte Standartwerte. + """ + # Basis-Konfiguration + enabled: bool # Atlas aktivieren (Standard: True nach Coverage-Layer-Setzung) + page_name_expression: str # QGIS Expression für Seitennamen (z.B. "@atlas_featurenumber") + + # Filterung + filter_features: bool # Features filtern? (Standard: False) + filter_expression: str # QGIS Expression für Feature-Filter + + # Sortierung + sort_features: bool # Features sortieren? (Standard: False) + sort_expression: str # QGIS Expression/Feldname zum Sortieren + sort_ascending: bool # Aufsteigend sortieren? (Standard: True) + + # Rendering + hide_coverage: bool # Coverage-Layer in Layout verstecken? (Standard: False) + limit_render_to_current_feature: bool # Rendering auf aktuelles Feature limitieren (QGIS 4.0+, Standard: False) + + # Metadaten + description: str # Beschreibung der Atlas-Konfiguration + + +# Vordefinierte Standardkonfigurationen für Baufreigabe +ATLAS_CONFIG_BAUFREIGABE_DEFAULT: AtlasConfig = { + "enabled": True, + "page_name_expression": "@atlas_featurenumber", + "filter_features": False, + "filter_expression": "", + "sort_features": False, + "sort_expression": "", + "sort_ascending": True, + "hide_coverage": False, + "limit_render_to_current_feature": False, + "description": "Baufreigabe Atlas mit Standardkonfiguration", +} KARTENNAME_VAR = "kartenname" @@ -25,6 +67,7 @@ DRUCKRECHTECK_LAYER_NAME = "Druckrechteck" KARTENNAME_38 = "§38" KARTENNAME_41 = "§41" +KARTENNAME_BAUFREIGABE = "Anlage zur Baufreigabe" MASSSTAB_WIE_KARTENFENSTER = "Wie Kartenfenster" THEMA_WIE_KARTENFENSTER = "wie kartenfenster" @@ -35,6 +78,7 @@ DRUCKBEREICH_RECHTECK = "Rechteck" KARTENNAME_BY_AUSWAHL = { KARTENNAME_38: "Planungsübersicht §38 FlurbG", KARTENNAME_41: "Karte zum Plan über die gemeinschaftlichen und öffentlichen Anlagen (§ 41 FlurbG)", + KARTENNAME_BAUFREIGABE: "Anlage zur Baufreigabe", } PLOTMASSSTAB_BY_AUSWAHL = { @@ -253,6 +297,269 @@ class PrintLogic: except Exception: pass + def create_atlas_layer_from_selection(self, source_layer: object | None = None) -> tuple[object | None, str]: + """Erstellt einen neuen Layer 'Atlaslayer' aus der Auswahl des Quell-Layers. + + Args: + source_layer: Der Layer mit der Auswahl (wenn None, wird aktiver Layer verwendet) + + Returns: + (created_layer, layer_id) oder (None, "") bei Fehler + """ + # Source-Layer bestimmen + if source_layer is None: + source_layer = self._resolve_active_layer() + if source_layer is None: + self.pruefmanager.zeige_hinweis( + "Atlas Layer erstellen", + "Kein Layer mit Auswahl gefunden.", + ) + return None, "" + + # Pruefen ob Auswahl vorhanden ist + selected_count = getattr(source_layer, "selectedFeatureCount", None) + if callable(selected_count): + try: + if int(cast(Any, selected_count())) == 0: + self.pruefmanager.zeige_hinweis( + "Atlas Layer erstellen", + "Auf dem Layer sind keine Features ausgewaehlt.", + ) + return None, "" + except Exception: + pass + + # Bestehenden Atlaslayer loeschen (optional Wiederverwedung) + existing_atlas = self._get_existing_layer_by_name("Atlaslayer") + if existing_atlas is not None: + try: + QgsProject.instance().removeMapLayer(existing_atlas) + except Exception: + pass + + # Neuen Memory-Layer mit Geometrie-Type des Quell-Layers erstellen + qgs_vector_layer = self._import_qgis_symbol("qgis.core", "QgsVectorLayer") + if qgs_vector_layer is None: + self.pruefmanager.zeige_hinweis( + "Atlas Layer erstellen", + "QgsVectorLayer ist nicht verfuegbar.", + ) + return None, "" + + # CRS vom Quell-Layer übernehmen + crs_authid = "EPSG:25832" + try: + source_crs = getattr(source_layer, "crs", lambda: None)() + if source_crs is not None: + crs_authid_method = getattr(source_crs, "authid", lambda: None) + if callable(crs_authid_method): + crs_authid = crs_authid_method() or crs_authid + except Exception: + pass + + # Geometrie-Type vom Quell-Layer bestimmen (vereinfacht: Polygon für Atlas) + # TODO: Intelligentere Bestimmung des Geometrie-Types + atlas_layer = qgs_vector_layer(f"Polygon?crs={crs_authid}", "Atlaslayer", "memory") + if not atlas_layer or not getattr(atlas_layer, "isValid", lambda: False)(): + self.pruefmanager.zeige_hinweis( + "Atlas Layer erstellen", + "Der Atlaslayer konnte nicht erstellt werden.", + ) + return None, "" + + # Ausgewaehlte Features kopieren + try: + get_selected = getattr(source_layer, "selectedFeatures", None) + if not callable(get_selected): + raise Exception("selectedFeatures ist nicht aufrufbar") + + selected_features = list(cast(Any, get_selected())) + if not selected_features: + raise Exception("Keine Features ausgewaehlt") + + qgs_feature = self._import_qgis_symbol("qgis.core", "QgsFeature") + if qgs_feature is None: + raise Exception("QgsFeature Symbol nicht verfuegbar") + + data_provider = getattr(atlas_layer, "dataProvider", lambda: None)() + if data_provider is None: + raise Exception("dataProvider nicht verfuegbar") + + new_features = [] + for source_feature in selected_features: + geometry = getattr(source_feature, "geometry", lambda: None)() + if geometry is None: + continue + new_feature = qgs_feature() + set_geometry = getattr(new_feature, "setGeometry", None) + if callable(set_geometry): + try: + set_geometry(geometry) + new_features.append(new_feature) + except Exception: + pass + + if new_features: + add_features = getattr(data_provider, "addFeatures", None) + if callable(add_features): + add_features(new_features) + + update_fields = getattr(atlas_layer, "updateFields", None) + if callable(update_fields): + update_fields() + + update_extents = getattr(atlas_layer, "updateExtents", None) + if callable(update_extents): + update_extents() + + except Exception as exc: + self.pruefmanager.zeige_hinweis( + "Atlas Layer erstellen", + f"Fehler beim Kopieren der Features: {exc}", + ) + return None, "" + + # Layer ins Projekt aufnehmen + try: + QgsProject.instance().addMapLayer(atlas_layer) + except Exception: + self.pruefmanager.zeige_hinweis( + "Atlas Layer erstellen", + "Der Layer konnte nicht ins Projekt aufgenommen werden.", + ) + return None, "" + + # Layer-ID speichern + layer_id = getattr(atlas_layer, "id", lambda: "")() or "" + set_variable("sn_abdeckungslayer", layer_id, scope="project") + + self.pruefmanager.zeige_hinweis( + "Atlas Layer erstellen", + f"Atlas Layer 'Atlaslayer' erfolgreich erstellt.", + ) + + return atlas_layer, layer_id + + def configure_atlas_for_baufreigabe_layout( + self, + layout_name: str = "", + atlas_config: AtlasConfig | None = None, + ) -> bool: + """Konfiguriert den Atlas fuer ein Baufreigabe-Layout mit erweiterten Optionen. + + Wird nach erfolgreicher Baufreigabe-Layout-Erstellung aufgerufen. + Setzt den Coverage-Layer (aus sn_abdeckungslayer Variable) und konfiguriert den Atlas + mit allen Optionen aus der AtlasConfig. + """ + try: + if atlas_config is None: + atlas_config = ATLAS_CONFIG_BAUFREIGABE_DEFAULT + + project = QgsProject.instance() + layout_manager = getattr(project, "layoutManager", lambda: None)() + if layout_manager is None: + self.pruefmanager.zeige_hinweis("Atlas konfigurieren", "Layout Manager nicht verfuegbar.") + return False + + if not layout_name: + layouts_fn = getattr(layout_manager, "layouts", None) + if not callable(layouts_fn): + return False + try: + layout_list = layouts_fn() + if not layout_list: + return False + layout = layout_list[-1] + except Exception: + return False + else: + layout_by_name = getattr(layout_manager, "layoutByName", None) + if not callable(layout_by_name): + return False + layout = layout_by_name(layout_name) + if layout is None: + return False + + atlas = getattr(layout, "atlas", lambda: None)() + if atlas is None: + return False + + coverage_layer_id = get_variable("sn_abdeckungslayer", scope="project") or "" + if not coverage_layer_id: + return True + + coverage_layer = project.mapLayer(coverage_layer_id) + if coverage_layer is None: + self.pruefmanager.zeige_hinweis( + "Atlas konfigurieren", + f"Atlas-Layer mit ID '{coverage_layer_id}' nicht gefunden.", + ) + return False + + set_coverage = getattr(atlas, "setCoverageLayer", None) + if callable(set_coverage): + set_coverage(coverage_layer) + + page_name_expr = atlas_config.get("page_name_expression", "") + if page_name_expr: + set_page_name = getattr(atlas, "setPageNameExpression", None) + if callable(set_page_name): + set_page_name(page_name_expr) + + if atlas_config.get("filter_features", False): + set_filter_features = getattr(atlas, "setFilterFeatures", None) + if callable(set_filter_features): + set_filter_features(True) + filter_expr = atlas_config.get("filter_expression", "") + if filter_expr: + set_filter_expr = getattr(atlas, "setFilterExpression", None) + if callable(set_filter_expr): + set_filter_expr(filter_expr) + + if atlas_config.get("sort_features", False): + set_sort_features = getattr(atlas, "setSortFeatures", None) + if callable(set_sort_features): + set_sort_features(True) + sort_expr = atlas_config.get("sort_expression", "") + if sort_expr: + set_sort_expr = getattr(atlas, "setSortExpression", None) + if callable(set_sort_expr): + set_sort_expr(sort_expr) + set_sort_asc = getattr(atlas, "setSortAscending", None) + if callable(set_sort_asc): + set_sort_asc(atlas_config.get("sort_ascending", True)) + + set_hide_coverage = getattr(atlas, "setHideCoverage", None) + if callable(set_hide_coverage): + set_hide_coverage(atlas_config.get("hide_coverage", False)) + + set_limit_render = getattr(atlas, "setLimitCoverageLayerRenderToCurrentFeature", None) + if callable(set_limit_render): + try: + set_limit_render(atlas_config.get("limit_render_to_current_feature", False)) + except Exception: + pass + + num_features = 0 + update_features = getattr(atlas, "updateFeatures", None) + if callable(update_features): + try: + num_features = update_features() or 0 + except Exception: + num_features = 0 + + set_enabled = getattr(atlas, "setEnabled", None) + if callable(set_enabled): + set_enabled(atlas_config.get("enabled", True)) + return True + + except Exception as exc: + self.pruefmanager.zeige_hinweis( + "Atlas konfigurieren", + f"Fehler beim Konfigurieren des Atlas: {exc}", + ) + return False + def _store_druckrechteck(self, layer: object, rectangle: Any) -> bool: qgs_feature = self._import_qgis_symbol("qgis.core", "QgsFeature") qgs_geometry = self._import_qgis_symbol("qgis.core", "QgsGeometry") @@ -579,6 +886,43 @@ class PrintLogic: auswahl = DRUCKBEREICH_VERFAHRENSGEBIET set_variable(DRUCKBEREICH_VAR, auswahl, scope="project") + def _get_baufreigabe_page_dimensions( + self, + kartenbild_w: float, + kartenbild_h: float, + zielgroesse: str, + ) -> tuple[str, float, float, float, float]: + """Berechnet Seitenformat und verfuegbare Kartenfenster fuer Baufreigabe. + + Args: + kartenbild_w: Berechnete Kartenbild-Breite aus Extent/Massstab + kartenbild_h: Berechnete Kartenbild-Hoehe aus Extent/Massstab + zielgroesse: "DIN A3" oder "DIN A4" (wird zu Querformat konvertiert) + + Returns: + (din_format, page_width_mm, page_height_mm, adjusted_map_w, adjusted_map_h) + """ + # Nur A3/A4 im Querformat fuer Baufreigabe + if zielgroesse == "DIN A3": + din_format = "DIN A3" + page_w, page_h = 420.0, 297.0 + else: + din_format = "DIN A4" + page_w, page_h = 297.0, 210.0 + + # Verfuegbare Flaeche fuer Kartenbild: Seite - Kartenschild und Maerchen + kartenschild_w = 120.0 + kartenschild_h = 92.0 + margin = 10.0 + + avail_w = page_w - 2 * margin - kartenschild_w - margin + avail_h = page_h - 2 * margin - kartenschild_h - margin + + adjusted_map_w = min(kartenbild_w, avail_w) + adjusted_map_h = min(kartenbild_h, avail_h) + + return din_format, page_w, page_h, adjusted_map_w, adjusted_map_h + def druckvorlage_anlegen( self, layer: object | None, @@ -590,7 +934,7 @@ class PrintLogic: ) -> dict: resolved_layer = layer or self._resolve_layer_from_project() - if kartenname_auswahl not in (KARTENNAME_38, KARTENNAME_41): + if kartenname_auswahl not in (KARTENNAME_38, KARTENNAME_41, KARTENNAME_BAUFREIGABE): self.pruefmanager.zeige_hinweis("Kartennamen waehlen", "Kartennamen waehlen") return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0} @@ -671,18 +1015,36 @@ class PrintLogic: vorlage_name = (vorlage_name or "").strip() or kartenname + # Verzweigung: Baufreigabe vs. Standard-Layout try: - PrintLayout().create_single_page_layout( - name=vorlage_name, - page_width_mm=ziel_w, - page_height_mm=ziel_h, - map_width_mm=kartenbild_w, - map_height_mm=kartenbild_h, - extent=extent, - plotmassstab=massstab_zahl, - thema=thema, - fit_extent_to_map=(druckbereich_auswahl != DRUCKBEREICH_VERFAHRENSGEBIET), - ) + if kartenname_auswahl == KARTENNAME_BAUFREIGABE: + # Baufreigabe: feste DIN A3/A4 Querformat-Logik mit 120mm Kartenschild + din_format, page_w, page_h, map_w, map_h = self._get_baufreigabe_page_dimensions( + kartenbild_w, kartenbild_h, zielgroesse + ) + PrintLayoutBaufreigabe().create_baufreigabe_layout( + name=vorlage_name if vorlage_name != kartenname else "Baufreigabe", + din_format=din_format, + map_width_mm=map_w, + map_height_mm=map_h, + extent=extent, + plotmassstab=massstab_zahl, + thema=thema, + fit_extent_to_map=(druckbereich_auswahl != DRUCKBEREICH_VERFAHRENSGEBIET), + ) + else: + # Standard-Layout: dynamische Groessenanpassung + PrintLayout().create_single_page_layout( + name=vorlage_name, + page_width_mm=ziel_w, + page_height_mm=ziel_h, + map_width_mm=kartenbild_w, + map_height_mm=kartenbild_h, + extent=extent, + plotmassstab=massstab_zahl, + thema=thema, + fit_extent_to_map=(druckbereich_auswahl != DRUCKBEREICH_VERFAHRENSGEBIET), + ) except ValueError as exc: self.pruefmanager.zeige_hinweis("Vorlage anlegen", str(exc)) return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0} @@ -693,4 +1055,8 @@ class PrintLogic: ) return {"ok": False, "switch_to_tab_a": False, "atlas_seiten": 0} + # Nach erfolgreicher Layout-Erstellung: Atlas konfigurieren (falls Baufreigabe) + if kartenname_auswahl == KARTENNAME_BAUFREIGABE: + self.configure_atlas_for_baufreigabe_layout() + return {"ok": True, "switch_to_tab_a": False, "atlas_seiten": 1} diff --git a/modules/stilpruefer.py b/modules/stilpruefer.py index f8b743d..cdb601f 100644 --- a/modules/stilpruefer.py +++ b/modules/stilpruefer.py @@ -5,6 +5,7 @@ Die Anwendung erfolgt später über eine Aktion. """ import os +from pathlib import Path from sn_basis.functions.os_wrapper import is_absolute_path from sn_basis.functions.sys_wrapper import get_plugin_root, file_exists, join_path @@ -43,7 +44,7 @@ class Stilpruefer: pfad = str(stil_pfad) - if not is_absolute_path(pfad): + if not is_absolute_path(pfad) and not pfad.startswith(("/", "\\")): plugin_root = get_plugin_root() pfad = str(join_path(plugin_root, "sn_plan41", "assets", pfad)) @@ -55,7 +56,7 @@ class Stilpruefer: ok=False, meldung=f"Die Stil-Datei '{stil_pfad}' wurde nicht gefunden.", aktion="datei_nicht_gefunden", - kontext=pfad, + kontext=Path(pfad), ) # ----------------------------------------------------- @@ -66,7 +67,7 @@ class Stilpruefer: ok=False, meldung="Die Stil-Datei muss die Endung '.qml' haben.", aktion="falsche_endung", - kontext=pfad, + kontext=Path(pfad), ) # ----------------------------------------------------- @@ -76,5 +77,5 @@ class Stilpruefer: ok=True, meldung="Stil-Datei ist gültig.", aktion="stil_anwendbar", - kontext=pfad, + kontext=Path(pfad), ) diff --git a/ui/tabs/print_tab.py b/ui/tabs/print_tab.py index 8884cd6..8d19eb4 100644 --- a/ui/tabs/print_tab.py +++ b/ui/tabs/print_tab.py @@ -13,9 +13,13 @@ from sn_basis.functions.qt_wrapper import ( QCheckBox, QHBoxLayout, QPushButton, + QToolButton, + ToolButtonTextBesideIcon, + ArrowDown, + ArrowRight, ) -from sn_basis.functions.qgiscore_wrapper import QgsProject -from sn_basis.functions.qgisui_wrapper import iface +from sn_basis.functions.qgiscore_wrapper import QgsProject, QgsMapLayerProxyModel +from sn_basis.functions.qgisui_wrapper import iface, QgsMapLayerComboBox from sn_basis.functions.variable_wrapper import get_variable, set_variable from sn_basis.modules.Pruefmanager import Pruefmanager from sn_basis.modules.DataGrabber import DataGrabber @@ -39,6 +43,7 @@ KARTENNAME_VAR = "tab_b_kartenname" KARTENNAME_PLACEHOLDER = "Kartenname waehlen" KARTENNAME_38 = "§38" KARTENNAME_41 = "§41" +KARTENNAME_BAUFREIGABE = "Anlage zur Baufreigabe" MASSSTAB_VAR = "tab_b_massstab" THEMA_VAR = "tab_b_thema" ZIELGROESSE_UI_VAR = "tab_b_zielgroesse" @@ -70,6 +75,10 @@ class PrintTab(QWidget): self._endlosrolle_cb: Optional[Any] = None self._druckpuffer_button: Optional[Any] = None self._btn_vorlage_erstellen: Optional[Any] = None + self._atlas_layer_combo: Optional[Any] = None + self._btn_auswahl_as_atlas: Optional[Any] = None + self._atlas_button: Optional[Any] = None + self._atlas_content: Optional[Any] = None self._build_ui() self._restore_state() @@ -110,6 +119,7 @@ class PrintTab(QWidget): kartenname_combo.addItem(KARTENNAME_PLACEHOLDER) kartenname_combo.addItem(KARTENNAME_38) kartenname_combo.addItem(KARTENNAME_41) + kartenname_combo.addItem(KARTENNAME_BAUFREIGABE) kartenname_combo.currentTextChanged.connect(self._on_kartenname_changed) self._kartenname_combo = kartenname_combo main_layout.addWidget(kartenname_combo) @@ -191,6 +201,49 @@ class PrintTab(QWidget): main_layout.addLayout(druckbereich_row) + # Atlas-Bereich (einklappbar, analog Tab A) + atlas_button = QToolButton() + atlas_button.setText("Atlas") + atlas_button.setCheckable(True) + atlas_button.setChecked(False) + atlas_button.setToolButtonStyle(ToolButtonTextBesideIcon) + atlas_button.setArrowType(ArrowRight) + atlas_button.setStyleSheet("font-weight: bold; margin-top: 6px;") + atlas_button.toggled.connect(self._toggle_atlas) + self._atlas_button = atlas_button + main_layout.addWidget(atlas_button) + + atlas_content = QWidget() + atlas_layout = QVBoxLayout() + atlas_layout.setSpacing(4) + atlas_layout.setContentsMargins(10, 4, 4, 4) + + atlas_layer_label = QLabel("Abdeckungslayer", self) + atlas_layout.addWidget(atlas_layer_label) + + atlas_layer_combo = QgsMapLayerComboBox(self) + set_filters = getattr(atlas_layer_combo, "setFilters", None) + if callable(set_filters): + set_filters(QgsMapLayerProxyModel.VectorLayer) + layer_changed = getattr(atlas_layer_combo, "layerChanged", None) + if layer_changed is not None: + try: + layer_changed.connect(self._on_atlas_layer_changed) + except Exception: + pass + self._atlas_layer_combo = atlas_layer_combo + atlas_layout.addWidget(atlas_layer_combo) + + btn_auswahl_as_atlas = QPushButton("Auswahl als Atlas anlegen", self) + btn_auswahl_as_atlas.clicked.connect(self._on_auswahl_as_atlas_clicked) + self._btn_auswahl_as_atlas = btn_auswahl_as_atlas + atlas_layout.addWidget(btn_auswahl_as_atlas) + + atlas_content.setLayout(atlas_layout) + atlas_content.setVisible(False) + self._atlas_content = atlas_content + main_layout.addWidget(atlas_content) + btn_vorlage_erstellen = QPushButton("Vorlage erstellen", self) btn_vorlage_erstellen.clicked.connect(self._on_vorlage_erstellen) self._btn_vorlage_erstellen = btn_vorlage_erstellen @@ -208,7 +261,7 @@ class PrintTab(QWidget): return saved_kartenname = get_variable(KARTENNAME_VAR, scope="project") - if saved_kartenname in (KARTENNAME_38, KARTENNAME_41): + if saved_kartenname in (KARTENNAME_38, KARTENNAME_41, KARTENNAME_BAUFREIGABE): self._kartenname_combo.setCurrentText(saved_kartenname) else: self._kartenname_combo.setCurrentText(KARTENNAME_PLACEHOLDER) @@ -252,12 +305,18 @@ class PrintTab(QWidget): druckpuffer_cm = DRUCKPUFFER_STANDARD_CM self._druckpuffer_button.setText(self._format_druckpuffer_label(max(0, druckpuffer_cm))) + self._restore_atlas_layer_selection() + def _on_kartenname_changed(self, value: str) -> None: - if value in (KARTENNAME_38, KARTENNAME_41): + if value in (KARTENNAME_38, KARTENNAME_41, KARTENNAME_BAUFREIGABE): set_variable(KARTENNAME_VAR, value, scope="project") else: set_variable(KARTENNAME_VAR, "", scope="project") + # Atlas-Bereich automatisch ein-/ausklappen je nach Kartenname. + if self._atlas_button: + self._atlas_button.setChecked(value == KARTENNAME_BAUFREIGABE) + if self.logic: self.logic.set_kartenname_for_auswahl(value) @@ -358,6 +417,36 @@ class PrintTab(QWidget): self._connect_theme_collection_signals() self._restore_state() + def _toggle_atlas(self, checked: bool) -> None: + if self._atlas_button: + self._atlas_button.setArrowType(ArrowDown if checked else ArrowRight) + if self._atlas_content: + self._atlas_content.setVisible(checked) + + def _restore_atlas_layer_selection(self) -> None: + if not self._atlas_layer_combo: + return + + layer_id = (get_variable("sn_abdeckungslayer", scope="project") or "").strip() + if not layer_id: + return + + try: + layer = QgsProject.instance().mapLayer(layer_id) + if layer is None: + return + + set_current_layer = getattr(self._atlas_layer_combo, "setLayer", None) + if callable(set_current_layer): + set_current_layer(layer) + return + + set_current_layer = getattr(self._atlas_layer_combo, "setCurrentLayer", None) + if callable(set_current_layer): + set_current_layer(layer) + except Exception: + pass + def _disconnect_theme_collection_signals(self) -> None: collection = self._connected_theme_collection if collection is not None: @@ -450,3 +539,101 @@ class PrintTab(QWidget): return float(scale) if scale else None except Exception: return None + + def _on_atlas_layer_changed(self, layer_or_name: Any) -> None: + """Speichert den ausgewaehlten Atlas-Layer als Layer-ID in Projektvariablen.""" + if layer_or_name is None or layer_or_name == "": + set_variable("sn_abdeckungslayer", "", scope="project") + return + + # Standardfall QgsMapLayerComboBox: echtes Layer-Objekt + layer_id_getter = getattr(layer_or_name, "id", None) + if callable(layer_id_getter): + layer_id = layer_id_getter() or "" + set_variable("sn_abdeckungslayer", layer_id, scope="project") + return + + # Fallback: ggf. Name aus Mock/anderen Widgets + value = str(layer_or_name).strip() + if not value: + set_variable("sn_abdeckungslayer", "", scope="project") + return + + try: + project = QgsProject.instance() + map_layers = project.mapLayers() + for layer_id, layer in map_layers.items(): + if getattr(layer, "name", lambda: "")() == value: + set_variable("sn_abdeckungslayer", layer_id, scope="project") + return + except Exception: + pass + + def _on_auswahl_as_atlas_clicked(self) -> None: + """Handler fuer Button 'Auswahl als Atlas anlegen'. + + Erstellt einen neuen Layer 'Atlaslayer' aus der aktuellen Feature-Auswahl + und traegt ihn ins Atlas-Dropdown ein. + """ + if not self.logic or not self.pruefmanager: + return + + atlas_layer, layer_id = self.logic.create_atlas_layer_from_selection() + if atlas_layer is None or not layer_id: + return + + if self._atlas_button and self._atlas_content: + self._atlas_button.setChecked(True) + self._toggle_atlas(True) + + # Neu erstellten Layer im Layer-Combo setzen + if self._atlas_layer_combo: + set_current_layer = getattr(self._atlas_layer_combo, "setLayer", None) + if callable(set_current_layer): + try: + set_current_layer(atlas_layer) + return + except Exception: + pass + + set_current_layer = getattr(self._atlas_layer_combo, "setCurrentLayer", None) + if callable(set_current_layer): + try: + set_current_layer(atlas_layer) + return + except Exception: + pass + + # Fallback nur fuer Mock/klassische ComboBox + self._populate_atlas_layer_combo() + + def _populate_atlas_layer_combo(self) -> None: + """Fallback-Befuellung, falls kein echtes QgsMapLayerComboBox Verhalten verfuegbar ist.""" + if not self._atlas_layer_combo: + return + + clear_fn = getattr(self._atlas_layer_combo, "clear", None) + add_item_fn = getattr(self._atlas_layer_combo, "addItem", None) + if not callable(clear_fn) or not callable(add_item_fn): + return + + self._atlas_layer_combo.blockSignals(True) + clear_fn() + add_item_fn("") + + try: + project = QgsProject.instance() + map_layers = getattr(project, "mapLayers", None) + if not callable(map_layers): + self._atlas_layer_combo.blockSignals(False) + return + + layers = map_layers().values() if hasattr(map_layers(), "values") else map_layers() + for layer in layers: + layer_name = getattr(layer, "name", lambda: None)() + if layer_name and layer_name not in ("", "Druckrechteck"): + add_item_fn(layer_name) + except Exception: + pass + + self._atlas_layer_combo.blockSignals(False)