Files
API_Karte_QGISDemo/vln_karten/panel.py
T
erikandClaude Fable 5.1 b97f066b49 Seitenleiste statt Werkzeugleiste
Erik, 08.09.2026: "ich will keine Toolbar, sondern so eine Seitenleiste auf der
rechten Seite."

- vln_karten/panel.py (neu): andockbares QDockWidget, rechts angedockt, mit den
  Abschnitten Anmeldung, Verfahren, OGC API und Klassisch (GeoJSON) sowie einer
  Statuszeile. Reine Oberflaeche; Klicks gehen als Rueckrufe ans Plugin.
- Verfahrensliste bekommt ein Suchfeld (Hausregel: Auswahllisten immer mit
  Filter). Die Auswahl bleibt beim Filtern erhalten, "kein Treffer" liefert
  kein VKZ.
- Der Stil-Knopf hat jetzt ein Haekchen "als Vorlage fuer alle Verfahren"; die
  Umschalttaste wirkt weiterhin.
- Abmelden ist ein eigener Knopf (verwirft den gespeicherten API-Key).
- Menue Web -> VLN Karten -> "Seitenleiste anzeigen" blendet sie wieder ein.
- tools/headless_panel_test.py: prueft Zustaende, Filter, Rueckrufe und rendert
  das Layout als PNG (QGIS 4.2 offscreen). Lauf am 08.09.2026: 17 Pruefungen,
  0 Fehler, 285 Verfahren in der Liste.
- README und metadata.txt (0.3.0) nachgezogen.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 08:28:11 +02:00

239 lines
9.8 KiB
Python

# -*- 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. OGC API — alle Layer laden, Stil speichern (mit Häkchen „für alle Verfahren")
4. Klassisch — die einzelnen GeoJSON-Datensätze, aktiven Layer hochladen
5. Statuszeile
"""
from qgis.PyQt.QtCore import Qt
from qgis.PyQt.QtWidgets import (QCheckBox, QComboBox, QDockWidget, QFrame, QGroupBox,
QHBoxLayout, QLabel, QLineEdit, 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, datensaetze=()):
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_ogc())
aussen.addWidget(self._bau_klassisch(datensaetze))
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_ogc(self):
kasten = QGroupBox("OGC API (empfohlen)")
lage = QVBoxLayout(kasten)
lage.setSpacing(6)
self.ogc_knopf = QPushButton("Alle Layer laden")
self.ogc_knopf.setToolTip("Verfahrenslayer, Vorerhebung und Maßnahmenblätter als "
"editierbare OAPIF-Layer; QGIS schreibt Änderungen direkt zurück")
self.ogc_knopf.clicked.connect(lambda: self._ruf("load_ogc"))
self.stil_knopf = QPushButton("Stil des aktiven Layers speichern")
self.stil_knopf.setToolTip("Braucht die Gruppe MODUL_KARTENSTIL (Verbandsmitarbeiter "
"immer)")
self.stil_knopf.clicked.connect(lambda: self._ruf("save_style"))
self.vorlage_haken = QCheckBox("als Vorlage für alle Verfahren")
self.vorlage_haken.setToolTip("Der Stil gilt dann für diese Art in jedem Verfahren, "
"z. B. für jede Karte alter Stand")
lage.addWidget(self.ogc_knopf)
lage.addWidget(self.stil_knopf)
lage.addWidget(self.vorlage_haken)
return kasten
def _bau_klassisch(self, datensaetze):
kasten = QGroupBox("Klassisch (GeoJSON)")
lage = QVBoxLayout(kasten)
lage.setSpacing(6)
lage.addWidget(_klein(QLabel("Lädt eine Kopie; Änderungen gehen erst mit "
"„Aktiven Layer hochladen“ zurück.")))
self.datensatz_knoepfe = []
for schluessel, beschriftung in datensaetze:
knopf = QPushButton(beschriftung)
knopf.clicked.connect(lambda _=False, s=schluessel: self._ruf("load_dataset", s))
lage.addWidget(knopf)
self.datensatz_knoepfe.append(knopf)
linie = QFrame()
linie.setFrameShape(QFrame.Shape.HLine)
linie.setFrameShadow(QFrame.Shadow.Sunken)
lage.addWidget(linie)
self.upload_knopf = QPushButton("Aktiven Layer hochladen")
self.upload_knopf.clicked.connect(lambda: self._ruf("upload"))
lage.addWidget(self.upload_knopf)
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.stil_knopf,
self.vorlage_haken, self.upload_knopf, *self.datensatz_knoepfe):
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._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 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)