Landkreis-Dropdown, BROR-Mehrfachauswahl-Dialog und UX-Fixes

- Landkreis-Feld in den Projekteigenschaften als Dropdown statt Freitext,
  Landkreiskennzahl zusätzlich als eigene Projektvariable verfügbar.
- ask_bror_kombination(en) auf echte Mehrfachauswahl umgestellt, inkl.
  Titel, Wappen-Icon, kompakter/anpassbarer Größe und Scrollbar-Fix.
- Neuer Dialog ask_landkreis_objekte für die landkreisweite Objektsuche
  (Mehrfachauswahl, "Alle auswählen", "Nur laden ohne weiteren Ablauf").
- qt_wrapper um QDialog/QDialogButtonBox/QListWidget/QListWidgetItem/
  QAbstractItemView/QIcon sowie QComboBox-userData/currentData erweitert.
This commit is contained in:
Michael Otto
2026-08-27 15:13:43 +02:00
parent 7a5ed12797
commit 749d6d6f52
7 changed files with 362 additions and 49 deletions
+5
View File
@@ -1,3 +1,8 @@
- Landkreis-Dropdown (statt Freitext) in den Projekteigenschaften ergänzt, inkl. Landkreiskennzahl als eigene Projektvariable
- Auswahldialog für mehrere BauRaumOderBodenordnungsrecht-Treffer auf Mehrfachauswahl umgestellt (mehrere Objekte gemeinsam laden/verschmelzen)
- Neuer Dialog zur landkreisweiten Objektsuche (Mehrfachauswahl, "Alle auswählen", "Nur laden ohne weiteren Ablauf")
- Dialoge: Titel, Wappen-Icon, kompaktere Größe, Scrollbar-Fix und größenveränderbare Liste ergänzt
---
Version 26.7.1-unstable:
- Verfahrensgebiet als shape laden, inkl. Abgleich mit BROBOR-Objekt
---
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

