Anzeigefenster immer mit Rahmen, Pfad der Korrektur-Datei im Tab (#56)

Befund aus dem QGIS-Test: Die Bildschirmwahl wirkte nicht, das Fenster
erschien immer auf dem Hauptbildschirm. Im Vollbild ließ es sich weder
verschieben noch schließen und verdeckte QGIS; nur ein hartes Beenden
von QGIS half.

Bildschirmwahl und Vollbild entfallen. Das Anzeigefenster öffnet als
normales Fenster mit Rahmen; der Bearbeiter schiebt es auf den Bildschirm
des Eigentümers und kann es dort maximieren. Größe und Lage werden beim
Beenden gemerkt und in der nächsten Sitzung wiederhergestellt (bis zum
Neustart von QGIS).

Unter den Knöpfen steht jetzt Pfad und Name der Korrektur-Datei, mit
Hinweis, solange sie noch nicht angelegt ist oder erst abgefragt wird.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-06 13:44:18 +02:00
co-authored by Claude Opus 5.5
parent e547cb90c5
commit 87341a7278
5 changed files with 189 additions and 201 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
- Neu: Anzeige für den Eigentümer im Tab „Eigentümer“. „Daten anzeigen“ in der Trefferliste zeigt Name, Anschrift und E-Mail der gewählten Einträge in einem eigenen Fenster – auf einem zweiten Bildschirm im Vollbild, sonst als normales Fenster. Die Sitzung wird von Hand begonnen und beendet; „Anzeige leeren“ entfernt die Daten, ohne das Fenster zu schließen, damit der nächste Eigentümer sie nicht sieht (#56)
- Neu: Angezeigte Daten lassen sich mit „Korrigieren …“ ändern. Die Eigentümerdatei bleibt unverändert; jede Änderung wird mit Zeitpunkt, Konto, Personen-ID, altem und neuem Wert an „<Eigentümerdatei>_korrekturen.csv“ angehängt und sofort angezeigt (#56)
- Neu: Anzeige für den Eigentümer im Tab „Eigentümer“. „Daten anzeigen“ in der Trefferliste zeigt Name, Anschrift und E-Mail der gewählten Einträge in einem eigenen Fenster mit Rahmen, das der Bearbeiter auf den Bildschirm des Eigentümers schiebt; Größe und Lage gelten bis zum Neustart von QGIS für die nächste Sitzung weiter. Die Sitzung wird von Hand begonnen und beendet; „Anzeige leeren“ entfernt die Daten, ohne das Fenster zu schließen, damit der nächste Eigentümer sie nicht sieht (#56)
- Neu: Angezeigte Daten lassen sich mit „Korrigieren …“ ändern. Die Eigentümerdatei bleibt unverändert; jede Änderung wird mit Zeitpunkt, Konto, Personen-ID, altem und neuem Wert an „<Eigentümerdatei>_korrekturen.csv“ angehängt und sofort angezeigt; Pfad und Name der Korrektur-Datei stehen unter den Knöpfen (#56)
- Neu: Eigentümersuche im Tab „Eigentümer“. Den Serienbriefexport (CSV) von Hand als Layer laden, als Eigentümerdatei wählen, Name und/oder Vorname eingeben: Die Trefferliste zeigt Name, Anschrift, E-Mail, den Wert von PER_RECHTSPERSON (0, 1, 5 oder 9, noch nicht entschlüsselt) und das Kennzeichen „verstorben“. Gleiche Personen mit gleicher Anschrift werden zusammengefasst, Groß-/Kleinschreibung und Umlautschreibweise spielen keine Rolle (#56)
- Intern: Die Auswahltabelle aus „Verfahrensgebiete aus Landkreis laden“ (Suchfeld, „Alle auswählen“, Mehrfachauswahl) ist als gemeinsame Funktion herausgelöst und wird von der Eigentümer-Trefferliste mitgenutzt; der Landkreis-Dialog verhält sich unverändert (#56)
- Neu: Tab „Eigentümer“ mit Flurstückssuche. Gemarkung (Name oder Nummer) und Flurstücksnummer (z. B. 562 oder 12/3) eingeben, die Treffer werden im Layer „Flurstücke“ ausgewählt und angezoomt. Ohne Nenner werden alle Flurstücke mit diesem Zähler gefunden. Fehlt der Layer „Flurstücke“, weist ein Dialog darauf hin, zuerst Verfahrensgebiet und Flurstücke aus ALKIS zu laden (#56)
+29 -47
View File
@@ -3,7 +3,6 @@
Alle Namen und Anschriften sind erfunden.
"""
import types
import unittest
from unittest.mock import MagicMock
@@ -15,10 +14,6 @@ ANNA = Eigentuemer(anrede="Frau", titel="Dr.", vorname="Anna", nachname="Beispie
verstorben=True, rechtsperson=("0",), per_ids=("4711",), konten=("98765",))
def _rechteck(x, y, b, h):
return types.SimpleNamespace(x=lambda: x, y=lambda: y, width=lambda: b, height=lambda: h)
class TestAnzeigetext(unittest.TestCase):
def test_name_anschrift_email(self):
@@ -57,52 +52,39 @@ class TestAnzeigetext(unittest.TestCase):
self.assertNotIn("Name", text)
class TestBildschirme(unittest.TestCase):
class TestFensterOeffnen(unittest.TestCase):
def test_vorgabe_ist_nicht_der_qgis_bildschirm(self):
"""Prüft, dass als Vorgabe der erste Bildschirm ohne QGIS gewählt wird, bei nur einem Bildschirm dieser."""
a, b, c = object(), object(), object()
self.assertEqual(ea.standard_index([a, b, c], a), 1)
self.assertEqual(ea.standard_index([a, b], b), 0)
self.assertEqual(ea.standard_index([a], a), 0)
self.assertEqual(ea.standard_index([], None), 0)
def test_bildschirm_text(self):
"""Prüft, dass ein Bildschirm mit Nummer und Auflösung beschriftet wird."""
bildschirm = types.SimpleNamespace(geometry=lambda: _rechteck(0, 0, 1920, 1080))
self.assertEqual(ea.bildschirm_text(bildschirm, 2), "Bildschirm 2 (1920 × 1080)")
self.assertEqual(ea.bildschirm_text(object(), 3), "Bildschirm 3")
def test_ohne_qt_anwendung_keine_bildschirme(self):
"""Prüft, dass ohne Qt-Anwendung (Mock-Modus) eine leere Bildschirmliste kommt statt eines Fehlers."""
self.assertEqual(ea.bildschirme(), [])
def test_vollbild_auf_gewaehltem_bildschirm(self):
"""Prüft, dass das Fenster im Vollbild dem gewählten Bildschirm zugeordnet und auf dessen Fläche gelegt wird."""
geo = _rechteck(1920, 0, 1920, 1080)
bildschirm = types.SimpleNamespace(geometry=lambda: geo)
def test_normales_fenster_in_standardgroesse(self):
"""Prüft, dass das Anzeigefenster ohne gemerkte Lage als normales Fenster mit Rahmen in Standardgröße öffnet, nie im Vollbild."""
fenster = MagicMock()
ea.zeige_auf_bildschirm(fenster, bildschirm, vollbild=True)
fenster.windowHandle.return_value.setScreen.assert_called_once_with(bildschirm)
fenster.setGeometry.assert_called_once_with(geo)
fenster.showFullScreen.assert_called_once()
fenster.showNormal.assert_not_called()
def test_normales_fenster_mittig(self):
"""Prüft, dass auf dem QGIS-Bildschirm ein normales Fenster mittig im freien Bereich öffnet."""
bildschirm = types.SimpleNamespace(availableGeometry=lambda: _rechteck(0, 40, 1600, 1000))
fenster = MagicMock()
ea.zeige_auf_bildschirm(fenster, bildschirm, vollbild=False)
fenster.setGeometry.assert_called_once_with(350, 215, 900, 650)
fenster.showNormal.assert_called_once()
fenster.showFullScreen.assert_not_called()
def test_ohne_bildschirm_einfach_zeigen(self):
"""Prüft, dass ohne bekannten Bildschirm das Fenster einfach gezeigt wird."""
fenster = MagicMock()
ea.zeige_auf_bildschirm(fenster, None, vollbild=True)
ea.zeige_anzeigefenster(fenster)
fenster.resize.assert_called_once_with(*ea.STANDARDGROESSE)
fenster.show.assert_called_once()
fenster.showFullScreen.assert_not_called()
fenster.setWindowFlags.assert_not_called()
def test_gemerkte_lage_wird_wiederhergestellt(self):
"""Prüft, dass eine gemerkte Größe und Lage der letzten Sitzung wiederhergestellt wird."""
fenster = MagicMock()
fenster.restoreGeometry.return_value = True
ea.zeige_anzeigefenster(fenster, b"geometrie")
fenster.restoreGeometry.assert_called_once_with(b"geometrie")
fenster.resize.assert_not_called()
fenster.show.assert_called_once()
fenster.showFullScreen.assert_not_called()
def test_unbrauchbare_lage_ergibt_standardgroesse(self):
"""Prüft, dass eine nicht wiederherstellbare Lage auf die Standardgröße zurückfällt."""
for ergebnis in (False, RuntimeError("kaputt")):
with self.subTest(ergebnis=ergebnis):
fenster = MagicMock()
if isinstance(ergebnis, Exception):
fenster.restoreGeometry.side_effect = ergebnis
else:
fenster.restoreGeometry.return_value = ergebnis
ea.zeige_anzeigefenster(fenster, b"alt")
fenster.resize.assert_called_once_with(*ea.STANDARDGROESSE)
fenster.show.assert_called_once()
if __name__ == "__main__":
+71 -28
View File
@@ -192,9 +192,12 @@ class TestTrefferliste(unittest.TestCase):
def _anz_tab(angezeigt=None, anzeige=None, layer_id="csv1"):
tab = EigentuemerTab.__new__(EigentuemerTab)
for name in ("btn_sitzung", "btn_anzeige_leeren", "btn_korrigieren", "sitzung_label", "bildschirm_combo"):
for name in ("btn_sitzung", "btn_anzeige_leeren", "btn_korrigieren", "sitzung_label", "korrektur_label",
"eigentuemer_layer_combo"):
setattr(tab, name, MagicMock())
tab.eigentuemer_layer_combo.currentData.return_value = layer_id
tab._anzeige = anzeige
tab._anzeige_geometrie = None
tab._sitzung_seit = datetime(2026, 10, 6, 9, 5) if anzeige is not None else None
tab._angezeigt = list(angezeigt or [])
tab._treffer_layer_id = layer_id
@@ -206,35 +209,32 @@ def _anz_tab(angezeigt=None, anzeige=None, layer_id="csv1"):
_BERND = Eigentuemer(vorname="Bernd", nachname="Muster", konten=("300",), per_ids=("8",))
@patch.object(et, "zeige_auf_bildschirm")
@patch.object(et, "qgis_bildschirm")
@patch.object(et, "hole_layer", return_value=None)
@patch.object(et, "zeige_anzeigefenster")
@patch.object(et, "EigentuemerAnzeige")
class TestSitzung(unittest.TestCase):
def test_sitzung_beginnen_im_vollbild_auf_zweitem_bildschirm(self, anzeige_klasse, qgis, zeige):
"""Prüft, dass „Sitzung beginnen“ ein leeres Anzeigefenster auf dem gewählten Bildschirm öffnet, im Vollbild, wenn QGIS woanders liegt."""
def test_sitzung_beginnen_oeffnet_leeres_fenster_mit_rahmen(self, anzeige_klasse, zeige, _layer):
"""Prüft, dass „Sitzung beginnen“ ein leeres Anzeigefenster als normales Fenster öffnet (ohne gemerkte Lage beim ersten Mal)."""
tab = _anz_tab()
zweiter = object()
tab.bildschirm_combo.gewaehlter_bildschirm.return_value = zweiter
qgis.return_value = object()
tab._on_sitzung_umschalten()
anzeige_klasse.assert_called_once_with(tab)
zeige.assert_called_once_with(anzeige_klasse.return_value, zweiter, True)
zeige.assert_called_once_with(anzeige_klasse.return_value, None)
self.assertIs(tab._anzeige, anzeige_klasse.return_value)
self.assertEqual(tab._angezeigt, [])
tab.btn_sitzung.setText.assert_called_with("Sitzung beenden")
self.assertIn("leer", tab.sitzung_label.setText.call_args[0][0])
def test_auf_qgis_bildschirm_kein_vollbild(self, anzeige_klasse, qgis, zeige):
"""Prüft, dass das Anzeigefenster auf dem QGIS-Bildschirm als normales Fenster öffnet."""
def test_lage_gilt_fuer_naechste_sitzung(self, anzeige_klasse, zeige, _layer):
"""Prüft, dass Größe und Lage des Fensters beim Beenden gemerkt und in der nächsten Sitzung wiederverwendet werden."""
tab = _anz_tab()
einziger = object()
tab.bildschirm_combo.gewaehlter_bildschirm.return_value = einziger
qgis.return_value = einziger
tab._sitzung_beginnen()
self.assertFalse(zeige.call_args[0][2])
anzeige_klasse.return_value.saveGeometry.return_value = b"zweiter-bildschirm"
tab._sitzung_beenden()
tab._sitzung_beginnen()
self.assertEqual(zeige.call_args_list[-1][0][1], b"zweiter-bildschirm")
def test_daten_anzeigen_beginnt_sitzung_bei_bedarf(self, anzeige_klasse, qgis, zeige):
def test_daten_anzeigen_beginnt_sitzung_bei_bedarf(self, anzeige_klasse, zeige, _layer):
"""Prüft, dass „Daten anzeigen“ ohne laufende Sitzung diese beginnt und die Einträge zeigt; die Statuszeile nennt nur die Anzahl."""
tab = _anz_tab()
tab._daten_anzeigen([_ANNA, _BERND])
@@ -244,7 +244,7 @@ class TestSitzung(unittest.TestCase):
self.assertIn("2 Einträge", status)
self.assertNotIn("Beispiel", status)
def test_daten_anzeigen_ohne_auswahl(self, anzeige_klasse, qgis, zeige):
def test_daten_anzeigen_ohne_auswahl(self, anzeige_klasse, zeige, _layer):
"""Prüft, dass „Daten anzeigen“ ohne Auswahl nur einen Hinweis gibt und keine Sitzung beginnt."""
tab = _anz_tab()
with patch.object(et, "warning") as warnung:
@@ -252,7 +252,7 @@ class TestSitzung(unittest.TestCase):
warnung.assert_called_once()
anzeige_klasse.assert_not_called()
def test_anzeige_leeren_laesst_fenster_offen(self, anzeige_klasse, qgis, zeige):
def test_anzeige_leeren_laesst_fenster_offen(self, anzeige_klasse, zeige, _layer):
"""Prüft, dass „Anzeige leeren“ die Daten entfernt, das Fenster aber nicht schließt."""
anzeige = MagicMock()
tab = _anz_tab(angezeigt=[_ANNA], anzeige=anzeige)
@@ -263,7 +263,7 @@ class TestSitzung(unittest.TestCase):
self.assertIs(tab._anzeige, anzeige)
tab.btn_anzeige_leeren.setEnabled.assert_called_with(False)
def test_sitzung_beenden_schliesst_fenster(self, anzeige_klasse, qgis, zeige):
def test_sitzung_beenden_schliesst_fenster(self, anzeige_klasse, zeige, _layer):
"""Prüft, dass „Sitzung beenden“ das Anzeigefenster schließt und die angezeigten Daten vergisst."""
anzeige = MagicMock()
tab = _anz_tab(angezeigt=[_ANNA], anzeige=anzeige)
@@ -274,21 +274,64 @@ class TestSitzung(unittest.TestCase):
self.assertEqual(tab._angezeigt, [])
tab.btn_sitzung.setText.assert_called_with("Sitzung beginnen")
def test_von_aussen_geschlossen_beendet_sitzung(self, anzeige_klasse, qgis, zeige):
"""Prüft, dass ein von außen geschlossenes Anzeigefenster (z. B. Alt+F4) die Sitzung im Tab beendet."""
def test_von_aussen_geschlossen_beendet_sitzung(self, anzeige_klasse, zeige, _layer):
"""Prüft, dass ein über den Fensterrahmen geschlossenes Anzeigefenster die Sitzung beendet und seine Lage gemerkt wird."""
tab = _anz_tab()
tab._sitzung_beginnen()
rueckruf = anzeige_klasse.return_value.beim_schliessen
rueckruf()
anzeige_klasse.return_value.saveGeometry.return_value = b"lage"
anzeige_klasse.return_value.beim_schliessen()
self.assertIsNone(tab._anzeige)
self.assertEqual(tab._anzeige_geometrie, b"lage")
class TestKorrekturLabel(unittest.TestCase):
def _text(self, tab):
return tab.korrektur_label.setText.call_args[0][0]
def test_ohne_eigentuemerdatei(self):
"""Prüft, dass ohne Eigentümerdatei unter den Knöpfen steht, dass keine gewählt ist."""
tab = _anz_tab(layer_id=None)
tab._aktualisiere_korrektur_label()
self.assertIn("keine Eigentümerdatei", self._text(tab))
def test_pfad_neben_der_eigentuemerdatei(self):
"""Prüft, dass der Pfad der Korrektur-CSV angezeigt wird, mit Hinweis, solange sie noch nicht angelegt ist."""
with tempfile.TemporaryDirectory() as ordner:
quelle = "file:///" + os.path.join(ordner, "export.csv").replace(os.sep, "/") + "?type=csv"
erwartet = os.path.normpath(os.path.join(ordner, "export_korrekturen.csv"))
tab = _anz_tab()
with patch.object(et, "hole_layer", return_value=types.SimpleNamespace(source=lambda: quelle)):
tab._aktualisiere_korrektur_label()
self.assertEqual(os.path.normcase(self._text(tab)),
os.path.normcase(f"Korrektur-Datei: {erwartet} (wird beim ersten Speichern angelegt)"))
open(erwartet, "w").close()
tab._aktualisiere_korrektur_label()
self.assertEqual(os.path.normcase(self._text(tab)), os.path.normcase(f"Korrektur-Datei: {erwartet}"))
def test_ohne_csv_quelle_wird_abgefragt(self):
"""Prüft, dass bei einer Quelle ohne CSV angekündigt wird, dass der Speicherort beim ersten Speichern abgefragt wird."""
tab = _anz_tab()
with patch.object(et, "hole_layer", return_value=types.SimpleNamespace(source=lambda: "memory?geometry=None")):
tab._aktualisiere_korrektur_label()
self.assertIn("abgefragt", self._text(tab))
def test_gewaehlter_pfad_wird_angezeigt(self):
"""Prüft, dass ein einmal gewählter Speicherort angezeigt wird."""
tab = _anz_tab()
tab._korrektur_pfade["csv1"] = "D:/Termine/korrekturen.csv"
with patch.object(et, "hole_layer", return_value=None):
tab._aktualisiere_korrektur_label()
self.assertIn(os.path.normpath("D:/Termine/korrekturen.csv"), self._text(tab))
@patch.object(et, "hole_layer", return_value=None)
@patch.object(et, "success")
@patch.object(et, "info")
@patch.object(et, "warning")
class TestKorrektur(unittest.TestCase):
def test_korrektur_wird_festgehalten_und_angezeigt(self, warnung, hinweis, erfolg):
def test_korrektur_wird_festgehalten_und_angezeigt(self, warnung, hinweis, erfolg, _layer):
"""Prüft, dass eine Korrektur in die Korrektur-CSV geschrieben, sofort angezeigt und ohne Namen gemeldet wird."""
anzeige = MagicMock()
tab = _anz_tab(angezeigt=[_ANNA, _BERND], anzeige=anzeige)
@@ -305,7 +348,7 @@ class TestKorrektur(unittest.TestCase):
self.assertIn("export_korrekturen.csv", meldung)
self.assertNotIn("Muster", meldung)
def test_ohne_aenderung_nichts_schreiben(self, warnung, hinweis, erfolg):
def test_ohne_aenderung_nichts_schreiben(self, warnung, hinweis, erfolg, _layer):
"""Prüft, dass ein unverändert gespeicherter Dialog nichts schreibt."""
tab = _anz_tab(angezeigt=[_ANNA], anzeige=MagicMock())
with patch.object(et, "frage_korrektur", return_value=(0, {"nachname": "Beispiel"})), \
@@ -314,7 +357,7 @@ class TestKorrektur(unittest.TestCase):
schreiben.assert_not_called()
hinweis.assert_called_once()
def test_abbruch_nichts_schreiben(self, warnung, hinweis, erfolg):
def test_abbruch_nichts_schreiben(self, warnung, hinweis, erfolg, _layer):
"""Prüft, dass ein abgebrochener Korrekturdialog nichts schreibt."""
tab = _anz_tab(angezeigt=[_ANNA], anzeige=MagicMock())
with patch.object(et, "frage_korrektur", return_value=None), \
@@ -322,7 +365,7 @@ class TestKorrektur(unittest.TestCase):
tab._on_korrigieren()
schreiben.assert_not_called()
def test_schreibfehler_warnung_und_anzeige_unveraendert(self, warnung, hinweis, erfolg):
def test_schreibfehler_warnung_und_anzeige_unveraendert(self, warnung, hinweis, erfolg, _layer):
"""Prüft, dass bei einem Schreibfehler gewarnt, die Anzeige nicht geändert und der Pfad beim nächsten Mal neu bestimmt wird."""
anzeige = MagicMock()
tab = _anz_tab(angezeigt=[_ANNA], anzeige=anzeige)
@@ -335,7 +378,7 @@ class TestKorrektur(unittest.TestCase):
self.assertIs(tab._angezeigt[0], _ANNA)
self.assertNotIn("csv1", tab._korrektur_pfade)
def test_ohne_anzeige_keine_korrektur(self, warnung, hinweis, erfolg):
def test_ohne_anzeige_keine_korrektur(self, warnung, hinweis, erfolg, _layer):
"""Prüft, dass ohne angezeigte Daten kein Korrekturdialog erscheint."""
tab = _anz_tab()
with patch.object(et, "frage_korrektur") as dialog:
+24 -90
View File
@@ -5,22 +5,23 @@ sieht. Es zeigt ausschließlich Name, Anschrift und E-Mail der vom Bearbeiter
gewählten Einträge – keine Konten, keine Kennzeichen, keine anderen Treffer.
Ohne Inhalt zeigt es nur einen neutralen Platzhalter.
Liegt das Fenster auf einem anderen Bildschirm als QGIS, öffnet es im Vollbild
ohne Rahmen; der Eigentümer kann es nicht versehentlich schließen oder
verschieben. Auf dem QGIS-Bildschirm (z. B. am Notebook ohne zweiten Monitor)
öffnet es als normales Fenster.
Das Fenster öffnet immer als normales Fenster mit Rahmen; der Bearbeiter
schiebt es selbst auf den Bildschirm des Eigentümers und kann es dort
maximieren. Eine automatische Bildschirmwahl mit Vollbild hat sich im QGIS-Test
nicht bewährt: Das Fenster landete stets auf dem Hauptbildschirm und ließ sich
im Vollbild weder verschieben noch schließen (siehe ``fallstricke.md``).
"""
import html
from typing import Callable, List, Optional, Sequence
from typing import Callable, Optional, Sequence
from sn_basis.functions.qt_wrapper import QWidget, QVBoxLayout, QLabel, QComboBox, QCoreApplication, Qt
from sn_basis.functions.qgisui_wrapper import get_main_window
from sn_basis.functions.qt_wrapper import QWidget, QVBoxLayout, QLabel, Qt
from sn_verfahrensgebiet.functions.eigentuemer_suche import Eigentuemer
PLATZHALTER = "Datenabgleich"
OHNE_EMAIL = "keine angegeben"
STANDARDGROESSE = (900, 650)
def _ortsteil(text: str) -> str:
@@ -91,88 +92,21 @@ class EigentuemerAnzeige(QWidget):
super().closeEvent(event)
# ---------------------------------------------------------------------------
# Bildschirme
# ---------------------------------------------------------------------------
def zeige_anzeigefenster(fenster, geometrie=None) -> None:
"""Zeigt das Anzeigefenster als normales Fenster mit Rahmen, nie im Vollbild.
def bildschirme() -> List[object]:
"""Alle angeschlossenen Bildschirme (``QScreen``); leer ohne Qt-Anwendung."""
try:
anwendung = QCoreApplication.instance()
return list(anwendung.screens()) if anwendung is not None else []
except Exception:
return []
def qgis_bildschirm() -> Optional[object]:
"""Der Bildschirm mit dem QGIS-Hauptfenster, oder ``None``."""
try:
return get_main_window().screen()
except Exception:
return None
def bildschirm_text(bildschirm, nummer: int) -> str:
"""Eintrag für die Auswahlliste, z. B. ``Bildschirm 2 (1920 × 1080)``."""
try:
geo = bildschirm.geometry()
return f"Bildschirm {nummer} ({geo.width()} × {geo.height()})"
except Exception:
return f"Bildschirm {nummer}"
def standard_index(alle: Sequence[object], qgis: Optional[object]) -> int:
"""Index des ersten Bildschirms, der nicht der QGIS-Bildschirm ist; sonst 0."""
for index, bildschirm in enumerate(alle):
if bildschirm is not qgis:
return index
return 0
def zeige_auf_bildschirm(fenster, bildschirm, vollbild: bool) -> None:
"""Öffnet ``fenster`` auf ``bildschirm`` – im Vollbild oder mittig als normales Fenster."""
if bildschirm is None:
fenster.show()
return
fenster.winId() # erzeugt das native Fenster, damit es einem Bildschirm zugeordnet werden kann
griff = fenster.windowHandle()
if griff is not None:
griff.setScreen(bildschirm)
if vollbild:
fenster.setGeometry(bildschirm.geometry())
fenster.showFullScreen()
else:
frei = bildschirm.availableGeometry()
breite, hoehe = min(900, frei.width()), min(650, frei.height())
fenster.setGeometry(frei.x() + (frei.width() - breite) // 2,
frei.y() + (frei.height() - hoehe) // 2, breite, hoehe)
fenster.showNormal()
fenster.raise_()
class BildschirmAuswahl(QComboBox):
"""Auswahl des Bildschirms für das Anzeigefenster; wird beim Aufklappen aufgefrischt.
Vorgabe ist der erste Bildschirm, auf dem QGIS nicht liegt.
Mit ``geometrie`` (aus ``saveGeometry()`` der letzten Sitzung) erscheint es
wieder dort, wohin der Bearbeiter es geschoben hatte; sonst in
Standardgröße an der von Qt gewählten Stelle.
"""
def aktualisieren(self) -> None:
bisher = self.currentData()
alle = bildschirme()
self.clear()
for nummer, bildschirm in enumerate(alle, start=1):
self.addItem(bildschirm_text(bildschirm, nummer), bildschirm.name())
index = self.findData(bisher) if bisher else -1
self.setCurrentIndex(index if index >= 0 else standard_index(alle, qgis_bildschirm()))
def showPopup(self) -> None:
self.aktualisieren()
super().showPopup()
def gewaehlter_bildschirm(self) -> Optional[object]:
"""Der gewählte ``QScreen`` oder ``None``, wenn es ihn nicht (mehr) gibt."""
name = self.currentData()
for bildschirm in bildschirme():
if bildschirm.name() == name:
return bildschirm
return None
wiederhergestellt = False
if geometrie is not None:
try:
wiederhergestellt = bool(fenster.restoreGeometry(geometrie))
except Exception:
wiederhergestellt = False
if not wiederhergestellt:
fenster.resize(*STANDARDGROESSE)
fenster.show()
fenster.raise_()
fenster.activateWindow()
+61 -32
View File
@@ -8,11 +8,13 @@ Zwei Bereiche:
- **Eigentümersuche:** Eigentümerdatei (CSV-Layer) wählen, Name und/oder Vorname
eingeben, „Suchen“ öffnet die Trefferliste.
Logik: :mod:`sn_verfahrensgebiet.functions.eigentuemer_suche`.
- **Anzeige für den Eigentümer:** Bildschirm wählen, Sitzung beginnen und
beenden, Anzeige leeren, angezeigte Daten korrigieren. „Daten anzeigen“ in
der Trefferliste füllt das Anzeigefenster (und beginnt bei Bedarf die
Sitzung). Korrekturen landen in einer Korrektur-CSV neben der
Eigentümerdatei (:mod:`sn_verfahrensgebiet.functions.eigentuemer_korrektur`).
- **Anzeige für den Eigentümer:** Sitzung beginnen und beenden, Anzeige
leeren, angezeigte Daten korrigieren. „Daten anzeigen“ in der Trefferliste
füllt das Anzeigefenster (und beginnt bei Bedarf die Sitzung). Das Fenster
schiebt der Bearbeiter selbst auf den Bildschirm des Eigentümers; Größe und
Lage gelten für die nächste Sitzung weiter. Korrekturen landen in einer
Korrektur-CSV neben der Eigentümerdatei, deren Pfad unter den Knöpfen steht
(:mod:`sn_verfahrensgebiet.functions.eigentuemer_korrektur`).
"""
import os
@@ -44,12 +46,7 @@ from sn_verfahrensgebiet.functions.eigentuemer_korrektur import (
wende_an,
)
from sn_verfahrensgebiet.ui.eigentuemer_treffer import EigentuemerTrefferFenster
from sn_verfahrensgebiet.ui.eigentuemer_anzeige import (
BildschirmAuswahl,
EigentuemerAnzeige,
qgis_bildschirm,
zeige_auf_bildschirm,
)
from sn_verfahrensgebiet.ui.eigentuemer_anzeige import EigentuemerAnzeige, zeige_anzeigefenster
from sn_verfahrensgebiet.ui.eigentuemer_korrektur_dialog import frage_korrektur
_TITEL_SUCHE = "Flurstückssuche"
@@ -158,18 +155,12 @@ class EigentuemerTab(QWidget):
titel_anzeige.setStyleSheet("margin-top: 12px;")
layout.addWidget(titel_anzeige)
zeile_bildschirm = QHBoxLayout()
zeile_bildschirm.addWidget(QLabel("Bildschirm:"))
self.bildschirm_combo = BildschirmAuswahl()
self.bildschirm_combo.setToolTip(
"Bildschirm, den der Eigentümer sieht. Auf einem anderen Bildschirm als QGIS "
"öffnet die Anzeige im Vollbild."
)
zeile_bildschirm.addWidget(self.bildschirm_combo, 1)
layout.addLayout(zeile_bildschirm)
zeile_sitzung = QHBoxLayout()
self.btn_sitzung = QPushButton("Sitzung beginnen")
self.btn_sitzung.setToolTip(
"Öffnet das Anzeigefenster. Es auf den Bildschirm des Eigentümers schieben; "
"Größe und Lage gelten für die nächste Sitzung weiter."
)
self.btn_anzeige_leeren = QPushButton("Anzeige leeren")
self.btn_anzeige_leeren.setToolTip(
"Entfernt die Daten aus dem Anzeigefenster, ohne es zu schließen – "
@@ -187,19 +178,27 @@ class EigentuemerTab(QWidget):
self.sitzung_label.setWordWrap(True)
layout.addWidget(self.sitzung_label)
self.korrektur_label = QLabel()
self.korrektur_label.setWordWrap(True)
self.korrektur_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
layout.addWidget(self.korrektur_label)
self.btn_sitzung.clicked.connect(self._on_sitzung_umschalten)
self.btn_anzeige_leeren.clicked.connect(self._on_anzeige_leeren)
self.btn_korrigieren.clicked.connect(self._on_korrigieren)
self.eigentuemer_layer_combo.currentIndexChanged.connect(
lambda _index: self._aktualisiere_korrektur_label()
)
self._trefferfenster = None
self._treffer_layer_id = None
self._anzeige = None
self._anzeige_geometrie = None
self._sitzung_seit = None
self._angezeigt = []
self._anzeige_layer_id = None
self._korrektur_pfade = {}
self.eigentuemer_layer_combo.aktualisieren()
self.bildschirm_combo.aktualisieren()
self._aktualisiere_sitzungsstatus()
self.setLayout(layout)
@@ -292,12 +291,10 @@ class EigentuemerTab(QWidget):
self._sitzung_beenden()
def _sitzung_beginnen(self) -> None:
"""Öffnet das Anzeigefenster leer auf dem gewählten Bildschirm."""
bildschirm = self.bildschirm_combo.gewaehlter_bildschirm()
"""Öffnet das Anzeigefenster leer, mit Rahmen, an der Stelle der letzten Sitzung."""
anzeige = EigentuemerAnzeige(self)
anzeige.beim_schliessen = self._anzeige_wurde_geschlossen
vollbild = bildschirm is not None and bildschirm is not qgis_bildschirm()
zeige_auf_bildschirm(anzeige, bildschirm, vollbild)
zeige_anzeigefenster(anzeige, self._anzeige_geometrie)
self._anzeige = anzeige
self._sitzung_seit = datetime.now()
self._angezeigt = []
@@ -312,6 +309,11 @@ class EigentuemerTab(QWidget):
anzeige.close()
def _anzeige_wurde_geschlossen(self) -> None:
if self._anzeige is not None:
try:
self._anzeige_geometrie = self._anzeige.saveGeometry()
except Exception:
pass
self._anzeige = None
self._sitzung_seit = None
self._angezeigt = []
@@ -349,6 +351,36 @@ class EigentuemerTab(QWidget):
inhalt = "leer" if not anzahl else ("1 Eintrag" if anzahl == 1 else f"{anzahl} Einträge")
text = f"Sitzung seit {self._sitzung_seit:%H:%M} Uhr – Anzeige: {inhalt}"
self.sitzung_label.setText(text)
self._aktualisiere_korrektur_label()
def _korrektur_layer_id(self):
"""Eigentümerdatei, zu der Korrekturen gehören: die angezeigte, sonst die gesuchte, sonst die gewählte."""
return self._anzeige_layer_id or self._treffer_layer_id or self.eigentuemer_layer_combo.currentData()
def _korrektur_vorschau(self):
"""Pfad der Korrektur-CSV ohne Rückfrage, oder ``None``, wenn er erst beim Speichern erfragt wird."""
layer_id = self._korrektur_layer_id()
if layer_id in self._korrektur_pfade:
return self._korrektur_pfade[layer_id]
layer = hole_layer(layer_id)
vorschlag = korrektur_pfad(layer.source()) if layer is not None else None
ordner = os.path.dirname(vorschlag) if vorschlag else ""
if vorschlag and os.path.isdir(ordner) and os.access(ordner, os.W_OK):
return vorschlag
return None
def _aktualisiere_korrektur_label(self) -> None:
"""Zeigt unter den Knöpfen, wohin Korrekturen geschrieben werden."""
if not self._korrektur_layer_id():
text = "Korrektur-Datei: – (keine Eigentümerdatei gewählt)"
else:
pfad = self._korrektur_vorschau()
if pfad is None:
text = "Korrektur-Datei: wird beim ersten Speichern abgefragt"
else:
zusatz = "" if os.path.exists(pfad) else " (wird beim ersten Speichern angelegt)"
text = f"Korrektur-Datei: {os.path.normpath(pfad)}{zusatz}"
self.korrektur_label.setText(text)
# ------------------------------------------------------------------
# Korrekturen
@@ -382,6 +414,7 @@ class EigentuemerTab(QWidget):
self._angezeigt[index] = wende_an(eintrag, aenderungen)
if self._anzeige is not None:
self._anzeige.zeige(self._angezeigt)
self._aktualisiere_korrektur_label()
anzahl = len(aenderungen)
text = "1 Änderung" if anzahl == 1 else f"{anzahl} Änderungen"
success(_TITEL_ANZEIGE, f"{text} in „{os.path.basename(pfad)}“ festgehalten.")
@@ -394,14 +427,10 @@ class EigentuemerTab(QWidget):
für die Eigentümerdatei, solange der Tab besteht.
"""
layer_id = self._anzeige_layer_id
if layer_id in self._korrektur_pfade:
return self._korrektur_pfade[layer_id]
pfad = self._korrektur_vorschau() if layer_id == self._korrektur_layer_id() else None
if pfad is None:
layer = hole_layer(layer_id)
vorschlag = korrektur_pfad(layer.source()) if layer is not None else None
ordner = os.path.dirname(vorschlag) if vorschlag else ""
if vorschlag and os.path.isdir(ordner) and os.access(ordner, os.W_OK):
pfad = vorschlag
else:
pfad, _ = QFileDialog.getSaveFileName(
self, "Korrektur-CSV wählen", vorschlag or os.path.expanduser("~"), "CSV-Dateien (*.csv)"
)