""" sn_plan41/ui/tab_b_ui.py – UI für Tab B (Druck) """ from __future__ import annotations from typing import Optional from sn_basis.functions.qt_wrapper import ( QWidget, QVBoxLayout, QLabel, QComboBox, QCheckBox, QHBoxLayout, QPushButton, ) from sn_basis.functions.qgiscore_wrapper import QgsProject from sn_basis.functions.qgisui_wrapper import iface from sn_basis.functions.variable_wrapper import get_variable, set_variable # Services (werden von DockWidget injiziert) from sn_basis.modules.Pruefmanager import Pruefmanager from sn_basis.modules.DataGrabber import DataGrabber from sn_plan41.ui.tab_b_logic import ( TabBLogic, MASSSTAB_WIE_KARTENFENSTER, PLOTMASSSTAB_BY_AUSWAHL, THEMA_WIE_KARTENFENSTER, DIN_GROESSEN, DIN_STANDARD, ZIELGROESSE_VAR, FORMFAKTOR_VAR, ) KARTENNAME_VAR = "tab_b_kartenname" KARTENNAME_PLACEHOLDER = "Kartenname wählen" KARTENNAME_38 = "§38" KARTENNAME_41 = "§41" MASSSTAB_VAR = "tab_b_massstab" THEMA_VAR = "tab_b_thema" ZIELGROESSE_UI_VAR = "tab_b_zielgroesse" FORMFAKTOR_UI_VAR = "tab_b_formfaktor" class TabB(QWidget): """ UI-Klasse für Tab B (Druck) des Plan41-Plugins. Zuständig für: - Auswahl des Druckthemas - Auswahl der Druckparameter - Start der Vorlagenanlage (Druck über QGIS-Druckfunktion) Services (Pruefmanager, DataGrabber) werden zur Laufzeit vom DockWidget injiziert. Alle fachlichen Prüfungen laufen über den zentralen Pruefmanager. """ tab_title = "Druck" #: Tab-Titel für BaseDockWidget def __init__(self, parent: Optional[QWidget] = None): """ Initialisiert die UI-Struktur. Services werden später über :meth:`set_services` injiziert. :param parent: Parent-Widget (typischerweise DockWidget) """ super().__init__(parent) # Services (werden von DockWidget gesetzt) self.pruefmanager: Optional[Pruefmanager] = None self.logic: Optional[TabBLogic] = None self._kartenname_combo: Optional[QComboBox] = None self._massstab_combo: Optional[QComboBox] = None self._thema_combo: Optional[QComboBox] = None self._theme_signal_connected = False self._connected_theme_collection: object = None # Referenz für sauberes Trennen self._zielgroesse_combo: Optional[QComboBox] = None self._endlosrolle_cb: Optional[QCheckBox] = None self._btn_vorlage_erstellen: Optional[QPushButton] = None self._build_ui() self._restore_state() self._connect_theme_collection_signals() self._connect_project_signals() def set_services(self, pruefmanager: Pruefmanager, data_grabber: DataGrabber) -> None: """Injiziert Services vom übergeordneten DockWidget.""" _ = data_grabber self.pruefmanager = pruefmanager self.logic = TabBLogic(pruefmanager=self.pruefmanager) if self._kartenname_combo: self.logic.set_kartenname_for_auswahl(self._kartenname_combo.currentText()) if self._massstab_combo: self.logic.set_plotmassstab_for_auswahl( self._massstab_combo.currentText(), self._get_current_canvas_scale(), ) if self._thema_combo: self.logic.set_view_for_auswahl(self._thema_combo.currentText()) if self._zielgroesse_combo: self.logic.set_zielgroesse_for_auswahl(self._zielgroesse_combo.currentText()) if self._endlosrolle_cb: self.logic.set_formfaktor(self._endlosrolle_cb.isChecked()) def _build_ui(self) -> None: """Erstellt die reduzierte UI für die Themenauswahl.""" main_layout = QVBoxLayout() main_layout.setSpacing(4) main_layout.setContentsMargins(4, 4, 4, 4) kartenname_label = QLabel("Kartenname") kartenname_label.setStyleSheet("font-weight: bold; margin-top: 6px;") main_layout.addWidget(kartenname_label) self._kartenname_combo = QComboBox(self) self._kartenname_combo.addItem(KARTENNAME_PLACEHOLDER) self._kartenname_combo.addItem(KARTENNAME_38) self._kartenname_combo.addItem(KARTENNAME_41) self._kartenname_combo.currentTextChanged.connect(self._on_kartenname_changed) main_layout.addWidget(self._kartenname_combo) massstab_label = QLabel("Maßstab") massstab_label.setStyleSheet("font-weight: bold; margin-top: 6px;") main_layout.addWidget(massstab_label) self._massstab_combo = QComboBox(self) self._massstab_combo.addItem(MASSSTAB_WIE_KARTENFENSTER) self._massstab_combo.addItems(list(PLOTMASSSTAB_BY_AUSWAHL.keys())) self._massstab_combo.currentTextChanged.connect(self._on_massstab_changed) main_layout.addWidget(self._massstab_combo) thema_label = QLabel("Thema") thema_label.setStyleSheet("font-weight: bold; margin-top: 6px;") main_layout.addWidget(thema_label) self._thema_combo = QComboBox(self) self._thema_combo.addItem(THEMA_WIE_KARTENFENSTER) self._thema_combo.addItems(self._get_gespeicherte_themen()) self._thema_combo.currentTextChanged.connect(self._on_thema_changed) main_layout.addWidget(self._thema_combo) zielgroesse_label = QLabel("max. Blattgröße") zielgroesse_label.setStyleSheet("font-weight: bold; margin-top: 6px;") main_layout.addWidget(zielgroesse_label) zielgroesse_row = QHBoxLayout() zielgroesse_row.setSpacing(6) self._zielgroesse_combo = QComboBox(self) self._zielgroesse_combo.addItems(list(DIN_GROESSEN.keys())) self._zielgroesse_combo.setCurrentText(DIN_STANDARD) self._zielgroesse_combo.currentTextChanged.connect(self._on_zielgroesse_changed) zielgroesse_row.addWidget(self._zielgroesse_combo) self._endlosrolle_cb = QCheckBox("Endlosrolle", self) self._endlosrolle_cb.setChecked(False) self._endlosrolle_cb.stateChanged.connect(self._on_formfaktor_changed) zielgroesse_row.addWidget(self._endlosrolle_cb) main_layout.addLayout(zielgroesse_row) self._btn_vorlage_erstellen = QPushButton("Vorlage erstellen", self) self._btn_vorlage_erstellen.clicked.connect(self._on_vorlage_erstellen) main_layout.addWidget(self._btn_vorlage_erstellen) main_layout.addStretch(1) self.setLayout(main_layout) def _restore_state(self) -> None: """Stellt die gespeicherten Combobox-Zustände wieder her.""" if not self._kartenname_combo or not self._massstab_combo or not self._thema_combo: return if not self._zielgroesse_combo or not self._endlosrolle_cb: return saved_kartenname = get_variable(KARTENNAME_VAR, scope="project") if saved_kartenname in (KARTENNAME_38, KARTENNAME_41): self._kartenname_combo.setCurrentText(saved_kartenname) else: self._kartenname_combo.setCurrentText(KARTENNAME_PLACEHOLDER) saved_massstab = get_variable(MASSSTAB_VAR, scope="project") valid_massstaebe = [MASSSTAB_WIE_KARTENFENSTER, *PLOTMASSSTAB_BY_AUSWAHL.keys()] if saved_massstab in valid_massstaebe: self._massstab_combo.setCurrentText(saved_massstab) else: self._massstab_combo.setCurrentText(MASSSTAB_WIE_KARTENFENSTER) aktuelle_themen = [THEMA_WIE_KARTENFENSTER, *self._get_gespeicherte_themen()] self._thema_combo.clear() self._thema_combo.addItems(aktuelle_themen) saved_thema = get_variable(THEMA_VAR, scope="project") if saved_thema in aktuelle_themen: self._thema_combo.setCurrentText(saved_thema) else: self._thema_combo.setCurrentText(THEMA_WIE_KARTENFENSTER) saved_zielgroesse = get_variable(ZIELGROESSE_UI_VAR, scope="project") if saved_zielgroesse in DIN_GROESSEN: self._zielgroesse_combo.setCurrentText(saved_zielgroesse) else: self._zielgroesse_combo.setCurrentText(DIN_STANDARD) saved_formfaktor = get_variable(FORMFAKTOR_UI_VAR, scope="project") self._endlosrolle_cb.setChecked(saved_formfaktor == "Endlosrolle") def _on_kartenname_changed(self, value: str) -> None: """Persistiert die Kartennamen-Auswahl und setzt ``sn_kartenname``.""" if value in (KARTENNAME_38, KARTENNAME_41): set_variable(KARTENNAME_VAR, value, scope="project") else: set_variable(KARTENNAME_VAR, "", scope="project") if self.logic: self.logic.set_kartenname_for_auswahl(value) def _on_massstab_changed(self, value: str) -> None: """Persistiert Maßstabsauswahl und setzt ``sn_plotmassstab``.""" set_variable(MASSSTAB_VAR, value, scope="project") if self.logic: self.logic.set_plotmassstab_for_auswahl(value, self._get_current_canvas_scale()) def _on_thema_changed(self, value: str) -> None: """Persistiert die Thema-Auswahl und setzt ``sn_view``.""" set_variable(THEMA_VAR, value, scope="project") if self.logic: self.logic.set_view_for_auswahl(value) def _on_zielgroesse_changed(self, value: str) -> None: """Persistiert Blattgröße und setzt ``sn_zielgroesse``.""" set_variable(ZIELGROESSE_UI_VAR, value, scope="project") if self.logic: self.logic.set_zielgroesse_for_auswahl(value) def _on_formfaktor_changed(self, state: int) -> None: """Persistiert Endlosrolle-Zustand und setzt ``sn_formfaktor``.""" checked = bool(state) set_variable(FORMFAKTOR_UI_VAR, "Endlosrolle" if checked else "Blatt", scope="project") if self.logic: self.logic.set_formfaktor(checked) def _on_vorlage_erstellen(self) -> None: """Startet die Pipeline Druckvorlage_anlegen.""" print("[TabB] _on_vorlage_erstellen aufgerufen") if not self.logic or not self.pruefmanager: print(f"[TabB] Abbruch: logic={self.logic}, pruefmanager={self.pruefmanager}") return # Layer direkt aus Tab A lesen (unabhängig von Projektvariable) layer = None tab_a = self._get_tab_a_widget() print(f"[TabB] tab_a Widget: {tab_a!r}") if tab_a is not None: layer_combo = getattr(tab_a, "layer_combo", None) print(f"[TabB] layer_combo: {layer_combo!r}") if layer_combo is not None: layer = layer_combo.currentLayer() print(f"[TabB] layer: {layer!r} (Name: {getattr(layer, 'name', lambda: '–')()})") print(f"[TabB] Rufe druckvorlage_anlegen auf mit layer={layer!r}, " f"kartenname={self._kartenname_combo.currentText() if self._kartenname_combo else '?'}, " f"massstab={self._massstab_combo.currentText() if self._massstab_combo else '?'}, " f"zielgroesse={self._zielgroesse_combo.currentText() if self._zielgroesse_combo else '?'}, " f"formfaktor={self._endlosrolle_cb.isChecked() if self._endlosrolle_cb else '?'}") result = self.logic.druckvorlage_anlegen( layer=layer, kartenname_auswahl=self._kartenname_combo.currentText() if self._kartenname_combo else "", massstab_auswahl=self._massstab_combo.currentText() if self._massstab_combo else "", zielgroesse=self._zielgroesse_combo.currentText() if self._zielgroesse_combo else DIN_STANDARD, formfaktor=self._endlosrolle_cb.isChecked() if self._endlosrolle_cb else False, ) print(f"[TabB] druckvorlage_anlegen Ergebnis: {result}") if result.get("switch_to_tab_a"): self._aktiviere_tab_a() def _get_tab_widget(self): """Findet das übergeordnete QTabWidget anhand des ``tabBar``-Attributs.""" try: widget = self.parent() while widget is not None: if hasattr(widget, "tabBar") and hasattr(widget, "setCurrentIndex"): return widget parent_fn = getattr(widget, "parent", None) widget = parent_fn() if callable(parent_fn) else None except Exception: pass return None def _get_tab_a_widget(self): """Gibt die Tab-A-Widget-Instanz zurück (Index 0 im übergeordneten QTabWidget).""" tab_widget = self._get_tab_widget() if tab_widget is None: return None try: return tab_widget.widget(0) except Exception: return None def _aktiviere_tab_a(self) -> None: """Wechselt den aktiven Reiter auf Tab A im übergeordneten QTabWidget.""" tab_widget = self._get_tab_widget() if tab_widget is not None: try: tab_widget.setCurrentIndex(0) except Exception: pass def _connect_project_signals(self) -> None: """Verbindet QgsProject-Signale für Projektwechsel/-neuladen.""" project = QgsProject.instance() for signal_name in ("readProject", "newProjectCreated", "cleared"): signal = getattr(project, signal_name, None) if signal is None: continue try: signal.connect(self._on_project_changed) except Exception: pass def _on_project_changed(self, *args) -> None: """Reagiert auf Projektwechsel: Signale neu binden, Combobox und State auffrischen.""" _ = args # Alte Theme-Collection-Signals zuerst trennen self._disconnect_theme_collection_signals() # Neu verbinden für das jetzt geladene Projekt self._theme_signal_connected = False self._connect_theme_collection_signals() # Ansicht-Liste + gespeicherten State wiederherstellen self._restore_state() def _disconnect_theme_collection_signals(self) -> None: """Trennt Signale der alten Theme-Collection sauber.""" collection = self._connected_theme_collection if collection is None: return for signal_name in ("mapThemesChanged", "changed", "themeChanged"): signal = getattr(collection, signal_name, None) if signal is None: continue try: signal.disconnect(self._refresh_thema_combo_live) except Exception: pass self._connected_theme_collection = None def _connect_theme_collection_signals(self) -> None: """Verbindet Signale der Theme-Collection für Live-Aktualisierung der Themenliste.""" if self._theme_signal_connected: return try: theme_collection = QgsProject.instance().mapThemeCollection() except Exception: return if theme_collection is None: return connected_any = False for signal_name in ("mapThemesChanged", "changed", "themeChanged"): signal = getattr(theme_collection, signal_name, None) if signal is None: continue try: signal.connect(self._refresh_thema_combo_live) connected_any = True except Exception: pass if connected_any: self._connected_theme_collection = theme_collection self._theme_signal_connected = connected_any def _refresh_thema_combo_live(self, *args) -> None: """Aktualisiert die Thema-Combobox bei Änderungen gespeicherter Layerthemen.""" _ = args if not self._thema_combo: return vorherige_auswahl = self._thema_combo.currentText() or THEMA_WIE_KARTENFENSTER eintraege = [THEMA_WIE_KARTENFENSTER, *self._get_gespeicherte_themen()] self._thema_combo.blockSignals(True) self._thema_combo.clear() self._thema_combo.addItems(eintraege) if vorherige_auswahl in eintraege: self._thema_combo.setCurrentText(vorherige_auswahl) else: self._thema_combo.setCurrentText(THEMA_WIE_KARTENFENSTER) self._thema_combo.blockSignals(False) self._on_thema_changed(self._thema_combo.currentText()) def _get_gespeicherte_themen(self) -> list[str]: """Liefert die Namen der im Projekt gespeicherten Layerthemen (QgsMapThemeCollection).""" try: theme_collection = QgsProject.instance().mapThemeCollection() if theme_collection is None: return [] themes = theme_collection.mapThemes() except Exception: return [] namen: list[str] = [] for theme_name in themes: name = str(theme_name or "").strip() if name and name not in namen: namen.append(name) return namen def _get_current_canvas_scale(self) -> float | None: """Liest den aktuellen Maßstab aus der Kartensicht.""" try: canvas = iface.mapCanvas() if canvas is None: return None scale = canvas.scale() return float(scale) if scale else None except Exception: return None