Beim Erstellen einer §38-/§41-Vorlage wertet BasemapLegende die Kacheln des sichtbaren basemap.de-Layers im Verfahrensgebiet (wahlweise Druckbereich) kachelweise über selectByGeometry aus, prüft die Renderer-Styles mit derselben Zoomlogik wie QGIS und bildet daraus Legendeneinträge. Ein Mehrfachauswahl-Dialog (neu: ask_mehrfachauswahl) lässt Kategorien abwählen; die Auswahl wird im Projekt gemerkt. Die gewählten Einträge stehen mit der Signatur des Projektlayers (zoomabhängige Werte festgeschrieben) in einem eigenen Legendenelement über der Quellenangabe. Ein eigenes Element ist nötig, weil die nach Karteninhalt filternde Hauptlegende die Hilfslayer ausblenden würde. Wrapper um die nötigen QGIS-/Qt-Symbole mit Mock ergänzt (eigener Block in qgiscore_wrapper; QgsRectangle hatte bisher keinen Mock). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
522 lines
16 KiB
Python
522 lines
16 KiB
Python
"""
|
||
sn_basis/functions/dialog_wrapper.py – Benutzer-Dialoge (Qt5/6/Mock-kompatibel)
|
||
"""
|
||
from typing import Any
|
||
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,
|
||
)
|
||
|
||
def ask_yes_no(
|
||
title: str,
|
||
message: str,
|
||
default: bool = True,
|
||
parent: Any = None,
|
||
) -> bool:
|
||
"""
|
||
Stellt Ja/Nein-Frage. Funktioniert in PyQt5/6 UND Mock-Modus.
|
||
"""
|
||
try:
|
||
if QT_VERSION == 0: # Mock-Modus
|
||
print(f"🔍 Mock-Modus: ask_yes_no('{title}') → {default}")
|
||
return default
|
||
|
||
# ✅ KORREKT: Verwende YES/NO-Aliase aus qt_wrapper!
|
||
buttons = YES | NO
|
||
default_button = YES if default else NO
|
||
|
||
result = QMessageBox.question(
|
||
parent, title, message, buttons, default_button
|
||
)
|
||
|
||
# ✅ int(result) == int(YES) funktioniert Qt5/6/Mock
|
||
print(f"DEBUG ask_yes_no: result={result}, YES={YES}, match={int(result) == int(YES)}")
|
||
return int(result) == int(YES)
|
||
|
||
except Exception as e:
|
||
print(f"⚠️ ask_yes_no Fehler: {e}")
|
||
return default
|
||
|
||
|
||
def show_info_dialog(title: str, message: str, parent: Any = None) -> None:
|
||
"""
|
||
Zeigt einen modalen Info-Dialog mit OK-Button.
|
||
Blockiert bis der Nutzer bestätigt.
|
||
|
||
von Test abgesehen, weil vernachlässigbar; HD 18.09.2026
|
||
"""
|
||
try:
|
||
if QT_VERSION == 0: # Mock-Modus
|
||
print(f"Mock-Modus: show_info_dialog('{title}')")
|
||
return
|
||
QMessageBox.information(parent, title, message)
|
||
except Exception as e:
|
||
print(f"⚠️ show_info_dialog Fehler: {e}")
|
||
|
||
|
||
def ask_text(
|
||
title: str,
|
||
label: str,
|
||
default_text: str = "",
|
||
parent: Any = None,
|
||
) -> tuple[str, bool]:
|
||
"""Zeigt einen modalen Texteingabe-Dialog und gibt Text + OK-Status zurück.
|
||
|
||
von Test abgesehen, weil vernachlässigbar; HD 18.09.2026
|
||
"""
|
||
try:
|
||
if QT_VERSION == 0: # Mock-Modus
|
||
print(f"Mock-Modus: ask_text('{title}') -> '{default_text}'")
|
||
return default_text, True
|
||
# PyQt6: QLineEdit.EchoMode.Normal / PyQt5: QLineEdit.Normal
|
||
echo_mode = (
|
||
getattr(QLineEdit, "Normal", None)
|
||
or getattr(getattr(QLineEdit, "EchoMode", None), "Normal", None)
|
||
or 0
|
||
)
|
||
text, accepted = QInputDialog.getText(
|
||
parent,
|
||
title,
|
||
label,
|
||
echo_mode,
|
||
default_text,
|
||
)
|
||
return str(text or ""), bool(accepted)
|
||
except Exception as e:
|
||
print(f"⚠️ ask_text Fehler: {e}")
|
||
return default_text, False
|
||
|
||
|
||
OverwriteDecision = Optional[Literal["overwrite", "append", "cancel"]]
|
||
|
||
|
||
def ask_overwrite_append_cancel_custom(
|
||
parent,
|
||
title: str,
|
||
message: str,
|
||
) -> Literal["overwrite", "append", "cancel"]:
|
||
"""Zeigt Dialog mit benutzerdefinierten Buttons: Überschreiben/Anhängen/Abbrechen.
|
||
|
||
Parameters
|
||
----------
|
||
parent :
|
||
Eltern-Widget oder None.
|
||
title : str
|
||
Dialog-Titel.
|
||
message : str
|
||
Hauptmeldung mit Erklärung.
|
||
|
||
Returns
|
||
-------
|
||
Literal["overwrite", "append", "cancel"]
|
||
Genaue Entscheidung des Nutzers.
|
||
"""
|
||
msg = QMessageBox(parent)
|
||
msg.setIcon(ICON_QUESTION)
|
||
msg.setWindowTitle(title)
|
||
msg.setText(message)
|
||
|
||
# Eigene Buttons mit exakten Texten
|
||
overwrite_btn = msg.addButton("Überschreiben", QMessageBox.ButtonRole.AcceptRole)
|
||
append_btn = msg.addButton("Anhängen", QMessageBox.ButtonRole.ActionRole)
|
||
cancel_btn = msg.addButton("Abbrechen", QMessageBox.ButtonRole.RejectRole)
|
||
|
||
exec_dialog(msg)
|
||
|
||
clicked = msg.clickedButton()
|
||
if clicked == overwrite_btn:
|
||
return "overwrite"
|
||
elif clicked == append_btn:
|
||
return "append"
|
||
else: # cancel_btn
|
||
return "cancel"
|
||
|
||
|
||
def ask_ergaenzen_entfernen_ersetzen_abbrechen(
|
||
parent: Any,
|
||
title: str,
|
||
message: str,
|
||
) -> Literal["ergaenzen", "entfernen", "ersetzen", "abbrechen"]:
|
||
"""Zeigt Dialog mit vier Optionen für Verfahrensgebiet-Editoperationen.
|
||
|
||
Optionen:
|
||
- **Ergänzen**: ausgewählte Flurstücke zum Verfahrensgebiet hinzufügen
|
||
- **Entfernen**: ausgewählte Flurstücke aus dem Verfahrensgebiet entfernen
|
||
- **Ersetzen**: Verfahrensgebiet vollständig durch Auswahl ersetzen
|
||
- **Abbrechen**: Vorgang beenden ohne Änderung
|
||
|
||
Parameters
|
||
----------
|
||
parent :
|
||
Eltern-Widget oder None.
|
||
title : str
|
||
Dialog-Titel.
|
||
message : str
|
||
Hauptmeldung mit Erklärung.
|
||
|
||
Returns
|
||
-------
|
||
Literal["ergaenzen", "entfernen", "ersetzen", "abbrechen"]
|
||
Genaue Entscheidung des Nutzers.
|
||
|
||
von Test abgesehen, weil vernachlässigbar; HD 18.09.2026
|
||
"""
|
||
if QT_VERSION == 0: # Mock-Modus
|
||
print(f"Mock-Modus: ask_ergaenzen_entfernen_ersetzen_abbrechen('{title}') → abbrechen")
|
||
return "abbrechen"
|
||
|
||
msg = QMessageBox(parent)
|
||
msg.setIcon(ICON_QUESTION)
|
||
msg.setWindowTitle(title)
|
||
msg.setText(message)
|
||
|
||
ergaenzen_btn = msg.addButton("Ergänzen", QMessageBox.ButtonRole.ActionRole)
|
||
entfernen_btn = msg.addButton("Entfernen", QMessageBox.ButtonRole.ActionRole)
|
||
ersetzen_btn = msg.addButton("Ersetzen", QMessageBox.ButtonRole.AcceptRole)
|
||
msg.addButton("Abbrechen", QMessageBox.ButtonRole.RejectRole)
|
||
|
||
exec_dialog(msg)
|
||
|
||
clicked = msg.clickedButton()
|
||
if clicked == ergaenzen_btn:
|
||
return "ergaenzen"
|
||
elif clicked == entfernen_btn:
|
||
return "entfernen"
|
||
elif clicked == ersetzen_btn:
|
||
return "ersetzen"
|
||
else:
|
||
return "abbrechen"
|
||
|
||
|
||
def ask_detailpruefung_oder_vg_laden(
|
||
parent: Any,
|
||
title: str,
|
||
message: str,
|
||
) -> Literal["detailpruefung", "als_vg_laden", "abbrechen"]:
|
||
"""Zeigt Dialog mit drei Optionen nach erkannter Flächenabweichung.
|
||
|
||
Optionen:
|
||
- **Detailprüfung starten**: Knickpunktbasierte Prüfung des BROR-Umrings
|
||
- **Bodenordnungsobjekt als VG laden**: BROR direkt als Verfahrensgebiet übernehmen
|
||
- **Abbrechen**: Vorgang beenden ohne weitere Aktion
|
||
|
||
Parameters
|
||
----------
|
||
parent :
|
||
Eltern-Widget oder None.
|
||
title : str
|
||
Dialog-Titel.
|
||
message : str
|
||
Hauptmeldung mit Flächen-Informationen.
|
||
|
||
Returns
|
||
-------
|
||
Literal["detailpruefung", "als_vg_laden", "abbrechen"]
|
||
Genaue Entscheidung des Nutzers.
|
||
|
||
von Test abgesehen, weil vernachlässigbar; HD 18.09.2026
|
||
"""
|
||
if QT_VERSION == 0: # Mock-Modus
|
||
print(f"Mock-Modus: ask_detailpruefung_oder_vg_laden('{title}') → abbrechen")
|
||
return "abbrechen"
|
||
|
||
msg = QMessageBox(parent)
|
||
msg.setIcon(ICON_QUESTION)
|
||
msg.setWindowTitle(title)
|
||
msg.setText(message)
|
||
|
||
detail_btn = msg.addButton("Detailprüfung starten", QMessageBox.ButtonRole.AcceptRole)
|
||
vg_btn = msg.addButton("Bodenordnungsobjekt als Verfahrensgebiet laden", QMessageBox.ButtonRole.ActionRole)
|
||
msg.addButton("Abbrechen", QMessageBox.ButtonRole.RejectRole)
|
||
|
||
exec_dialog(msg)
|
||
|
||
clicked = msg.clickedButton()
|
||
if clicked == detail_btn:
|
||
return "detailpruefung"
|
||
elif clicked == vg_btn:
|
||
return "als_vg_laden"
|
||
else:
|
||
return "abbrechen"
|
||
|
||
|
||
def ask_detailpruefung_oder_abbrechen(
|
||
parent: Any,
|
||
title: str,
|
||
message: str,
|
||
) -> Literal["detailpruefung", "abbrechen"]:
|
||
"""Zeigt Dialog mit zwei Optionen nach erkannter Flächenabweichung (Shape-Workflow).
|
||
|
||
Optionen:
|
||
- **Detailprüfung starten**: Knickpunktbasierte Prüfung des Shape-VG-Umrings
|
||
- **Abbrechen**: Vorgang beenden, Shape-Verfahrensgebiet unverändert behalten
|
||
|
||
Parameters
|
||
----------
|
||
parent :
|
||
Eltern-Widget oder None.
|
||
title : str
|
||
Dialog-Titel.
|
||
message : str
|
||
Hauptmeldung mit Flächen-Informationen.
|
||
|
||
Returns
|
||
-------
|
||
Literal["detailpruefung", "abbrechen"]
|
||
Genaue Entscheidung des Nutzers.
|
||
|
||
von Test abgesehen, weil vernachlässigbar; HD 18.09.2026
|
||
"""
|
||
if QT_VERSION == 0: # Mock-Modus
|
||
print(f"Mock-Modus: ask_detailpruefung_oder_abbrechen('{title}') → abbrechen")
|
||
return "abbrechen"
|
||
|
||
msg = QMessageBox(parent)
|
||
msg.setIcon(ICON_QUESTION)
|
||
msg.setWindowTitle(title)
|
||
msg.setText(message)
|
||
|
||
detail_btn = msg.addButton("Detailprüfung starten", QMessageBox.ButtonRole.AcceptRole)
|
||
msg.addButton("Abbrechen", QMessageBox.ButtonRole.RejectRole)
|
||
|
||
exec_dialog(msg)
|
||
|
||
clicked = msg.clickedButton()
|
||
if clicked == detail_btn:
|
||
return "detailpruefung"
|
||
else:
|
||
return "abbrechen"
|
||
|
||
|
||
class ProgressDialog:
|
||
def __init__(self, total: int, title: str = "Fortschritt", label: str = "Verarbeite..."):
|
||
self.total = max(total, 1)
|
||
self._canceled = False
|
||
|
||
if QT_VERSION == 0:
|
||
self.value = 0
|
||
self.label = label
|
||
self.title = title
|
||
return
|
||
|
||
self._dlg = QProgressDialog(label, "Abbrechen", 0, self.total)
|
||
self._dlg.setWindowTitle(title)
|
||
|
||
# Qt5 vs Qt6: WindowModality-Enum unterschiedlich verfügbar
|
||
modality = None
|
||
if hasattr(Qt, "WindowModality"):
|
||
try:
|
||
modality = Qt.WindowModality.WindowModal
|
||
except Exception:
|
||
modality = None
|
||
if modality is None and hasattr(Qt, "WindowModal"):
|
||
modality = Qt.WindowModal
|
||
if modality is not None:
|
||
try:
|
||
self._dlg.setWindowModality(modality)
|
||
except Exception:
|
||
pass
|
||
|
||
self._dlg.setMinimumDuration(0)
|
||
self._dlg.setAutoClose(False)
|
||
self._dlg.setAutoReset(False)
|
||
self._dlg.setValue(0)
|
||
|
||
def on_cancel():
|
||
if self._dlg and self._dlg.value() >= self.total:
|
||
# OK-Button am Ende
|
||
self._dlg.close()
|
||
return
|
||
self._canceled = True
|
||
self._dlg.close()
|
||
|
||
try:
|
||
self._dlg.canceled.connect(on_cancel)
|
||
except Exception:
|
||
pass
|
||
|
||
def set_total(self, total: int) -> None:
|
||
"""von Test abgesehen, weil vernachlässigbar; HD 18.09.2026"""
|
||
self.total = max(total, 1)
|
||
if QT_VERSION == 0:
|
||
return
|
||
|
||
if self._dlg is not None:
|
||
self._dlg.setMaximum(self.total)
|
||
|
||
def set_value(self, value: int) -> None:
|
||
"""von Test abgesehen, weil vernachlässigbar; HD 18.09.2026"""
|
||
if QT_VERSION == 0:
|
||
self.value = value
|
||
return
|
||
|
||
if self._dlg is not None:
|
||
self._dlg.setValue(min(value, self.total))
|
||
if value >= self.total:
|
||
self._dlg.setLabelText("Fertig. Klicken Sie auf OK, um das Fenster zu schließen.")
|
||
self._dlg.setCancelButtonText("OK")
|
||
QCoreApplication.processEvents()
|
||
|
||
def set_label(self, text: str) -> None:
|
||
"""von Test abgesehen, weil vernachlässigbar; HD 18.09.2026"""
|
||
if QT_VERSION == 0:
|
||
self.label = text
|
||
return
|
||
|
||
if self._dlg is not None:
|
||
self._dlg.setLabelText(text)
|
||
QCoreApplication.processEvents()
|
||
|
||
def is_canceled(self) -> bool:
|
||
"""von Test abgesehen, weil vernachlässigbar; HD 18.09.2026"""
|
||
if QT_VERSION == 0:
|
||
return self._canceled
|
||
|
||
if self._dlg is not None:
|
||
return self._canceled or self._dlg.wasCanceled()
|
||
|
||
return self._canceled
|
||
|
||
def close(self) -> None:
|
||
"""von Test abgesehen, weil vernachlässigbar; HD 18.09.2026"""
|
||
if QT_VERSION == 0:
|
||
return
|
||
|
||
if self._dlg is not None:
|
||
self._dlg.close()
|
||
|
||
def raise_to_front(self) -> None:
|
||
"""Bringt den Fortschrittsdialog in den Vordergrund.
|
||
|
||
Wird aufgerufen, nachdem ein modaler Subdialog (z. B. Auswahlbox)
|
||
geschlossen wurde und der Fortschrittsdialog wieder sichtbar sein soll.
|
||
|
||
von Test abgesehen, weil vernachlässigbar; HD 18.09.2026
|
||
"""
|
||
if QT_VERSION == 0:
|
||
return
|
||
if self._dlg is not None:
|
||
try:
|
||
self._dlg.raise_()
|
||
self._dlg.activateWindow()
|
||
QCoreApplication.processEvents()
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def create_progress_dialog(total: int, title: str = "Fortschritt", label: str = "Verarbeite...") -> ProgressDialog:
|
||
"""von Test abgesehen, weil vernachlässigbar; HD 18.09.2026"""
|
||
return ProgressDialog(total, title, label)
|
||
|
||
|
||
|
||
def ask_mehrfachauswahl(
|
||
titel: str,
|
||
hinweis: str,
|
||
eintraege: list[dict],
|
||
vorauswahl: set[str] | list[str],
|
||
parent: Any = None,
|
||
) -> Optional[list[str]]:
|
||
"""Zeigt eine Liste mit Häkchen, in der Einträge an- und abgewählt werden.
|
||
|
||
Parameters
|
||
----------
|
||
eintraege :
|
||
Dicts mit ``schluessel`` und ``text``, optional ``gruppe`` (Zwischenüberschrift; Einträge
|
||
einer Gruppe müssen aufeinander folgen) und ``icon`` (z. B. Symbolvorschau).
|
||
vorauswahl :
|
||
Schlüssel der anfangs angehakten Einträge.
|
||
|
||
Returns
|
||
-------
|
||
list[str] | None
|
||
Schlüssel der angehakten Einträge in Listenreihenfolge, ``None`` bei Abbruch.
|
||
Im Mock-Modus die Vorauswahl.
|
||
"""
|
||
vorauswahl = set(vorauswahl)
|
||
if QT_VERSION == 0: # Mock-Modus
|
||
return [e["schluessel"] for e in eintraege if e["schluessel"] in vorauswahl]
|
||
|
||
from sn_basis.functions.qt_wrapper import (
|
||
QDialog, QDialogButtonBox, QFont, QHBoxLayout, QLabel, QListWidget, QListWidgetItem, QPushButton,
|
||
QVBoxLayout, DialogAccepted, ItemIsEnabled, ItemIsUserCheckable, CheckStateChecked,
|
||
CheckStateUnchecked, UserRole,
|
||
)
|
||
|
||
dialog = QDialog(parent)
|
||
dialog.setWindowTitle(titel)
|
||
layout = QVBoxLayout(dialog)
|
||
|
||
beschriftung = QLabel(hinweis)
|
||
beschriftung.setWordWrap(True)
|
||
layout.addWidget(beschriftung)
|
||
|
||
suche = QLineEdit()
|
||
suche.setPlaceholderText("Suche …")
|
||
layout.addWidget(suche)
|
||
|
||
liste = QListWidget()
|
||
kopf_schrift = QFont()
|
||
kopf_schrift.setBold(True)
|
||
zeilen: list[tuple[Any, dict]] = []
|
||
koepfe: dict[str, Any] = {}
|
||
letzte_gruppe = None
|
||
for eintrag in eintraege:
|
||
gruppe = eintrag.get("gruppe")
|
||
if gruppe and gruppe != letzte_gruppe:
|
||
kopf = QListWidgetItem(gruppe)
|
||
kopf.setFlags(ItemIsEnabled)
|
||
kopf.setFont(kopf_schrift)
|
||
liste.addItem(kopf)
|
||
koepfe[gruppe] = kopf
|
||
letzte_gruppe = gruppe
|
||
zeile = QListWidgetItem(eintrag["text"])
|
||
if eintrag.get("icon") is not None:
|
||
zeile.setIcon(eintrag["icon"])
|
||
zeile.setFlags(ItemIsUserCheckable | ItemIsEnabled)
|
||
zeile.setCheckState(CheckStateChecked if eintrag["schluessel"] in vorauswahl else CheckStateUnchecked)
|
||
zeile.setData(UserRole, eintrag["schluessel"])
|
||
liste.addItem(zeile)
|
||
zeilen.append((zeile, eintrag))
|
||
layout.addWidget(liste, 1)
|
||
|
||
def _filtern(text: str) -> None:
|
||
suchtext = text.strip().lower()
|
||
sichtbare_gruppen = set()
|
||
for zeile, eintrag in zeilen:
|
||
treffer = not suchtext or suchtext in eintrag["text"].lower() \
|
||
or suchtext in (eintrag.get("gruppe") or "").lower()
|
||
zeile.setHidden(not treffer)
|
||
if treffer:
|
||
sichtbare_gruppen.add(eintrag.get("gruppe"))
|
||
for gruppe, kopf in koepfe.items():
|
||
kopf.setHidden(gruppe not in sichtbare_gruppen)
|
||
|
||
def _alle_setzen(zustand: Any) -> None:
|
||
for zeile, _ in zeilen:
|
||
if not zeile.isHidden():
|
||
zeile.setCheckState(zustand)
|
||
|
||
suche.textChanged.connect(_filtern)
|
||
|
||
knopfzeile = QHBoxLayout()
|
||
alle = QPushButton("Alle auswählen")
|
||
alle.clicked.connect(lambda *_: _alle_setzen(CheckStateChecked))
|
||
keine = QPushButton("Keine auswählen")
|
||
keine.clicked.connect(lambda *_: _alle_setzen(CheckStateUnchecked))
|
||
knopfzeile.addWidget(alle)
|
||
knopfzeile.addWidget(keine)
|
||
knopfzeile.addStretch(1)
|
||
layout.addLayout(knopfzeile)
|
||
|
||
knoepfe = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
||
knoepfe.accepted.connect(dialog.accept)
|
||
knoepfe.rejected.connect(dialog.reject)
|
||
layout.addWidget(knoepfe)
|
||
dialog.resize(460, 560)
|
||
|
||
if exec_dialog(dialog) != DialogAccepted:
|
||
return None
|
||
return [eintrag["schluessel"] for zeile, eintrag in zeilen if zeile.checkState() == CheckStateChecked]
|