Eigentümerdatei (CSV-Layer mit Nachname/Vorname) wählen, Name und/oder Vorname eingeben: Eine nicht modale Trefferliste zeigt Name, Anschrift, E-Mail und die Kennzeichen „verstorben“/„Rechtsperson“. Gleiche Personen mit gleicher Anschrift werden zusammengefasst; Groß-/Kleinschreibung und Umlautschreibweise spielen keine Rolle. Von QGIS als Zahl gelesene PLZ bekommen ihre führende Null zurück. Meldungen enthalten keine Namen. Die Auswahltabelle aus „Verfahrensgebiete aus Landkreis laden“ ist als baue_auswahltabelle herausgelöst und wird von der Trefferliste mitgenutzt. Zweiter Schritt von #56; Anzeigefenster und Korrekturen folgen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
345 lines
13 KiB
Python
345 lines
13 KiB
Python
"""sn_verfahrensgebiet/functions/verfahrensgebiet_dialoge.py – Dialoge zum
|
||
Laden eines Verfahrensgebiets aus ALKIS (Qt5/6/Mock-kompatibel).
|
||
|
||
Enthält außerdem die gemeinsame Auswahltabelle (:func:`baue_auswahltabelle`),
|
||
die auch die Eigentümer-Trefferliste (#56) nutzt.
|
||
"""
|
||
from typing import Any, Optional
|
||
from sn_basis.functions.qt_wrapper import (
|
||
QT_VERSION, exec_dialog, QInputDialog, QLineEdit,
|
||
QDialog, QDialogButtonBox, QListWidget, QListWidgetItem, QAbstractItemView,
|
||
QTableWidget, QTableWidgetItem, QTableWidgetSelectionRange,
|
||
QIcon, QVBoxLayout, QHBoxLayout, QLabel, QCheckBox,
|
||
DialogAccepted, ExtendedSelection, SelectRows, NoEditTriggers, AscendingOrder,
|
||
UserRole,
|
||
)
|
||
from sn_basis.functions.landkreise import ALLE_LANDKREISE
|
||
import os
|
||
|
||
# .../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 zeilen_sichtbar(zeilen_texte: list[list[str]], suchtext: str) -> list[bool]:
|
||
"""Ob jede Tabellenzeile zum Suchtext passt: Teilstring in irgendeiner Spalte,
|
||
ohne Groß-/Kleinschreibung. Ein leerer Suchtext zeigt alle Zeilen."""
|
||
such = (suchtext or "").strip().lower()
|
||
return [not such or any(such in (t or "").lower() for t in texte) for texte in zeilen_texte]
|
||
|
||
|
||
def baue_auswahltabelle(
|
||
layout: Any,
|
||
spaltenkoepfe: list[str],
|
||
zeilen: list[tuple[str, ...]],
|
||
platzhalter: str = "Suche …",
|
||
alle_auswaehlen: bool = True,
|
||
) -> Any:
|
||
"""Fügt Suchfeld, „Alle auswählen“, Auswahltabelle und Mehrfachauswahl-Hinweis
|
||
in ``layout`` ein und gibt die Tabelle zurück.
|
||
|
||
Gemeinsamer Baustein von :func:`ask_landkreis_objekte` und der
|
||
Eigentümer-Trefferliste (#56). Die Tabelle ist nach Spalte 0 aufsteigend
|
||
sortiert, per Klick auf den Spaltenkopf umsortierbar und erlaubt die
|
||
Mehrfachauswahl ganzer Zeilen. Spalte 0 trägt den Index der Zeile in
|
||
``zeilen`` (``UserRole``), damit die Auswahl auch nach dem Umsortieren
|
||
zugeordnet werden kann (:func:`ausgewaehlte_indizes`).
|
||
"""
|
||
suche_zeile = QHBoxLayout()
|
||
suche_zeile.addWidget(QLabel("Suche:"))
|
||
suche_feld = QLineEdit()
|
||
suche_feld.setPlaceholderText(platzhalter)
|
||
suche_zeile.addWidget(suche_feld)
|
||
layout.addLayout(suche_zeile)
|
||
|
||
alle_checkbox = None
|
||
if alle_auswaehlen:
|
||
alle_checkbox = QCheckBox("Alle auswählen")
|
||
layout.addWidget(alle_checkbox)
|
||
|
||
table = QTableWidget(len(zeilen), len(spaltenkoepfe))
|
||
table.setHorizontalHeaderLabels(spaltenkoepfe)
|
||
table.setSelectionMode(ExtendedSelection)
|
||
table.setSelectionBehavior(SelectRows)
|
||
table.setEditTriggers(NoEditTriggers)
|
||
table.verticalHeader().setVisible(False)
|
||
table.verticalHeader().setDefaultSectionSize(22) # flachere Zeilen
|
||
table.horizontalHeader().setStretchLastSection(True)
|
||
# Sortierung erst nach dem Befüllen einschalten, sonst wandern Zeilen beim setItem
|
||
table.setSortingEnabled(False)
|
||
for row, texte in enumerate(zeilen):
|
||
for col, text in enumerate(texte):
|
||
item = QTableWidgetItem(text)
|
||
if col == 0:
|
||
item.setData(UserRole, row)
|
||
table.setItem(row, col, item)
|
||
table.setSortingEnabled(True)
|
||
table.sortByColumn(0, AscendingOrder)
|
||
layout.addWidget(table, 1) # Stretch-Faktor: wächst mit Fenstergröße
|
||
|
||
def _suche_geaendert(text):
|
||
texte = [
|
||
[table.item(row, col).text() for col in range(table.columnCount())]
|
||
for row in range(table.rowCount())
|
||
]
|
||
for row, sichtbar in enumerate(zeilen_sichtbar(texte, text)):
|
||
table.setRowHidden(row, not sichtbar)
|
||
|
||
suche_feld.textChanged.connect(_suche_geaendert)
|
||
|
||
def _alle_auswaehlen_umschalten(checked):
|
||
table.clearSelection()
|
||
if checked:
|
||
letzte_spalte = table.columnCount() - 1
|
||
for row in range(table.rowCount()):
|
||
if not table.isRowHidden(row):
|
||
# setRangeSelected (statt selectRow) fügt der Selektion
|
||
# hinzu, ohne vorherige Zeilen wieder abzuwählen.
|
||
table.setRangeSelected(
|
||
QTableWidgetSelectionRange(row, 0, row, letzte_spalte), True
|
||
)
|
||
|
||
if alle_checkbox is not None:
|
||
alle_checkbox.stateChanged.connect(_alle_auswaehlen_umschalten)
|
||
|
||
layout.addWidget(QLabel("<small>Mehrfachauswahl mit Strg + Umschalt</small>"))
|
||
return table
|
||
|
||
|
||
def ausgewaehlte_indizes(table: Any) -> list[int]:
|
||
"""Indizes der ausgewählten Zeilen, bezogen auf ``zeilen`` aus
|
||
:func:`baue_auswahltabelle`, aufsteigend."""
|
||
zeilen = {item.row() for item in table.selectedItems()}
|
||
return sorted(int(table.item(row, 0).data(UserRole)) for row in zeilen)
|
||
|
||
|
||
def ask_bror_kombinationen(
|
||
parent: Any,
|
||
kombinationen: list[tuple[str, str]],
|
||
) -> 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. Es können ein oder
|
||
mehrere Objekte gemeinsam ausgewählt und geladen (verschmolzen) werden.
|
||
|
||
Parameters
|
||
----------
|
||
parent :
|
||
Eltern-Widget oder None.
|
||
kombinationen :
|
||
Sortierte Liste von ``(adv_name, bezeichnung)``-Tupeln.
|
||
|
||
Returns
|
||
-------
|
||
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 []
|
||
|
||
if QT_VERSION == 0: # Mock-Modus
|
||
print(f"Mock-Modus: ask_bror_kombinationen → {kombinationen[:1]}")
|
||
return kombinationen[:1]
|
||
|
||
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:"
|
||
)
|
||
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]],
|
||
) -> list[tuple[str, str]]:
|
||
"""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). Ob nach dem Laden der weitere Workflow
|
||
(Gemarkung/Flurstücke/Flächenvergleich) fortgesetzt wird, klärt eine
|
||
separate Stop-Abfrage nach dem Laden – nicht mehr dieser Dialog.
|
||
|
||
Parameters
|
||
----------
|
||
parent :
|
||
Eltern-Widget oder None.
|
||
kombinationen :
|
||
Sortierte Liste von ``(adv_name, bezeichnung)``-Tupeln.
|
||
|
||
Returns
|
||
-------
|
||
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 []
|
||
|
||
if QT_VERSION == 0: # Mock-Modus
|
||
print(f"Mock-Modus: ask_landkreis_objekte → {kombinationen[:1]}")
|
||
return kombinationen[:1]
|
||
|
||
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)
|
||
|
||
# Standardsortierung nach Bezeichnung; per Klick auf Spaltenkopf änderbar.
|
||
sortierte_kombinationen = sorted(kombinationen, key=lambda kb: kb[1])
|
||
table = baue_auswahltabelle(
|
||
layout,
|
||
["Nummer", "Name"],
|
||
[(bez, adv) for adv, bez in sortierte_kombinationen],
|
||
platzhalter="Nummer oder Name …",
|
||
)
|
||
table.resizeColumnToContents(0)
|
||
table.setColumnWidth(0, max(110, table.columnWidth(0)))
|
||
table.selectRow(0)
|
||
|
||
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
||
buttons.accepted.connect(dialog.accept)
|
||
buttons.rejected.connect(dialog.reject)
|
||
layout.addWidget(buttons)
|
||
|
||
dialog.adjustSize()
|
||
# Bezeichnungs-Spalte + großzügige Breite für den ADV-Namen (lange Namen
|
||
# sollen möglichst ohne Abschneiden sichtbar sein) + Rahmen/Scrollbar-Puffer.
|
||
breite = max(dialog.sizeHint().width(), table.columnWidth(0) + 480)
|
||
|
||
# Tabelle 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(sortierte_kombinationen), 15)
|
||
zeilenhoehe = table.verticalHeader().defaultSectionSize() if table.rowCount() else 20
|
||
kopfzeile_hoehe = table.horizontalHeader().height()
|
||
# + Puffer für eine ggf. eingeblendete horizontale Scrollbar (bei langen
|
||
# Einträgen), damit diese nicht die letzte Zeile abschneidet.
|
||
scrollbar_puffer = table.horizontalScrollBar().sizeHint().height()
|
||
table_hoehe = (
|
||
zeilenhoehe * sichtbare_zeilen + kopfzeile_hoehe
|
||
+ 2 * table.frameWidth() + 4 + scrollbar_puffer
|
||
)
|
||
table.setMinimumHeight(table_hoehe)
|
||
|
||
dialog.resize(breite, dialog.sizeHint().height())
|
||
|
||
if exec_dialog(dialog) != DialogAccepted:
|
||
return []
|
||
|
||
ausgewaehlte_zeilen = sorted(set(item.row() for item in table.selectedItems()))
|
||
ausgewaehlt = [(table.item(row, 1).text(), table.item(row, 0).text()) for row in ausgewaehlte_zeilen]
|
||
return ausgewaehlt
|
||
|
||
|
||
def ask_landkreis_auswahl(
|
||
parent: Any,
|
||
landkreise: list[tuple[str, str]],
|
||
) -> Optional[str]:
|
||
"""Fragt per Dropdown-Dialog einen Landkreis ab (Fallback, wenn weder
|
||
Verfahrensnummer noch Landkreis in den Projekteigenschaften gesetzt ist).
|
||
|
||
Parameters
|
||
----------
|
||
parent :
|
||
Eltern-Widget oder None.
|
||
landkreise :
|
||
Liste von ``(kennzahl, name)``-Tupeln, z.B. ``LANDKREISE`` aus
|
||
:mod:`sn_basis.functions.landkreise`.
|
||
|
||
Die Auswahlliste beginnt mit einem leeren Platzhalter (der Nutzer muss
|
||
aktiv einen Eintrag wählen) und enthält zusätzlich "Alle Landkreise"
|
||
(Sentinel :data:`~sn_basis.functions.landkreise.ALLE_LANDKREISE`).
|
||
|
||
Returns
|
||
-------
|
||
Optional[str]
|
||
Gewählte Landkreiskennzahl, :data:`ALLE_LANDKREISE` bei Auswahl aller
|
||
Landkreise, oder ``None`` bei Abbruch, leerem Platzhalter oder leerer
|
||
Landkreisliste.
|
||
"""
|
||
if not landkreise:
|
||
return None
|
||
|
||
if QT_VERSION == 0: # Mock-Modus
|
||
print("Mock-Modus: ask_landkreis_auswahl → None (keine aktive Auswahl)")
|
||
return None
|
||
|
||
ALLE_LABEL = "Alle Landkreise"
|
||
namen = [""] + [ALLE_LABEL] + [name for _, name in landkreise]
|
||
|
||
dialog = QInputDialog(parent)
|
||
dialog.setWindowTitle("Landkreis auswählen")
|
||
if os.path.exists(_WAPPEN_ICON):
|
||
dialog.setWindowIcon(QIcon(_WAPPEN_ICON))
|
||
dialog.setLabelText("Landkreis:")
|
||
dialog.setComboBoxItems(namen)
|
||
dialog.setComboBoxEditable(False)
|
||
|
||
if exec_dialog(dialog) != DialogAccepted:
|
||
return None
|
||
|
||
name = dialog.textValue()
|
||
if not name:
|
||
return None
|
||
if name == ALLE_LABEL:
|
||
return ALLE_LANDKREISE
|
||
|
||
name_zu_kennzahl = {n: k for k, n in landkreise}
|
||
return name_zu_kennzahl.get(str(name))
|