""" sn_basis/ui/tabs/print_tab.py - Zentraler Druck-Tab für sn-Plugins. """ from __future__ import annotations from typing import Any, Optional from sn_basis.functions.qt_wrapper import ( QWidget, QVBoxLayout, QLabel, QComboBox, QCheckBox, QHBoxLayout, QPushButton, QToolButton, ToolButtonTextBesideIcon, ArrowDown, ArrowRight, ) 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 from sn_basis.modules.print_logic import ( PrintLogic, MASSSTAB_WIE_KARTENFENSTER, PLOTMASSSTAB_BY_AUSWAHL, THEMA_WIE_KARTENFENSTER, DIN_GROESSEN, DIN_STANDARD, DRUCKBEREICH_VAR, DRUCKPUFFER_VAR, DRUCKPUFFER_STANDARD_CM, DRUCKBEREICH_VERFAHRENSGEBIET, DRUCKBEREICH_WIE_KARTENFENSTER, DRUCKBEREICH_AKTUELLE_AUSWAHL, DRUCKBEREICH_RECHTECK, ) KARTENNAME_VAR = "tab_b_kartenname" KARTENNAME_PLACEHOLDER = "Kartenname wählen" 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" FORMFAKTOR_UI_VAR = "tab_b_formfaktor" ATLAS_ERZEUGEN_VAR = "tab_b_atlas_erzeugen" DRUCKBEREICH_OPTIONEN = [ DRUCKBEREICH_VERFAHRENSGEBIET, DRUCKBEREICH_WIE_KARTENFENSTER, DRUCKBEREICH_AKTUELLE_AUSWAHL, DRUCKBEREICH_RECHTECK, ] class PrintTab(QWidget): """UI-Klasse für den zentralen Druck-Tab.""" tab_title = "Druck" def __init__(self, parent: Optional[Any] = None): super().__init__(parent) self.pruefmanager: Optional[Pruefmanager] = None self.logic = PrintLogic(self.pruefmanager) #self.logic: Optional[PrintLogic] = None self._kartenname_combo: Optional[Any] = None self._massstab_combo: Optional[Any] = None self._thema_combo: Optional[Any] = None self._theme_signal_connected = False self._connected_theme_collection: object = None self._zielgroesse_combo: Optional[Any] = None self._druckbereich_combo: Optional[Any] = None 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_erzeugen_cb: Optional[Any] = None self._atlas_button: Optional[Any] = None self._atlas_content: Optional[Any] = 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: _ = data_grabber self.pruefmanager = pruefmanager self.logic = PrintLogic(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._druckbereich_combo: self.logic.set_druckbereich_for_auswahl(self._druckbereich_combo.currentText()) if self._endlosrolle_cb: self.logic.set_formfaktor(self._endlosrolle_cb.isChecked()) def _build_ui(self) -> None: 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) kartenname_combo = QComboBox(self) 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) massstab_label = QLabel("Maßstab") massstab_label.setStyleSheet("font-weight: bold; margin-top: 6px;") main_layout.addWidget(massstab_label) massstab_combo = QComboBox(self) massstab_combo.addItem(MASSSTAB_WIE_KARTENFENSTER) massstab_combo.addItems(list(PLOTMASSSTAB_BY_AUSWAHL.keys())) massstab_combo.currentTextChanged.connect(self._on_massstab_changed) self._massstab_combo = massstab_combo main_layout.addWidget(massstab_combo) thema_label = QLabel("Thema") thema_label.setStyleSheet("font-weight: bold; margin-top: 6px;") main_layout.addWidget(thema_label) thema_combo = QComboBox(self) thema_combo.addItem(THEMA_WIE_KARTENFENSTER) thema_combo.addItems(self._get_gespeicherte_themen()) thema_combo.currentTextChanged.connect(self._on_thema_changed) self._thema_combo = thema_combo main_layout.addWidget(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) zielgroesse_combo = QComboBox(self) zielgroesse_combo.addItems(list(DIN_GROESSEN.keys())) zielgroesse_combo.setCurrentText(DIN_STANDARD) zielgroesse_combo.currentTextChanged.connect(self._on_zielgroesse_changed) self._zielgroesse_combo = zielgroesse_combo zielgroesse_row.addWidget(zielgroesse_combo) endlosrolle_cb = QCheckBox("Endlosrolle", self) endlosrolle_cb.setChecked(False) endlosrolle_cb.stateChanged.connect(self._on_formfaktor_changed) self._endlosrolle_cb = endlosrolle_cb zielgroesse_row.addWidget(endlosrolle_cb) main_layout.addLayout(zielgroesse_row) druckbereich_row = QHBoxLayout() druckbereich_row.setSpacing(6) druckbereich_column = QVBoxLayout() druckbereich_column.setSpacing(4) druckbereich_column.setContentsMargins(0, 0, 0, 0) druckbereich_label = QLabel("Druckbereich") druckbereich_label.setStyleSheet("font-weight: bold; margin-top: 6px;") druckbereich_column.addWidget(druckbereich_label) druckbereich_combo = QComboBox(self) druckbereich_combo.addItems(DRUCKBEREICH_OPTIONEN) druckbereich_combo.currentTextChanged.connect(self._on_druckbereich_changed) self._druckbereich_combo = druckbereich_combo druckbereich_column.addWidget(druckbereich_combo) druckbereich_row.addLayout(druckbereich_column, 1) druckpuffer_column = QVBoxLayout() druckpuffer_column.setSpacing(4) druckpuffer_column.setContentsMargins(0, 0, 0, 0) druckpuffer_label = QLabel("Kartenpuffer") druckpuffer_label.setStyleSheet("font-weight: bold; margin-top: 6px;") druckpuffer_column.addWidget(druckpuffer_label) druckpuffer_button = QPushButton(self._format_druckpuffer_label(DRUCKPUFFER_STANDARD_CM), self) druckpuffer_button.setToolTip("Puffer") druckpuffer_button.clicked.connect(self._on_druckpuffer_clicked) self._druckpuffer_button = druckpuffer_button druckpuffer_column.addWidget(druckpuffer_button) druckbereich_row.addLayout(druckpuffer_column) 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_erzeugen_cb = QCheckBox("Atlas erzeugen", self) atlas_erzeugen_cb.setChecked(True) atlas_erzeugen_cb.stateChanged.connect(self._on_atlas_erzeugen_changed) self._atlas_erzeugen_cb = atlas_erzeugen_cb atlas_layout.addWidget(atlas_erzeugen_cb) 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 main_layout.addWidget(btn_vorlage_erstellen) main_layout.addStretch(1) self.setLayout(main_layout) def _restore_state(self) -> None: 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 if not self._druckbereich_combo or not self._druckpuffer_button: return saved_kartenname = get_variable(KARTENNAME_VAR, scope="project") if saved_kartenname in (KARTENNAME_38, KARTENNAME_41, KARTENNAME_BAUFREIGABE): 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") saved_druckbereich = get_variable(DRUCKBEREICH_VAR, scope="project") if saved_druckbereich in DRUCKBEREICH_OPTIONEN: self._druckbereich_combo.setCurrentText(saved_druckbereich) else: self._druckbereich_combo.setCurrentText(DRUCKBEREICH_VERFAHRENSGEBIET) saved_druckpuffer = get_variable(DRUCKPUFFER_VAR, scope="project") try: druckpuffer_cm = int(str(saved_druckpuffer).strip()) except (TypeError, ValueError): druckpuffer_cm = DRUCKPUFFER_STANDARD_CM self._druckpuffer_button.setText(self._format_druckpuffer_label(max(0, druckpuffer_cm))) if self._atlas_erzeugen_cb: atlas_raw = get_variable(ATLAS_ERZEUGEN_VAR, scope="project") atlas_checked = True if atlas_raw in (None, "") else str(atlas_raw).strip().lower() in { "1", "true", "ja", "yes", "on", } self._atlas_erzeugen_cb.setChecked(atlas_checked) self._on_atlas_erzeugen_changed(1 if atlas_checked else 0) self._restore_atlas_layer_selection() def _on_kartenname_changed(self, value: str) -> None: 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) def _on_massstab_changed(self, value: str) -> None: 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: 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: 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: 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_druckbereich_changed(self, value: str) -> None: if value not in DRUCKBEREICH_OPTIONEN: value = DRUCKBEREICH_VERFAHRENSGEBIET set_variable(DRUCKBEREICH_VAR, value, scope="project") if self.logic: self.logic.set_druckbereich_for_auswahl(value) if value == DRUCKBEREICH_RECHTECK: self.logic.activate_druckrechteck_capture() def _on_atlas_erzeugen_changed(self, state: int) -> None: enabled = bool(state) set_variable(ATLAS_ERZEUGEN_VAR, "1" if enabled else "0", scope="project") if self._atlas_layer_combo: set_enabled = getattr(self._atlas_layer_combo, "setEnabled", None) if callable(set_enabled): try: set_enabled(enabled) except Exception: pass if self._btn_auswahl_as_atlas: self._btn_auswahl_as_atlas.setEnabled(enabled) def _on_druckpuffer_clicked(self) -> None: if not self.pruefmanager: return current_value = get_variable(DRUCKPUFFER_VAR, scope="project") or str(DRUCKPUFFER_STANDARD_CM) text_value, bestaetigt = self.pruefmanager.frage_text( "Puffer um den Druckbereich", "Puffer um den Druckbereich", default_text=str(current_value), ) if not bestaetigt: return try: puffer_value = int(str(text_value).strip()) except (TypeError, ValueError): self.pruefmanager.zeige_hinweis( "Puffer um den Druckbereich", "Der Puffer muss als ganze Zahl eingegeben werden.", ) return if puffer_value < 0: self.pruefmanager.zeige_hinweis( "Puffer um den Druckbereich", "Der Puffer darf nicht negativ sein.", ) return set_variable(DRUCKPUFFER_VAR, str(puffer_value), scope="project") if self._druckpuffer_button: self._druckpuffer_button.setText(self._format_druckpuffer_label(puffer_value)) def _format_druckpuffer_label(self, value_cm: int) -> str: return f"{value_cm} cm" def _on_vorlage_erstellen(self) -> None: if not self.logic or not self.pruefmanager: return self.logic.druckvorlage_anlegen( layer=None, 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, druckbereich_auswahl=self._druckbereich_combo.currentText() if self._druckbereich_combo else DRUCKBEREICH_VERFAHRENSGEBIET, ) def _disconnect_project_signals(self) -> None: try: project = QgsProject.instance() except Exception: return if project is None: return for signal_name in ("readProject", "newProjectCreated", "cleared"): signal = getattr(project, signal_name, None) if signal is None: continue try: signal.disconnect(self._on_project_changed) except Exception: pass def cleanup(self) -> None: """Trennt Signalverbindungen vor dem Schliessen des Tabs.""" self._disconnect_theme_collection_signals() self._disconnect_project_signals() def _connect_project_signals(self) -> None: try: project = QgsProject.instance() except Exception: return if project is None: return 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: _ = args self._disconnect_theme_collection_signals() self._theme_signal_connected = False 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: for signal_name in ("mapThemesChanged", "changed", "themeChanged"): try: signal = getattr(collection, signal_name, None) except RuntimeError: break except Exception: continue if signal is None: continue try: signal.disconnect(self._refresh_thema_combo_live) except Exception: pass self._connected_theme_collection = None self._theme_signal_connected = False def _connect_theme_collection_signals(self) -> None: 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: _ = 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]: 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: try: canvas = iface.mapCanvas() if canvas is None: return None scale = canvas.scale() return float(scale) if scale else None except Exception: return None def _on_atlas_layer_changed(self, layer_or_name: Any) -> None: """Speichert den ausgewählten 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 für Button 'Auswahl als Atlas anlegen'. Erstellt einen neuen Layer 'Atlaslayer' aus der aktuellen Feature-Auswahl und trägt 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 für Mock/klassische ComboBox self._populate_atlas_layer_combo() def _populate_atlas_layer_combo(self) -> None: """Fallback-Befüllung, falls kein echtes QgsMapLayerComboBox Verhalten verfügbar 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)