+167 -28
View File
@@ -6,7 +6,10 @@ from typing import Literal, Optional
from sn_basis.functions.qt_wrapper import (
QMessageBox, YES, NO, CANCEL, QT_VERSION, exec_dialog, ICON_QUESTION,
QProgressDialog, QCoreApplication, Qt, QInputDialog, QLineEdit,
QDialog, QDialogButtonBox, QListWidget, QListWidgetItem, QAbstractItemView,
QIcon, QVBoxLayout, QLabel, QCheckBox, DialogAccepted, ExtendedSelection,
)
import os
def ask_yes_no(
title: str,
@@ -391,15 +394,21 @@ def create_progress_dialog(total: int, title: str = "Fortschritt", label: str =
return ProgressDialog(total, title, label)
def ask_bror_kombination(
# .../python/plugins (drei Ebenen über dieser Datei)
_PLUGINS_DIR = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
_WAPPEN_ICON = os.path.join(_PLUGINS_DIR, "sn_basis", "favicon.ico")
def ask_bror_kombinationen(
parent: Any,
kombinationen: list[tuple[str, str]],
) -> Optional[tuple[str, str]]:
"""Zeigt Dialog zur Auswahl einer BROR-Kombination (adv_name + bezeichnung).
) -> list[tuple[str, str]]:
"""Zeigt Dialog zur Auswahl einer oder mehrerer BROR-Kombinationen
(adv_name + bezeichnung).
Wird aufgerufen, wenn der WFS mehrere verschiedene Kombinationen aus
``adv_name`` und ``bezeichnung`` zurückliefert, sodass der Nutzer
entscheiden muss, welche Objekte in die Pipeline einfließen sollen.
``adv_name`` und ``bezeichnung`` zurückliefert. Es können ein oder
mehrere Objekte gemeinsam ausgewählt und geladen (verschmolzen) werden.
Parameters
----------
@@ -410,33 +419,163 @@ def ask_bror_kombination(
Returns
-------
Optional[tuple[str, str]]
Das gewählte ``(adv_name, bezeichnung)``-Tupel oder ``None`` bei
Abbruch bzw. leerer Eingabe.
list[tuple[str, str]]
Die gewählten ``(adv_name, bezeichnung)``-Tupel. Leere Liste bei
Abbruch oder wenn nichts ausgewählt wurde.
"""
if not kombinationen:
return None
return []
if QT_VERSION == 0: # Mock-Modus
print(f"Mock-Modus: ask_bror_kombination → {kombinationen[0]}")
return kombinationen[0]
print(f"Mock-Modus: ask_bror_kombinationen{kombinationen[:1]}")
return kombinationen[:1]
items = [f"{adv} | {bez}" for adv, bez in kombinationen]
item, ok = QInputDialog.getItem(
parent,
"BROR-Objekt auswählen",
"Mehrere BauRaumOderBodenordnungsrecht-Objekte gefunden.\n"
"Bitte wählen Sie die gewünschte Kombination\n"
"aus ADV-Name und Bezeichnung:",
items,
0,
False,
dialog = QDialog(parent)
dialog.setWindowTitle("Verfahrensgebiet aus ALKIS laden")
if os.path.exists(_WAPPEN_ICON):
dialog.setWindowIcon(QIcon(_WAPPEN_ICON))
layout = QVBoxLayout(dialog)
hinweis = QLabel(
f"Es wurden {len(kombinationen)} Objekte gefunden.\n\n"
"Bitte wählen Sie eines oder mehrere Objekte aus:"
)
if not ok:
return None
try:
idx = items.index(item)
return kombinationen[idx]
except ValueError:
return None
hinweis.setWordWrap(True)
layout.addWidget(hinweis)
list_widget = QListWidget()
list_widget.setSelectionMode(ExtendedSelection)
for adv, bez in kombinationen:
item = QListWidgetItem(f"{adv} | {bez}")
list_widget.addItem(item)
list_widget.item(0).setSelected(True)
layout.addWidget(list_widget, 1) # Stretch-Faktor: wächst mit Fenstergröße
mehrfachauswahl_hinweis = QLabel("<small>Mehrfachauswahl mit Strg + Umschalt</small>")
layout.addWidget(mehrfachauswahl_hinweis)
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
buttons.accepted.connect(dialog.accept)
buttons.rejected.connect(dialog.reject)
layout.addWidget(buttons)
dialog.adjustSize()
breite = dialog.sizeHint().width()
# Liste auf max. 5 sichtbare Zeilen begrenzen, Rest per Scrollbar
sichtbare_zeilen = min(len(kombinationen), 5)
zeilenhoehe = list_widget.sizeHintForRow(0) if list_widget.count() else 20
# + Puffer für eine ggf. eingeblendete horizontale Scrollbar (bei langen
# Einträgen), damit diese nicht die letzte Zeile abschneidet.
scrollbar_puffer = list_widget.horizontalScrollBar().sizeHint().height()
list_hoehe = zeilenhoehe * sichtbare_zeilen + 2 * list_widget.frameWidth() + 4 + scrollbar_puffer
list_widget.setMinimumHeight(list_hoehe)
dialog.resize(breite, dialog.sizeHint().height())
if exec_dialog(dialog) != DialogAccepted:
return []
ausgewaehlte_indizes = sorted(list_widget.row(item) for item in list_widget.selectedItems())
return [kombinationen[i] for i in ausgewaehlte_indizes]
def ask_landkreis_objekte(
parent: Any,
kombinationen: list[tuple[str, str]],
) -> tuple[list[tuple[str, str]], bool]:
"""Zeigt Dialog zur Auswahl eines, mehrerer oder aller BROR-Objekte eines
Landkreises (Fallback, wenn keine Verfahrensnummer gesetzt ist).
Wie :func:`ask_bror_kombinationen`, zusätzlich mit einer "Alle
auswählen"-Checkbox (da hier potenziell deutlich mehr Treffer als bei der
VKZ-Suche auftreten) und einer "Nur laden"-Checkbox, mit der gesteuert
wird, ob nach dem Laden der weitere Workflow (Gemarkung/Flurstücke/
Flächenvergleich) fortgesetzt werden soll.
Parameters
----------
parent :
Eltern-Widget oder None.
kombinationen :
Sortierte Liste von ``(adv_name, bezeichnung)``-Tupeln.
Returns
-------
tuple[list[tuple[str, str]], bool]
``(ausgewaehlte_kombinationen, nur_laden)``. ``([], False)`` bei
Abbruch oder wenn nichts ausgewählt wurde.
"""
if not kombinationen:
return [], False
if QT_VERSION == 0: # Mock-Modus
print(f"Mock-Modus: ask_landkreis_objekte → {kombinationen[:1]}")
return kombinationen[:1], False
dialog = QDialog(parent)
dialog.setWindowTitle("Verfahrensgebiete aus Landkreis laden")
if os.path.exists(_WAPPEN_ICON):
dialog.setWindowIcon(QIcon(_WAPPEN_ICON))
layout = QVBoxLayout(dialog)
hinweis = QLabel(
f"Es wurden {len(kombinationen)} Objekte gefunden.\n\n"
"Bitte wählen Sie eines oder mehrere Objekte aus:"
)
hinweis.setWordWrap(True)
layout.addWidget(hinweis)
alle_auswaehlen = QCheckBox("Alle auswählen")
layout.addWidget(alle_auswaehlen)
list_widget = QListWidget()
list_widget.setSelectionMode(ExtendedSelection)
for adv, bez in kombinationen:
item = QListWidgetItem(f"{bez} | {adv}")
list_widget.addItem(item)
list_widget.item(0).setSelected(True)
layout.addWidget(list_widget, 1) # Stretch-Faktor: wächst mit Fenstergröße
def _alle_auswaehlen_umschalten(checked):
if checked:
list_widget.selectAll()
else:
list_widget.clearSelection()
alle_auswaehlen.stateChanged.connect(_alle_auswaehlen_umschalten)
mehrfachauswahl_hinweis = QLabel("<small>Mehrfachauswahl mit Strg + Umschalt</small>")
layout.addWidget(mehrfachauswahl_hinweis)
nur_laden_checkbox = QCheckBox("Nur laden (kein weiterer Ablauf)")
layout.addWidget(nur_laden_checkbox)
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
buttons.accepted.connect(dialog.accept)
buttons.rejected.connect(dialog.reject)
layout.addWidget(buttons)
dialog.adjustSize()
breite = dialog.sizeHint().width()
# Liste auf max. 15 sichtbare Zeilen begrenzen, Rest per Scrollbar hier
# potenziell deutlich mehr Treffer als bei der VKZ-Suche, daher großzügiger
# bemessen als bei ask_bror_kombinationen (dort 5).
sichtbare_zeilen = min(len(kombinationen), 15)
zeilenhoehe = list_widget.sizeHintForRow(0) if list_widget.count() else 20
# + Puffer für eine ggf. eingeblendete horizontale Scrollbar (bei langen
# Einträgen), damit diese nicht die letzte Zeile abschneidet.
scrollbar_puffer = list_widget.horizontalScrollBar().sizeHint().height()
list_hoehe = zeilenhoehe * sichtbare_zeilen + 2 * list_widget.frameWidth() + 4 + scrollbar_puffer
list_widget.setMinimumHeight(list_hoehe)
dialog.resize(breite, dialog.sizeHint().height())
if exec_dialog(dialog) != DialogAccepted:
return [], False
ausgewaehlte_indizes = sorted(list_widget.row(item) for item in list_widget.selectedItems())
ausgewaehlt = [kombinationen[i] for i in ausgewaehlte_indizes]
return ausgewaehlt, nur_laden_checkbox.isChecked()
+28
View File
@@ -0,0 +1,28 @@
"""sn_basis/functions/landkreise.py Sächsische Landkreise/Helfer mit Kennzahl.
Quelle: landkreiskennzahlen.csv
"""
#: Liste von (Landkreiskennzahl, Landkreis/Helfer)-Tupeln, in CSV-Reihenfolge.
LANDKREISE: list[tuple[str, str]] = [
("11", "Chemnitz, Stadt"),
("12", "Dresden, Stadt"),
("13", "Leipzig, Stadt"),
("21", "Erzgebirgskreis"),
("22", "Mittelsachsen"),
("23", "Vogtlandkreis"),
("24", "Zwickau"),
("25", "Bautzen"),
("26", "Görlitz"),
("27", "Meißen"),
("28", "Sächsische Schweiz-Osterzgebirge"),
("29", "Leipzig"),
("30", "Nordsachsen"),
("80", "LfULG"),
]
#: Kennzahl -> Name
KENNZAHL_ZU_NAME: dict[str, str] = dict(LANDKREISE)
#: Name -> Kennzahl
NAME_ZU_KENNZAHL: dict[str, str] = {name: kennzahl for kennzahl, name in LANDKREISE}
+119 -10
View File
@@ -46,6 +46,12 @@ QCheckBox: Type[Any] = object
QHBoxLayout: Type[Any] = object
QFont: Type[Any] = object
QSettings: Type[Any] = object
QDialog: Type[Any] = object
QDialogButtonBox: Type[Any] = object
QListWidget: Type[Any] = object
QListWidgetItem: Type[Any] = object
QAbstractItemView: Type[Any] = object
QIcon: Type[Any] = object
def exec_dialog(dialog: Any) -> Any:
@@ -92,8 +98,13 @@ try:
QComboBox as _QComboBox,
QCheckBox as _QCheckBox,
QHBoxLayout as _QHBoxLayout,
QDialog as _QDialog,
QDialogButtonBox as _QDialogButtonBox,
QListWidget as _QListWidget,
QListWidgetItem as _QListWidgetItem,
QAbstractItemView as _QAbstractItemView,
)
from qgis.PyQt.QtGui import QFont as _QFont
from qgis.PyQt.QtGui import QFont as _QFont, QIcon as _QIcon
from qgis.PyQt.QtCore import (
QEventLoop as _QEventLoop,
QTimer as _QTimer,
@@ -143,6 +154,12 @@ try:
QHBoxLayout = _QHBoxLayout
QFont = _QFont
QSettings = _QSettings
QDialog = _QDialog
QDialogButtonBox = _QDialogButtonBox
QListWidget = _QListWidget
QListWidgetItem = _QListWidgetItem
QAbstractItemView = _QAbstractItemView
QIcon = _QIcon
# ✅ QT6 ENUMS
YES = QMessageBox.StandardButton.Yes
NO = QMessageBox.StandardButton.No
@@ -163,7 +180,9 @@ try:
DockWidgetClosable = QDockWidget.DockWidgetFeature.DockWidgetClosable
DockAreaLeft = Qt.DockWidgetArea.LeftDockWidgetArea
DockAreaRight = Qt.DockWidgetArea.RightDockWidgetArea
DialogAccepted = QDialog.DialogCode.Accepted
ExtendedSelection = QAbstractItemView.SelectionMode.ExtendedSelection
def exec_dialog(dialog: Any) -> Any:
return dialog.exec()
@@ -194,8 +213,13 @@ except (ImportError, AttributeError):
QComboBox as _QComboBox,
QCheckBox as _QCheckBox,
QHBoxLayout as _QHBoxLayout,
QDialog as _QDialog,
QDialogButtonBox as _QDialogButtonBox,
QListWidget as _QListWidget,
QListWidgetItem as _QListWidgetItem,
QAbstractItemView as _QAbstractItemView,
)
from PyQt5.QtGui import QFont as _QFont
from PyQt5.QtGui import QFont as _QFont, QIcon as _QIcon
from PyQt5.QtCore import (
QEventLoop as _QEventLoop,
QTimer as _QTimer,
@@ -243,7 +267,13 @@ except (ImportError, AttributeError):
QHBoxLayout= _QHBoxLayout
QFont = _QFont
QSettings = _QSettings
QDialog = _QDialog
QDialogButtonBox = _QDialogButtonBox
QListWidget = _QListWidget
QListWidgetItem = _QListWidgetItem
QAbstractItemView = _QAbstractItemView
QIcon = _QIcon
# ✅ PYQT5 ENUMS
YES = QMessageBox.Yes
NO = QMessageBox.No
@@ -265,7 +295,9 @@ except (ImportError, AttributeError):
DockWidgetClosable = QDockWidget.DockWidgetClosable
DockAreaLeft = Qt.LeftDockWidgetArea
DockAreaRight = Qt.RightDockWidgetArea
DialogAccepted = QDialog.Accepted
ExtendedSelection = QAbstractItemView.ExtendedSelection
def exec_dialog(dialog: Any) -> Any:
return dialog.exec_()
@@ -482,13 +514,19 @@ except (ImportError, AttributeError):
class _MockComboBox:
def __init__(self, parent=None):
self._items = []
self._items = [] # Liste von (text, userData)
self._index = -1
self.currentTextChanged = type('Signal', (), {'connect': lambda s, cb: None, 'emit': lambda s, v: None})()
def addItem(self, text: str) -> None: self._items.append(text)
def addItem(self, text: str, userData=None) -> None:
self._items.append((text, userData))
if self._index < 0:
self._index = 0
def addItems(self, items): [self.addItem(it) for it in items]
def findText(self, text: str) -> int:
return self._items.index(text) if text in self._items else -1
def findText(self, text: str) -> int:
for i, (t, _) in enumerate(self._items):
if t == text:
return i
return -1
def setCurrentIndex(self, idx: int) -> None:
if 0 <= idx < len(self._items):
self._index = idx
@@ -497,7 +535,11 @@ except (ImportError, AttributeError):
idx = self.findText(text)
if idx >= 0: self.setCurrentIndex(idx)
def currentText(self) -> str:
return self._items[self._index] if 0 <= self._index < len(self._items) else ""
return self._items[self._index][0] if 0 <= self._index < len(self._items) else ""
def currentData(self):
return self._items[self._index][1] if 0 <= self._index < len(self._items) else None
def count(self) -> int:
return len(self._items)
QComboBox = _MockComboBox
@@ -605,6 +647,73 @@ except (ImportError, AttributeError):
self._checked = checked
QCheckBox = _MockQCheckBox
DialogAccepted = 1
ExtendedSelection = 3
class _MockQDialog(_MockWidget):
DialogCode = type("DialogCode", (), {"Accepted": DialogAccepted, "Rejected": 0})
def __init__(self, *args, **kwargs):
self._layout = None
def setWindowTitle(self, title: str) -> None: pass
def setWindowIcon(self, icon) -> None: pass
def exec(self) -> int: return DialogAccepted
def exec_(self) -> int: return DialogAccepted
def accept(self) -> None: pass
def reject(self) -> None: pass
def resize(self, *args, **kwargs) -> None: pass
def adjustSize(self) -> None: pass
def sizeHint(self):
return type("Size", (), {"width": lambda self=None: 200, "height": lambda self=None: 100})()
QDialog = _MockQDialog
class _MockQDialogButtonBox(_MockWidget):
StandardButton = type("StandardButton", (), {"Ok": 1, "Cancel": 2})
Ok = 1
Cancel = 2
def __init__(self, *args, **kwargs):
self.accepted = type('Signal', (), {'connect': lambda s, cb: None})()
self.rejected = type('Signal', (), {'connect': lambda s, cb: None})()
QDialogButtonBox = _MockQDialogButtonBox
class _MockQListWidgetItem:
def __init__(self, text: str = ""):
self._text = text
self._selected = False
def setSelected(self, value: bool) -> None: self._selected = value
def isSelected(self) -> bool: return self._selected
QListWidgetItem = _MockQListWidgetItem
class _MockQListWidget(_MockWidget):
def __init__(self, *args, **kwargs):
self._items = []
def addItem(self, item) -> None: self._items.append(item)
def item(self, index: int): return self._items[index] if 0 <= index < len(self._items) else None
def count(self) -> int: return len(self._items)
def row(self, item) -> int: return self._items.index(item) if item in self._items else -1
def selectedItems(self): return [i for i in self._items if i.isSelected()]
def setSelectionMode(self, *args, **kwargs) -> None: pass
def sizeHintForRow(self, index: int) -> int: return 20
def setFixedHeight(self, height: int) -> None: pass
def frameWidth(self) -> int: return 1
QListWidget = _MockQListWidget
class _MockQAbstractItemView:
SelectionMode = type("SelectionMode", (), {"ExtendedSelection": ExtendedSelection})
QAbstractItemView = _MockQAbstractItemView
class _MockQIcon:
def __init__(self, *args, **kwargs): pass
QIcon = _MockQIcon
def exec_dialog(dialog: Any) -> Any:
return YES
# --------------------------- TEST ---------------------------
+1
View File
@@ -20,6 +20,7 @@ class SettingsLogic:
"amt",
"behoerde",
"landkreis_user",
"landkreis_kennzahl",
"sachgebiet",
"bezeichnung",
"name",
+42 -11
View File
@@ -1,5 +1,5 @@
"""
sn_basis/ui/tabs/settings_tab.py Einstellungen-Tab für Projekteinformationen.
sn_basis/ui/tabs/settings_tab.py Einstellungen-Tab für Projektinformationen.
Verwaltet benutzerspezifische und projektspezifische Einstellungen.
"""
@@ -8,28 +8,33 @@ from sn_basis.functions.qt_wrapper import (
QGridLayout,
QLabel,
QLineEdit,
QComboBox,
QGroupBox,
QVBoxLayout,
QPushButton,
)
from sn_basis.functions.settings_logic import SettingsLogic
from sn_basis.functions.landkreise import LANDKREISE
#: Feldschlüssel, die als Landkreis-Dropdown (statt Freitext) dargestellt werden.
_LANDKREIS_DROPDOWN_FELDER = {"landkreis_user"}
class SettingsTab(QWidget):
"""
Tab-Widget für die Verwaltung von Projekt- und Benutzereinstellungen.
Attributes:
tab_title: Anzeigetitel des Tabs ("Projekteigenschaften")
logic: SettingsLogic-Instanz für persistente Einstellungen
"""
tab_title = "Projekteigenschaften"
def __init__(self, parent=None):
"""
Initialisiert den Settings-Tab mit Eingabefeldern.
Args:
parent: Übergeordnetes Widget (Standard: None)
"""
@@ -64,7 +69,7 @@ class SettingsTab(QWidget):
self.user_inputs = {}
for row, (key, label) in enumerate(self.user_fields.items()):
input_widget = QLineEdit()
input_widget = self._build_input(key)
self.user_inputs[key] = input_widget
user_layout.addWidget(QLabel(label), row, 0)
@@ -80,7 +85,7 @@ class SettingsTab(QWidget):
self.project_inputs = {}
for row, (key, label) in enumerate(self.project_fields.items()):
input_widget = QLineEdit()
input_widget = self._build_input(key)
self.project_inputs[key] = input_widget
project_layout.addWidget(QLabel(label), row, 0)
@@ -107,14 +112,35 @@ class SettingsTab(QWidget):
# Daten laden
self.load_data()
# ---------------------------------------------------------
# Eingabewidget je Feld erzeugen
# ---------------------------------------------------------
def _build_input(self, key: str):
if key in _LANDKREIS_DROPDOWN_FELDER:
combo = QComboBox()
combo.addItem("", None) # Platzhalter: keine Vorbelegung bei leerem Projekt
for kennzahl, name in LANDKREISE:
combo.addItem(name, kennzahl)
return combo
return QLineEdit()
# ---------------------------------------------------------
# Speichern
# ---------------------------------------------------------
def save_data(self):
fields = {
key: widget.text()
for key, widget in {**self.user_inputs, **self.project_inputs}.items()
}
fields = {}
for key, widget in {**self.user_inputs, **self.project_inputs}.items():
if isinstance(widget, QComboBox):
fields[key] = widget.currentText()
else:
fields[key] = widget.text()
# Landkreiskennzahl aus der Dropdown-Auswahl zusätzlich als eigene
# Variable bereitstellen (siehe landkreise.py).
landkreis_widget = self.user_inputs.get("landkreis_user")
if isinstance(landkreis_widget, QComboBox):
fields["landkreis_kennzahl"] = landkreis_widget.currentData() or ""
self.logic.save(fields)
# ---------------------------------------------------------
@@ -123,4 +149,9 @@ class SettingsTab(QWidget):
def load_data(self):
data = self.logic.load()
for key, widget in {**self.user_inputs, **self.project_inputs}.items():
widget.setText(data.get(key, ""))
if isinstance(widget, QComboBox):
gespeicherter_name = data.get(key, "")
index = widget.findText(gespeicherter_name)
widget.setCurrentIndex(index if index >= 0 else 0)
else:
widget.setText(data.get(key, ""))