Merge pull request 'Tabs der LNO-Docks in Scroll-Bereich packen (#104)' (#105) from bugfix/104-dock-scrollbereich into unstable
Reviewed-on: #105
This commit was merged in pull request #105.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
- Bugfix: Ein langer Tab (z. B. der Druck-Tab mit aufgeklapptem Basemap-Bereich) schob den unteren Rand des QGIS-Fensters über den Bildschirm hinaus, statt einen Scrollbalken zu zeigen. Jeder Tab der LNO-Docks liegt jetzt in einem Scroll-Bereich; das gilt auch für Fachplugins, die ihre Tabs selbst aufbauen (Plan41, Verfahrensgebiet), ohne dass sie geändert werden mussten (#104)
|
||||
- Basemap-Bereich im Druck-Tab: neuer Abschnitt „Basemap-Layer“ mit Knopf „Basemap hinzufügen“ und Auswahl „Thema“ (Farbe, Grau; Standard Farbe). Liegt schon ein Layer mit derselben Quell-URL im Projekt, nennt eine Rückfrage ihn und bietet „Zusätzlich laden“, „Ersetzen“ (vorhandenen Layer im gewählten Thema neu laden, Position und Bezüge in Layouts bleiben) und „Abbrechen“. Die Themenauswahl stellt den im Layerbaum ausgewählten Basemap-Layer um (sonst den ersten). Vor dem Überschreiben der Darstellung wird geprüft, ob sie seit dem Laden des Themas oder Stils verändert wurde (Prüfwert als Layer-Eigenschaft); dann „Als Stil speichern …“, „Verwerfen“ oder „Abbrechen“ (#100)
|
||||
- Stilvorlagen des Basemap-Layers vermerken ihr Thema; Laden, Speichern und „Punktlinien korrigieren“ wirken auf den im Layerbaum ausgewählten Basemap-Layer, sonst wie bisher auf den ersten (#100)
|
||||
- Neue allgemeine Funktionen `dialog_wrapper.ask_auswahl` (Rückfrage mit frei benannten Knöpfen) und `qgisui_wrapper.get_active_layer` (#100)
|
||||
|
||||
+42
-1
@@ -56,6 +56,9 @@ QTableWidgetItem: Type[Any] = object
|
||||
QTableWidgetSelectionRange: Type[Any] = object
|
||||
QIcon: Type[Any] = object
|
||||
QSize: Type[Any] = object
|
||||
QScrollArea: Type[Any] = object
|
||||
QFrame: Type[Any] = object
|
||||
FrameNoFrame: Any = 0
|
||||
|
||||
|
||||
def exec_dialog(dialog: Any) -> Any:
|
||||
@@ -116,6 +119,8 @@ try:
|
||||
QTableWidget as _QTableWidget,
|
||||
QTableWidgetItem as _QTableWidgetItem,
|
||||
QTableWidgetSelectionRange as _QTableWidgetSelectionRange,
|
||||
QScrollArea as _QScrollArea,
|
||||
QFrame as _QFrame,
|
||||
)
|
||||
from qgis.PyQt.QtGui import QFont as _QFont, QIcon as _QIcon
|
||||
from qgis.PyQt.QtCore import (
|
||||
@@ -176,6 +181,8 @@ try:
|
||||
QTableWidget = _QTableWidget
|
||||
QTableWidgetItem = _QTableWidgetItem
|
||||
QTableWidgetSelectionRange = _QTableWidgetSelectionRange
|
||||
QScrollArea = _QScrollArea
|
||||
QFrame = _QFrame
|
||||
QIcon = _QIcon
|
||||
QSize = _QSize
|
||||
# ✅ QT6 ENUMS
|
||||
@@ -194,6 +201,7 @@ try:
|
||||
SizePolicyPreferred = QSizePolicy.Policy.Preferred
|
||||
SizePolicyMaximum = QSizePolicy.Policy.Maximum
|
||||
DockWidgetMovable = QDockWidget.DockWidgetFeature.DockWidgetMovable
|
||||
FrameNoFrame = QFrame.Shape.NoFrame
|
||||
DockWidgetFloatable = QDockWidget.DockWidgetFeature.DockWidgetFloatable
|
||||
DockWidgetClosable = QDockWidget.DockWidgetFeature.DockWidgetClosable
|
||||
DockAreaLeft = Qt.DockWidgetArea.LeftDockWidgetArea
|
||||
@@ -247,6 +255,8 @@ except (ImportError, AttributeError):
|
||||
QTableWidget as _QTableWidget,
|
||||
QTableWidgetItem as _QTableWidgetItem,
|
||||
QTableWidgetSelectionRange as _QTableWidgetSelectionRange,
|
||||
QScrollArea as _QScrollArea,
|
||||
QFrame as _QFrame,
|
||||
)
|
||||
from PyQt5.QtGui import QFont as _QFont, QIcon as _QIcon
|
||||
from PyQt5.QtCore import (
|
||||
@@ -305,6 +315,8 @@ except (ImportError, AttributeError):
|
||||
QTableWidget = _QTableWidget
|
||||
QTableWidgetItem = _QTableWidgetItem
|
||||
QTableWidgetSelectionRange = _QTableWidgetSelectionRange
|
||||
QScrollArea = _QScrollArea
|
||||
QFrame = _QFrame
|
||||
QIcon = _QIcon
|
||||
QSize = _QSize
|
||||
|
||||
@@ -325,6 +337,7 @@ except (ImportError, AttributeError):
|
||||
SizePolicyPreferred = QSizePolicy.Preferred
|
||||
SizePolicyMaximum = QSizePolicy.Maximum
|
||||
DockWidgetMovable = QDockWidget.DockWidgetMovable
|
||||
FrameNoFrame = QFrame.NoFrame
|
||||
DockWidgetFloatable = QDockWidget.DockWidgetFloatable
|
||||
DockWidgetClosable = QDockWidget.DockWidgetClosable
|
||||
DockAreaLeft = Qt.LeftDockWidgetArea
|
||||
@@ -492,9 +505,28 @@ except (ImportError, AttributeError):
|
||||
def show(self) -> None: pass
|
||||
def deleteLater(self) -> None: pass
|
||||
def closeEvent(self, event) -> None: pass
|
||||
def setWidget(self, widget) -> None: self._widget = widget
|
||||
def widget(self): return getattr(self, "_widget", None)
|
||||
def setFeatures(self, *args, **kwargs) -> None: pass
|
||||
def setAllowedAreas(self, *args, **kwargs) -> None: pass
|
||||
def setWindowTitle(self, title: str) -> None: self._title = title
|
||||
|
||||
QDockWidget = _MockQDockWidget
|
||||
|
||||
class _MockQScrollArea(_MockWidget):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._widget = None
|
||||
self._resizable = False
|
||||
def setWidget(self, widget) -> None: self._widget = widget
|
||||
def widget(self): return self._widget
|
||||
def setWidgetResizable(self, value: bool) -> None: self._resizable = bool(value)
|
||||
def widgetResizable(self) -> bool: return self._resizable
|
||||
def setFrameShape(self, *args, **kwargs) -> None: pass
|
||||
|
||||
QScrollArea = _MockQScrollArea
|
||||
QFrame = _MockWidget
|
||||
FrameNoFrame = 0
|
||||
|
||||
class _MockAction:
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._checked = False
|
||||
@@ -556,8 +588,17 @@ except (ImportError, AttributeError):
|
||||
DockWidgetClosable = 4
|
||||
|
||||
class _MockTabWidget:
|
||||
def __init__(self, *args, **kwargs): self._tabs = []
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._tabs = []
|
||||
self._aktuell = 0
|
||||
def addTab(self, widget, title: str): self._tabs.append((widget, title))
|
||||
def insertTab(self, index: int, widget, title: str): self._tabs.insert(index, (widget, title))
|
||||
def removeTab(self, index: int): self._tabs.pop(index)
|
||||
def count(self) -> int: return len(self._tabs)
|
||||
def widget(self, index: int): return self._tabs[index][0] if 0 <= index < len(self._tabs) else None
|
||||
def tabText(self, index: int) -> str: return self._tabs[index][1]
|
||||
def currentIndex(self) -> int: return self._aktuell
|
||||
def setCurrentIndex(self, index: int) -> None: self._aktuell = index
|
||||
|
||||
QTabWidget = _MockTabWidget
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"""Tests für sn_basis/ui/base_dockwidget.py::closeEvent (Issue #65)."""
|
||||
"""Tests für sn_basis/ui/base_dockwidget.py: closeEvent (Issue #65) und Scroll-Bereich je Tab (Basis #104)."""
|
||||
|
||||
import unittest
|
||||
|
||||
from sn_basis.functions.qt_wrapper import QScrollArea, QTabWidget
|
||||
from sn_basis.ui.base_dockwidget import BaseDockWidget
|
||||
|
||||
|
||||
@@ -95,5 +96,73 @@ class CloseEventTests(unittest.TestCase):
|
||||
dock.closeEvent(None) # darf keine Ausnahme werfen
|
||||
|
||||
|
||||
class _TabA:
|
||||
tab_title = "Daten"
|
||||
|
||||
|
||||
class _TabB:
|
||||
tab_title = "Druck"
|
||||
|
||||
|
||||
class _Dock(BaseDockWidget):
|
||||
tabs = [_TabA, _TabB]
|
||||
|
||||
|
||||
class ScrollbereichTests(unittest.TestCase):
|
||||
"""Lange Tabs dürfen das QGIS-Fenster nicht über den Bildschirmrand schieben (Basis #104)."""
|
||||
|
||||
def test_jeder_tab_liegt_in_einem_scrollbereich(self):
|
||||
"""Prüft, dass BaseDockWidget jeden Tab in einen mitwachsenden Scroll-Bereich packt, damit ein langer Tab scrollt, statt das Fenster zu verlängern."""
|
||||
reiter = _Dock().widget()
|
||||
self.assertEqual(reiter.count(), 2)
|
||||
for index, klasse in enumerate((_TabA, _TabB)):
|
||||
bereich = reiter.widget(index)
|
||||
self.assertIsInstance(bereich, QScrollArea)
|
||||
self.assertTrue(bereich.widgetResizable())
|
||||
self.assertIsInstance(bereich.widget(), klasse)
|
||||
self.assertEqual(reiter.tabText(index), klasse.tab_title)
|
||||
|
||||
def test_fachplugin_mit_eigenem_tabaufbau_bekommt_scrollbereich(self):
|
||||
"""Prüft, dass auch ein Fachplugin, das seine Tabs selbst aufbaut und per setWidget einsetzt (wie Plan41, Verfahrensgebiet), Scroll-Bereiche bekommt."""
|
||||
dock = _Dock()
|
||||
eigene = QTabWidget()
|
||||
tab = _TabA()
|
||||
eigene.addTab(tab, "Eigener Tab")
|
||||
eigene.setCurrentIndex(0)
|
||||
dock.setWidget(eigene)
|
||||
self.assertIs(dock.widget(), eigene)
|
||||
self.assertIsInstance(eigene.widget(0), QScrollArea)
|
||||
self.assertIs(eigene.widget(0).widget(), tab)
|
||||
self.assertEqual(eigene.tabText(0), "Eigener Tab")
|
||||
self.assertEqual(eigene.currentIndex(), 0)
|
||||
|
||||
def test_scrollbereich_wird_nicht_doppelt_angelegt(self):
|
||||
"""Prüft, dass ein Tab, der schon in einem Scroll-Bereich liegt, beim erneuten Einsetzen nicht ein zweites Mal verpackt wird."""
|
||||
dock = _Dock()
|
||||
reiter = dock.widget()
|
||||
bereich = reiter.widget(0)
|
||||
dock.setWidget(reiter)
|
||||
self.assertIs(reiter.widget(0), bereich)
|
||||
|
||||
def test_setwidget_none_und_andere_widgets_bleiben_unveraendert(self):
|
||||
"""Prüft, dass setWidget(None) und Widgets ohne Tabs ohne Fehler und unverändert eingesetzt werden."""
|
||||
dock = _Dock()
|
||||
dock.setWidget(None)
|
||||
self.assertIsNone(dock.widget())
|
||||
einfach = _TabA()
|
||||
dock.setWidget(einfach)
|
||||
self.assertIs(dock.widget(), einfach)
|
||||
|
||||
def test_cleanup_erreicht_tabs_im_scrollbereich(self):
|
||||
"""Prüft, dass closeEvent cleanup() auch für Tabs aufruft, die in einem Scroll-Bereich liegen."""
|
||||
dock = _neues_dock()
|
||||
tab = _FakeTab()
|
||||
bereich = QScrollArea()
|
||||
bereich.setWidget(tab)
|
||||
dock.widget = lambda: _FakeTabWidget([bereich])
|
||||
dock.closeEvent(None)
|
||||
self.assertTrue(tab.cleanup_aufgerufen)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
+48
-1
@@ -9,6 +9,8 @@ from sn_basis.functions.message_wrapper import warning, error
|
||||
from sn_basis.functions.qt_wrapper import (
|
||||
QDockWidget,
|
||||
QTabWidget,
|
||||
QScrollArea,
|
||||
FrameNoFrame,
|
||||
Qt,
|
||||
DockWidgetMovable,
|
||||
DockWidgetFloatable,
|
||||
@@ -18,6 +20,36 @@ from sn_basis.functions.qt_wrapper import (
|
||||
)
|
||||
|
||||
|
||||
def _inhalt(seite):
|
||||
"""Tab-Instanz einer Reiterseite; liegt sie in einem Scroll-Bereich, dessen Inhalt."""
|
||||
if isinstance(seite, QScrollArea):
|
||||
return seite.widget()
|
||||
return seite
|
||||
|
||||
|
||||
def tabs_mit_scrollbereich(tab_widget) -> None:
|
||||
"""Packt jeden Tab eines QTabWidget in einen mitwachsenden Scroll-Bereich (Basis #104).
|
||||
|
||||
Ohne Scroll-Bereich verlangt Qt für das Dock mindestens die Höhe des höchsten Tabs; ist
|
||||
der Tab höher als der Bildschirm, schiebt QGIS seinen unteren Fensterrand aus dem Bild.
|
||||
Tabs, die schon in einem Scroll-Bereich liegen, bleiben unverändert.
|
||||
"""
|
||||
if not callable(getattr(tab_widget, "count", None)) or not callable(getattr(tab_widget, "insertTab", None)):
|
||||
return
|
||||
aktuell = tab_widget.currentIndex()
|
||||
for index in range(tab_widget.count()):
|
||||
seite = tab_widget.widget(index)
|
||||
if seite is None or isinstance(seite, QScrollArea):
|
||||
continue
|
||||
titel = tab_widget.tabText(index)
|
||||
bereich = QScrollArea()
|
||||
bereich.setWidgetResizable(True)
|
||||
bereich.setFrameShape(FrameNoFrame)
|
||||
tab_widget.removeTab(index)
|
||||
bereich.setWidget(seite)
|
||||
tab_widget.insertTab(index, bereich, titel)
|
||||
tab_widget.setCurrentIndex(aktuell)
|
||||
|
||||
|
||||
class BaseDockWidget(QDockWidget):
|
||||
"""
|
||||
@@ -26,6 +58,8 @@ class BaseDockWidget(QDockWidget):
|
||||
- Titel wird automatisch aus base_title + subtitle erzeugt
|
||||
- Tabs werden dynamisch aus der Klassenvariable 'tabs' erzeugt
|
||||
- Die zugehörige Toolbar-Action wird beim Schließen zurückgesetzt
|
||||
- Jeder Tab liegt in einem Scroll-Bereich (``setWidget``), auch wenn ein Fachplugin
|
||||
seine Tabs selbst aufbaut (Basis #104)
|
||||
"""
|
||||
|
||||
base_title = "LNO Sachsen"
|
||||
@@ -90,6 +124,19 @@ class BaseDockWidget(QDockWidget):
|
||||
str(e),
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Inhalt setzen
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def setWidget(self, widget):
|
||||
"""Wie ``QDockWidget.setWidget``; die Tabs eines QTabWidget bekommen je einen Scroll-Bereich."""
|
||||
if widget is not None:
|
||||
try:
|
||||
tabs_mit_scrollbereich(widget)
|
||||
except Exception as e:
|
||||
warning("Scroll-Bereich konnte nicht angelegt werden", str(e))
|
||||
super().setWidget(widget)
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Dock schließen
|
||||
# ---------------------------------------------------------
|
||||
@@ -103,7 +150,7 @@ class BaseDockWidget(QDockWidget):
|
||||
tab_widget = self.widget()
|
||||
if tab_widget and hasattr(tab_widget, "count") and hasattr(tab_widget, "widget"):
|
||||
for index in range(tab_widget.count()):
|
||||
tab = tab_widget.widget(index)
|
||||
tab = _inhalt(tab_widget.widget(index))
|
||||
cleanup = getattr(tab, "cleanup", None)
|
||||
if callable(cleanup):
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user