Bugfix/96 legenden spaltenbreite #97
+1
-1
@@ -1,4 +1,4 @@
|
||||
- Bugfix: Mehrspaltige Legenden waren sehr breit, weil alle Spalten so breit wie der längste Text waren. Legendentext bricht jetzt nach 60 mm um, Spalten sind ungleich breit, der Titel der Basemap-Legende ist zweizeilig – im Test wurde die dreispaltige Basemap-Legende 226 statt 431 mm breit (#96)
|
||||
- Bugfix: Mehrspaltige Legenden waren sehr breit, weil alle Spalten so breit wie der längste Text waren. Legendentext bricht jetzt nach 60 mm um, Spalten sind ungleich breit, der Titel der Basemap-Legende ist zweizeilig – im Test wurde die dreispaltige Basemap-Legende 226 statt 431 mm breit. Standard- und Mindestbreite der Legenden ist die Breite des Kartenschilds (180 mm); sie wird am Layout vermerkt und beim Laden des Projekts wieder gesetzt, weil QGIS sie nicht speichert (#96)
|
||||
- Bugfix: Bei kleinen Verfahren oder großem Maßstab überlappten sich Legenden, Quellenangabe und Kartenschild, weil sie zusammen höher als die Karte waren. Beim Anlegen einer §38-/§41-Vorlage werden die Legenden jetzt bei Bedarf zwei- oder dreispaltig gesetzt (Kartenrand wird dafür breiter); reicht auch das nicht, wird die Seite höher und Quellenangabe und Kartenschild rutschen nach unten. Größe und Lage der Karte bleiben gleich (#93)
|
||||
- Bugfix: Weiße Basemap-Flächen ohne Kontur (z. B. „Platz“) waren in der Basemap-Legende auf weißem Grund unsichtbar; ihr Legendensymbol bekommt jetzt einen dünnen grauen Rand (0,15 mm), die Karte bleibt unverändert (#92)
|
||||
- Bugfix: Punktlinien des Basemap-Layers (Baumreihe, Hecke, Durchlass, Seilbahnen u. a., 12 Styles) wurden in Karte und Legende nicht gezeichnet – basemap.de nutzt Strichlänge 0 mit runder Kappe, Qt zeichnet Striche der Länge 0 nicht. Neuer Knopf „Punktlinien korrigieren“ im Basemap-Bereich des Druck-Tabs ersetzt die Strichlänge 0 durch 0,01 (festes Muster und zoomabhängiger Ausdruck); die Korrektur lässt sich als Stilvorlage sichern (#90)
|
||||
|
||||
@@ -8,6 +8,10 @@ from sn_basis.functions.verfahrensgebiet_manager import (
|
||||
setup_verfahrensgebiet_listener,
|
||||
teardown_verfahrensgebiet_listener,
|
||||
)
|
||||
from sn_basis.modules.print_kartenrand import (
|
||||
setup_mindestbreiten_listener,
|
||||
teardown_mindestbreiten_listener,
|
||||
)
|
||||
from sn_basis.ui.navigation import Navigation
|
||||
|
||||
|
||||
@@ -41,6 +45,8 @@ class BasisPlugin:
|
||||
|
||||
# Zentrale Verwaltung des Verfahrensgebiet-Layers starten
|
||||
setup_verfahrensgebiet_listener()
|
||||
# Mindestbreite der Drucklegenden nach dem Laden eines Projekts wieder setzen (Basis #96)
|
||||
setup_mindestbreiten_listener()
|
||||
|
||||
for name, plugin in plugins.items():
|
||||
if name.startswith("sn_") and name != "sn_basis":
|
||||
@@ -57,6 +63,7 @@ class BasisPlugin:
|
||||
Räumt UI-Komponenten sauber auf.
|
||||
"""
|
||||
teardown_verfahrensgebiet_listener()
|
||||
teardown_mindestbreiten_listener()
|
||||
|
||||
if self.ui:
|
||||
self.ui.remove_all()
|
||||
|
||||
@@ -40,7 +40,7 @@ from sn_basis.functions.qgiscore_wrapper import (
|
||||
QgsVectorLayer,
|
||||
)
|
||||
from sn_basis.functions.qt_wrapper import QSize
|
||||
from sn_basis.modules.print_kartenrand import TEXTUMBRUCH_MM
|
||||
from sn_basis.modules.print_kartenrand import MIN_SPALTENBREITE_MM, TEXTUMBRUCH_MM
|
||||
|
||||
MM = QgsUnitTypes.LayoutMillimeters
|
||||
|
||||
@@ -332,6 +332,8 @@ def _neues_legendenelement(layout: Any) -> Any:
|
||||
_aufruf(legende, "setFrameEnabled", True)
|
||||
_aufruf(legende, "setBackgroundEnabled", False)
|
||||
_aufruf(legende, "setFrameStrokeWidth", _layout_messung(RAHMENSTAERKE_MM))
|
||||
# Mindestens so breit wie das Kartenschild (Basis #96)
|
||||
_aufruf(legende, "setMinimumSize", QgsLayoutSize(MIN_SPALTENBREITE_MM, 0.0, MM))
|
||||
layout.addLayoutItem(legende)
|
||||
return legende
|
||||
|
||||
|
||||
@@ -21,13 +21,22 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Callable
|
||||
|
||||
from sn_basis.functions.qgiscore_wrapper import QgsLayoutExporter, QgsLayoutPoint, QgsLayoutSize, QgsUnitTypes
|
||||
from sn_basis.functions.qgiscore_wrapper import (
|
||||
QGIS_AVAILABLE,
|
||||
QgsLayoutExporter,
|
||||
QgsLayoutPoint,
|
||||
QgsLayoutSize,
|
||||
QgsProject,
|
||||
QgsUnitTypes,
|
||||
)
|
||||
from sn_basis.functions.qt_wrapper import QSize
|
||||
|
||||
MM = QgsUnitTypes.LayoutMillimeters
|
||||
|
||||
RAND_MM = 10.0 # Seitenrand und Abstand Karte → Kartenrand
|
||||
MIN_SPALTENBREITE_MM = 180.0 # Breite von Kartenschild und Quellenangabe
|
||||
# Breite von Kartenschild und Quellenangabe (PrintLayout._LEGEND_WIDTH_MM) – zugleich Standard- und
|
||||
# Mindestbreite der Legenden, damit sie optisch mit dem Kartenschild übereinstimmen.
|
||||
MIN_SPALTENBREITE_MM = 180.0
|
||||
ABSTAND_LEGENDEN_MM = 4.0 # unter jeder Legende
|
||||
QUELLE_HOEHE_MM = 20.0
|
||||
ABSTAND_QUELLE_SCHILD_MM = 10.0
|
||||
@@ -41,6 +50,9 @@ TEXTUMBRUCH_MM = 60.0
|
||||
HAUPTKARTE_ID = "Hauptkarte"
|
||||
LEGENDEN_IDS = ("Legende", "Legende Basemap") # von oben nach unten
|
||||
BASEMAP_LEGENDE_ID = "Legende Basemap"
|
||||
# Layout-Eigenschaft: Mindestbreite der Legenden. QGIS speichert setMinimumSize nicht im Projekt;
|
||||
# der Vermerk erlaubt, sie beim Laden wieder zu setzen (wende_mindestbreiten_an).
|
||||
LAYOUT_MINDESTBREITE = "sn_legende_mindestbreite"
|
||||
QUELLENANGABE_ID = "Quellenangabe"
|
||||
# Elemente unterhalb der Legenden, die bei höherer Seite gemeinsam nach unten rutschen
|
||||
UNTERE_ELEMENTE = (
|
||||
@@ -141,6 +153,84 @@ def _rendere_legenden(layout: Any, legenden: list[Any]) -> None:
|
||||
_aufruf(exporter, "renderRegionToImage", legende.sceneBoundingRect(), QSize(), VERMESSUNG_DPI)
|
||||
|
||||
|
||||
def setze_legenden_mindestbreite(layout: Any, breite: float = MIN_SPALTENBREITE_MM, merken: bool = True) -> int:
|
||||
"""Gibt den Legenden des Layouts eine Mindestbreite (Standard: Breite des Kartenschilds).
|
||||
|
||||
Die Legenden passen sich weiter ihrem Inhalt an, werden aber nie schmaler. Mit ``merken`` wird die
|
||||
Breite am Layout vermerkt, damit sie nach dem Laden des Projekts wieder gesetzt werden kann.
|
||||
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
Anzahl der Legenden, die eine Mindestbreite bekommen haben.
|
||||
"""
|
||||
anzahl = 0
|
||||
for legenden_id in LEGENDEN_IDS:
|
||||
legende = _aufruf(layout, "itemById", legenden_id)
|
||||
if legende is not None:
|
||||
legende.setMinimumSize(QgsLayoutSize(breite, 0.0, MM))
|
||||
anzahl += 1
|
||||
if merken:
|
||||
layout.setCustomProperty(LAYOUT_MINDESTBREITE, float(breite))
|
||||
return anzahl
|
||||
|
||||
|
||||
def wende_mindestbreiten_an(project: Any | None = None) -> int:
|
||||
"""Setzt die vermerkten Mindestbreiten der Legenden aller Drucklayouts erneut (nach dem Laden).
|
||||
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
Anzahl der Layouts, an denen eine Mindestbreite gesetzt wurde.
|
||||
"""
|
||||
project = project or QgsProject.instance()
|
||||
verwaltung = _aufruf(project, "layoutManager")
|
||||
anzahl = 0
|
||||
for layout in _aufruf(verwaltung, "printLayouts", standard=[]) or []:
|
||||
wert = _aufruf(layout, "customProperty", LAYOUT_MINDESTBREITE, None)
|
||||
try:
|
||||
breite = float(wert)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if breite > 0 and setze_legenden_mindestbreite(layout, breite, merken=False):
|
||||
anzahl += 1
|
||||
return anzahl
|
||||
|
||||
|
||||
_listener_aktiv = False
|
||||
|
||||
|
||||
def _on_projekt_gelesen(*_: Any) -> None:
|
||||
wende_mindestbreiten_an()
|
||||
|
||||
|
||||
def setup_mindestbreiten_listener() -> None:
|
||||
"""Setzt die Mindestbreiten bei jedem Laden eines Projekts neu (einmal beim Plugin-Start aufrufen)."""
|
||||
global _listener_aktiv
|
||||
if _listener_aktiv or not QGIS_AVAILABLE:
|
||||
return
|
||||
try:
|
||||
project = QgsProject.instance()
|
||||
project.readProject.connect(_on_projekt_gelesen)
|
||||
_listener_aktiv = True
|
||||
wende_mindestbreiten_an(project) # bereits geöffnetes Projekt
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def teardown_mindestbreiten_listener() -> None:
|
||||
"""Trennt den Listener wieder (beim Entladen des Plugins; idempotent)."""
|
||||
global _listener_aktiv
|
||||
if not _listener_aktiv:
|
||||
return
|
||||
try:
|
||||
QgsProject.instance().readProject.disconnect(_on_projekt_gelesen)
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
_listener_aktiv = False
|
||||
|
||||
|
||||
def _verschiebe(element: Any, dy: float) -> None:
|
||||
position = element.positionWithUnits()
|
||||
element.attemptMove(QgsLayoutPoint(_wert(position, "x"), _wert(position, "y") + dy, MM))
|
||||
@@ -160,6 +250,7 @@ def ordne_kartenrand_an(layout: Any, rendern: Callable[[Any, list], None] | None
|
||||
legenden = [l for l in (_aufruf(layout, "itemById", i) for i in LEGENDEN_IDS) if l is not None]
|
||||
for legende in legenden:
|
||||
_aufruf(legende, "setLocked", False)
|
||||
setze_legenden_mindestbreite(layout)
|
||||
(rendern or _rendere_legenden)(layout, legenden)
|
||||
|
||||
groessen = {n: [_legende_groesse(l, n) for l in legenden] for n in range(1, MAX_SPALTEN + 1)}
|
||||
|
||||
@@ -235,6 +235,19 @@ class TestPlatzierung(unittest.TestCase):
|
||||
self.assertFalse(einstellungen.werte["setEqualColumnWidth"])
|
||||
self.assertEqual(einstellungen.werte["setAutoWrapLinesAfter"], bll.TEXTUMBRUCH_MM)
|
||||
|
||||
def test_neue_basemap_legende_mindestens_so_breit_wie_kartenschild(self):
|
||||
"""Prüft, dass eine neu angelegte Basemap-Legende die Breite des Kartenschilds als Mindestbreite bekommt."""
|
||||
class Element:
|
||||
def __init__(self, layout): self.mindestbreite = None
|
||||
def __getattr__(self, name): return lambda *a: None
|
||||
def setMinimumSize(self, groesse):
|
||||
self.mindestbreite = groesse.width() if callable(groesse.width) else groesse.width
|
||||
|
||||
layout = type("L", (), {"addLayoutItem": lambda s, e: None})()
|
||||
with patch.object(bll, "QgsLayoutItemLegend", Element):
|
||||
legende = bll._neues_legendenelement(layout)
|
||||
self.assertEqual(legende.mindestbreite, 180.0)
|
||||
|
||||
def test_titel_zweizeilig(self):
|
||||
"""Prüft, dass der Titel der Basemap-Legende zweizeilig ist, damit er die Mindestbreite nicht bestimmt (#96)."""
|
||||
self.assertEqual(bll.LEGENDE_TITEL.splitlines(), ["Kartengrundlage", "(basemap.de)"])
|
||||
|
||||
@@ -97,11 +97,14 @@ class _Legende(_Element):
|
||||
super().__init__(x, y, groessen[1][0], groessen[1][1])
|
||||
self.groessen, self.spalten, self.unten = groessen, 1, unten_verankert
|
||||
self.gleiche_breite, self.umbruch = True, 0
|
||||
self.mindestbreite = None
|
||||
|
||||
def setColumnCount(self, n): self.spalten = n
|
||||
def setSplitLayer(self, wert): pass
|
||||
def setEqualColumnWidth(self, wert): self.gleiche_breite = wert
|
||||
def setAutoWrapLinesAfter(self, mm): self.umbruch = mm
|
||||
def setMinimumSize(self, groesse):
|
||||
self.mindestbreite = groesse.width() if callable(groesse.width) else groesse.width
|
||||
def setLocked(self, wert): pass
|
||||
def adjustBoxSize(self):
|
||||
self.b, self.h = self.groessen[self.spalten]
|
||||
@@ -121,6 +124,10 @@ class _Seite:
|
||||
class _Layout:
|
||||
def __init__(self, elemente, seite):
|
||||
self.elemente, self.seite = elemente, seite
|
||||
self.eigenschaften = {}
|
||||
|
||||
def setCustomProperty(self, k, v): self.eigenschaften[k] = v
|
||||
def customProperty(self, k, standard=None): return self.eigenschaften.get(k, standard)
|
||||
|
||||
def itemById(self, element_id): return self.elemente.get(element_id)
|
||||
def pageCollection(self): return type("P", (), {"page": lambda s, i: self.seite})()
|
||||
@@ -160,6 +167,14 @@ class TestOrdneKartenrandAn(unittest.TestCase):
|
||||
self.assertFalse(elemente["Legende"].gleiche_breite)
|
||||
self.assertEqual(elemente["Legende"].umbruch, kr.TEXTUMBRUCH_MM)
|
||||
|
||||
def test_legenden_mindestens_so_breit_wie_kartenschild(self):
|
||||
"""Prüft, dass die Legenden die Breite des Kartenschilds als Mindestbreite bekommen und das Layout sie für das Neuladen vermerkt."""
|
||||
layout, elemente = self._layout({1: (60, 300), 2: (120, 160), 3: (175, 110)})
|
||||
ordne_kartenrand_an(layout, rendern=lambda *a: None)
|
||||
self.assertEqual(elemente["Legende"].mindestbreite, kr.MIN_SPALTENBREITE_MM)
|
||||
self.assertEqual(kr.MIN_SPALTENBREITE_MM, 180.0) # Breite von Kartenschild und Quellenangabe
|
||||
self.assertEqual(float(layout.customProperty(kr.LAYOUT_MINDESTBREITE)), kr.MIN_SPALTENBREITE_MM)
|
||||
|
||||
def test_passt_nichts_wird_verschoben(self):
|
||||
"""Prüft, dass bei ausreichend hoher Karte weder Seite noch Kartenschild verändert werden."""
|
||||
layout, elemente = self._layout({1: (100, 10), 2: (200, 6), 3: (300, 4)})
|
||||
@@ -174,6 +189,44 @@ class TestOrdneKartenrandAn(unittest.TestCase):
|
||||
self.assertIsNone(ordne_kartenrand_an(_Layout({}, _Seite(100, 100)), rendern=lambda *a: None))
|
||||
|
||||
|
||||
class TestMindestbreiteBeimLaden(unittest.TestCase):
|
||||
|
||||
def _projekt(self, *layouts):
|
||||
verwaltung = type("V", (), {"printLayouts": lambda s: list(layouts)})()
|
||||
return type("P", (), {"layoutManager": lambda s: verwaltung})()
|
||||
|
||||
def test_mindestbreite_nur_fuer_vermerkte_layouts(self):
|
||||
"""Prüft, dass beim Laden die Mindestbreite nur an Layouts mit Vermerk wieder gesetzt wird."""
|
||||
mit = _Layout({"Legende": _Legende(0, 0, {1: (60, 10)}), "Legende Basemap": _Legende(0, 0, {1: (90, 10)})}, _Seite(1, 1))
|
||||
mit.setCustomProperty(kr.LAYOUT_MINDESTBREITE, "180.0")
|
||||
ohne = _Layout({"Legende": _Legende(0, 0, {1: (60, 10)})}, _Seite(1, 1))
|
||||
self.assertEqual(kr.wende_mindestbreiten_an(self._projekt(mit, ohne)), 1)
|
||||
self.assertEqual(mit.elemente["Legende"].mindestbreite, 180.0)
|
||||
self.assertEqual(mit.elemente["Legende Basemap"].mindestbreite, 180.0)
|
||||
self.assertIsNone(ohne.elemente["Legende"].mindestbreite)
|
||||
|
||||
def test_listener_verbinden_und_trennen(self):
|
||||
"""Prüft, dass der Listener readProject einmal verbindet, beim Trennen wieder löst und beides mehrfach aufgerufen werden darf."""
|
||||
signal = MagicMock()
|
||||
projekt = MagicMock()
|
||||
projekt.readProject = signal
|
||||
projekt.layoutManager.return_value.printLayouts.return_value = []
|
||||
with patch.object(kr, "QGIS_AVAILABLE", True), patch.object(kr, "QgsProject") as qgs_projekt:
|
||||
qgs_projekt.instance.return_value = projekt
|
||||
kr.setup_mindestbreiten_listener()
|
||||
kr.setup_mindestbreiten_listener()
|
||||
self.assertEqual(signal.connect.call_count, 1)
|
||||
kr.teardown_mindestbreiten_listener()
|
||||
kr.teardown_mindestbreiten_listener()
|
||||
self.assertEqual(signal.disconnect.call_count, 1)
|
||||
|
||||
def test_ohne_qgis_kein_listener(self):
|
||||
"""Prüft, dass im Mock-Modus kein Listener verbunden wird."""
|
||||
with patch.object(kr, "QGIS_AVAILABLE", False), patch.object(kr, "QgsProject") as qgs_projekt:
|
||||
kr.setup_mindestbreiten_listener()
|
||||
qgs_projekt.instance.assert_not_called()
|
||||
|
||||
|
||||
class TestAnbindungPrintLayout(unittest.TestCase):
|
||||
|
||||
def test_vorlage_ordnet_kartenrand_an(self):
|
||||
|
||||
Reference in New Issue
Block a user