# -*- coding: utf-8 -*- """Seitenleiste (andockbares Panel) des Plugins — rechts in QGIS. Ersetzt die frühere Werkzeugleiste (Erik, 08.09.2026: „keine Toolbar, sondern eine Seitenleiste auf der rechten Seite"). Das Panel ist reine Oberfläche: es hält keinen Zustand der Fachlogik, sondern meldet Klicks über Rückrufe (``callbacks``) an ``VlnKartenPlugin`` und wird von dort über ``set_angemeldet`` / ``set_verfahren`` umgeschaltet. Aufbau von oben nach unten: 1. Anmeldung — Status + Knopf (Anmelden/Abmelden) 2. Verfahren — Suchfeld (filtert) + Auswahlliste 3. Layer — Auswahl der Gruppen, laden, Änderungen speichern 4. Geodienste — die Linkliste des Verfahrens: auswählen und als Layer laden 5. Darstellung — Stil speichern (mit Häkchen „für alle Verfahren") 6. Statuszeile Der frühere GeoJSON-Weg (Kopie laden, später als Ganzes hochladen) ist am 08.09.2026 entfallen (Erik: „das Speichern via OGC einbauen, GeoJSON nicht mehr verwenden"). Über die OGC API schreibt QGIS jede Änderung einzeln zurück; „Änderungen speichern" schließt nur die offenen Bearbeitungssitzungen. """ from qgis.PyQt.QtCore import Qt from qgis.PyQt.QtWidgets import (QCheckBox, QComboBox, QDockWidget, QFrame, QGroupBox, QHBoxLayout, QLabel, QLineEdit, QListWidget, QListWidgetItem, QPushButton, QSizePolicy, QVBoxLayout, QWidget) PANEL_TITEL = "VLN Karten" PANEL_OBJEKT = "VlnKartenPanel" def _klein(widget): """Hinweistexte eine Stufe kleiner und gedämpft — wie in QGIS-Panels üblich. Die Farbe kommt aus der Palette (``palette(mid)``), damit der Text in hellen wie dunklen QGIS-Themen lesbar bleibt; ``setEnabled(False)`` wäre dafür der falsche Weg, weil es das Widget zugleich unbedienbar macht. """ schrift = widget.font() schrift.setPointSizeF(max(8.0, schrift.pointSizeF() - 1.0)) widget.setFont(schrift) widget.setWordWrap(True) widget.setStyleSheet("color: palette(mid);") return widget class VlnPanel(QDockWidget): """Die Seitenleiste. ``callbacks`` bündelt die Rückrufe ins Plugin.""" def __init__(self, parent=None, callbacks=None): super().__init__(PANEL_TITEL, parent) self.setObjectName(PANEL_OBJEKT) self.setAllowedAreas(Qt.DockWidgetArea.RightDockWidgetArea | Qt.DockWidgetArea.LeftDockWidgetArea) self._cb = callbacks or {} self._verfahren = [] # [(beschriftung, vkz)] — ungefiltert self._stumm = False # unterdrückt Signale beim programmatischen Füllen inhalt = QWidget() aussen = QVBoxLayout(inhalt) aussen.setContentsMargins(8, 8, 8, 8) aussen.setSpacing(10) aussen.addWidget(self._bau_anmeldung()) aussen.addWidget(self._bau_verfahren()) aussen.addWidget(self._bau_layer()) aussen.addWidget(self._bau_dienste()) aussen.addWidget(self._bau_darstellung()) aussen.addStretch(1) aussen.addWidget(self._bau_status()) inhalt.setMinimumWidth(260) self.setWidget(inhalt) self.set_angemeldet(False) # ------------------------------------------------------------------ # Aufbau # ------------------------------------------------------------------ def _bau_anmeldung(self): kasten = QGroupBox("Anmeldung") lage = QVBoxLayout(kasten) lage.setSpacing(6) self.status_label = QLabel("Nicht angemeldet") self.status_label.setWordWrap(True) self.login_knopf = QPushButton("Anmelden …") self.login_knopf.clicked.connect(lambda: self._ruf("login")) self.logout_knopf = QPushButton("Abmelden") self.logout_knopf.clicked.connect(lambda: self._ruf("logout")) zeile = QHBoxLayout() zeile.addWidget(self.login_knopf) zeile.addWidget(self.logout_knopf) lage.addWidget(self.status_label) lage.addLayout(zeile) return kasten def _bau_verfahren(self): kasten = QGroupBox("Verfahren") lage = QVBoxLayout(kasten) lage.setSpacing(6) self.filter_feld = QLineEdit() self.filter_feld.setPlaceholderText("filtern (Name oder Kennzeichen) …") self.filter_feld.setClearButtonEnabled(True) self.filter_feld.textChanged.connect(self._filter_geaendert) self.combo = QComboBox() self.combo.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self.combo.setToolTip("Teilnehmergemeinschaft, für die geladen und gespeichert wird") self.combo.currentIndexChanged.connect(self._auswahl_geaendert) lage.addWidget(self.filter_feld) lage.addWidget(self.combo) return kasten def _bau_layer(self): kasten = QGroupBox("Layer") lage = QVBoxLayout(kasten) lage.setSpacing(6) # Jede Sammlung kostet beim Anlegen rund eine halbe Sekunde (der Provider fragt # Sammlung, Schema, Stil, OPTIONS und die erste Seite ab). Wer nur einen Teil braucht, # ist mit der Auswahl deutlich schneller: 27 Layer ~14 s, die Vorerhebung allein ~2 s. lage.addWidget(_klein(QLabel("Was geladen wird:"))) self.gruppen_haken = {} for schluessel, beschriftung in (("Verfahren", "Verfahren (Umring, Plan 41, KAS …)"), ("Vorerhebung", "Vorerhebung"), ("Maßnahmenblätter", "Maßnahmenblätter")): haken = QCheckBox(beschriftung) haken.setChecked(True) lage.addWidget(haken) self.gruppen_haken[schluessel] = haken self.ogc_knopf = QPushButton("Layer laden") self.ogc_knopf.setToolTip("Die angehakten Sammlungen als editierbare Layer der OGC API") self.ogc_knopf.clicked.connect(lambda: self._ruf("load_ogc")) linie = QFrame() linie.setFrameShape(QFrame.Shape.HLine) linie.setFrameShadow(QFrame.Shadow.Sunken) self.speichern_knopf = QPushButton("Änderungen speichern") self.speichern_knopf.setToolTip("Schließt die offenen Bearbeitungssitzungen aller " "geladenen VLN-Layer; jede Änderung geht einzeln an " "die API") self.speichern_knopf.clicked.connect(lambda: self._ruf("save_edits")) lage.addWidget(self.ogc_knopf) lage.addWidget(linie) lage.addWidget(self.speichern_knopf) lage.addWidget(_klein(QLabel("Bearbeiten wie gewohnt über den Stift; Speichern schreibt " "jede Änderung sofort zurück."))) return kasten def _bau_dienste(self): """Die Linkliste des gewählten Verfahrens — vierstufig gepflegt, hier nur geladen. Die Liste kommt fertig aufgelöst vom Server (landesweit, Landkreis, Team, Verfahren); je Zeile steht die Herkunft dabei, damit man sieht, wo ein Dienst herkommt. """ kasten = QGroupBox("Geodienste (Linkliste)") lage = QVBoxLayout(kasten) lage.setSpacing(6) lage.addWidget(_klein(QLabel("WMS-, WFS- und ArcGIS-Dienste für dieses Verfahren."))) self.dienste_liste = QListWidget() self.dienste_liste.setMinimumHeight(140) self.dienste_liste.setToolTip("Angehakte Dienste werden geladen") lage.addWidget(self.dienste_liste) zeile = QHBoxLayout() self.dienste_holen_knopf = QPushButton("Liste holen") self.dienste_holen_knopf.clicked.connect(lambda: self._ruf("dienste_holen")) self.dienste_laden_knopf = QPushButton("Ausgewählte laden") self.dienste_laden_knopf.clicked.connect(lambda: self._ruf("dienste_laden")) zeile.addWidget(self.dienste_holen_knopf) zeile.addWidget(self.dienste_laden_knopf) lage.addLayout(zeile) self.dienste_alle = QPushButton("alle an- oder abwählen") self.dienste_alle.setFlat(True) self.dienste_alle.clicked.connect(self._dienste_umschalten) lage.addWidget(self.dienste_alle) return kasten def _dienste_umschalten(self): """Alles an, wenn noch etwas aus ist; sonst alles aus.""" etwas_aus = any(self.dienste_liste.item(i).checkState() != Qt.CheckState.Checked for i in range(self.dienste_liste.count())) ziel = Qt.CheckState.Checked if etwas_aus else Qt.CheckState.Unchecked for i in range(self.dienste_liste.count()): self.dienste_liste.item(i).setCheckState(ziel) def set_dienste(self, dienste): """Die geholte Liste anzeigen: Name, Gruppe und Herkunft, alle zunächst angehakt.""" self.dienste_liste.clear() for d in dienste or []: text = "%s · %s · %s" % (d.get("name"), d.get("gruppe_name") or "", d.get("herkunft_text") or "") if d.get("stil"): text += " · mit Stil" eintrag = QListWidgetItem(text) eintrag.setFlags(eintrag.flags() | Qt.ItemFlag.ItemIsUserCheckable) eintrag.setCheckState(Qt.CheckState.Checked) eintrag.setData(Qt.ItemDataRole.UserRole, d) eintrag.setToolTip("%s\n%s" % (d.get("url"), d.get("beschreibung") or "")) self.dienste_liste.addItem(eintrag) def gewaehlte_dienste(self): """Die angehakten Einträge als Liste der Server-Datensätze.""" aus = [] for i in range(self.dienste_liste.count()): e = self.dienste_liste.item(i) if e.checkState() == Qt.CheckState.Checked: aus.append(e.data(Qt.ItemDataRole.UserRole)) return aus def _bau_darstellung(self): kasten = QGroupBox("Darstellung") lage = QVBoxLayout(kasten) lage.setSpacing(6) self.stil_knopf = QPushButton("Stil des aktiven Layers speichern") self.stil_knopf.setToolTip("Den Stil dieses Verfahrens darf jeder speichern, der das " "Verfahren sieht") self.stil_knopf.clicked.connect(lambda: self._ruf("save_style")) self.vorlage_haken = QCheckBox("als Vorlage für alle Verfahren") self.vorlage_haken.setToolTip("Gilt dann für diese Art in jedem Verfahren, z. B. für " "jede Karte alter Stand — nur für die Gruppe " "MODUL_KARTENSTIL") lage.addWidget(self.stil_knopf) lage.addWidget(self.vorlage_haken) return kasten def _bau_status(self): self.status_zeile = _klein(QLabel("")) self.status_zeile.setToolTip("letzte Aktion") return self.status_zeile # ------------------------------------------------------------------ # Zustand (vom Plugin gesetzt) # ------------------------------------------------------------------ def set_angemeldet(self, angemeldet, wer=None): self.status_label.setText(("Angemeldet: %s" % wer) if angemeldet and wer else ("Angemeldet" if angemeldet else "Nicht angemeldet")) self.login_knopf.setVisible(not angemeldet) self.logout_knopf.setVisible(angemeldet) for widget in (self.filter_feld, self.combo, self.ogc_knopf, self.speichern_knopf, self.stil_knopf, self.vorlage_haken, self.dienste_holen_knopf, self.dienste_laden_knopf, self.dienste_alle, self.dienste_liste, *self.gruppen_haken.values()): widget.setEnabled(angemeldet) if not angemeldet: self._stumm = True self._verfahren = [] self.combo.clear() self.combo.addItem("— bitte anmelden —", None) self.filter_feld.clear() self.dienste_liste.clear() self._stumm = False def set_verfahren(self, verfahren): """``verfahren``: Liste von ``{"vkz":…, "name":…}`` aus der API.""" self._verfahren = [ ("%s — %s" % (v["vkz"], v["name"]) if v.get("name") else str(v["vkz"]), v["vkz"]) for v in verfahren ] self._liste_fuellen() def set_status(self, text): self.status_zeile.setText(text or "") def aktuelles_vkz(self): return self.combo.currentData() def aktueller_text(self): return self.combo.currentText() def als_vorlage(self): return self.vorlage_haken.isChecked() def gewaehlte_gruppen(self): """Namen der angehakten Layer-Gruppen (wie in ``ogc_layers.GRUPPEN``).""" return {name for name, haken in self.gruppen_haken.items() if haken.isChecked()} def waehle_vkz(self, vkz): """Setzt die Auswahl ohne Rückruf (Wiederherstellen aus der Projektdatei).""" self._stumm = True index = self.combo.findData(vkz) if vkz else -1 self.combo.setCurrentIndex(index if index >= 0 else 0) self._stumm = False def anzahl_eintraege(self): return self.combo.count() # ------------------------------------------------------------------ # Innenleben # ------------------------------------------------------------------ def _liste_fuellen(self, muster=""): """Füllt die Auswahlliste, gefiltert; die bisherige Auswahl bleibt wenn möglich.""" vorher = self.combo.currentData() muster = (muster or "").strip().lower() treffer = [(t, v) for t, v in self._verfahren if muster in t.lower()] self._stumm = True self.combo.clear() if not self._verfahren: self.combo.addItem("— keine Verfahren —", None) elif not treffer: self.combo.addItem("— kein Treffer —", None) else: self.combo.addItem("— Verfahren wählen —", None) for beschriftung, vkz in treffer: self.combo.addItem(beschriftung, vkz) index = self.combo.findData(vorher) if vorher else -1 self.combo.setCurrentIndex(index if index >= 0 else 0) self._stumm = False if index < 0 and vorher: self._auswahl_geaendert(0) def _filter_geaendert(self, text): self._liste_fuellen(text) def _auswahl_geaendert(self, _index): if not self._stumm: self._ruf("verfahren_gewaehlt", self.combo.currentData()) def _ruf(self, name, *args): rueckruf = self._cb.get(name) if rueckruf: rueckruf(*args)