From bbda2cd61b274a60dfe81aa693dbe3cccfa39fd3 Mon Sep 17 00:00:00 2001 From: daniel Date: Mon, 21 Sep 2026 10:22:29 +0200 Subject: [PATCH] =?UTF-8?q?Navigation.remove=5Fall()=20r=C3=A4umt=20Toolba?= =?UTF-8?q?r/Men=C3=BC=20beim=20Unload=20tats=C3=A4chlich=20auf=20(#72)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit remove_toolbar()/remove_menu() nahmen Toolbar und Menü bisher nur aus der sichtbaren Leiste, das Qt-Objekt blieb als Kind von main_window bestehen. Beim Neuladen mit dem Plugin Reloader meldete QGIS deshalb "removing duplicated widget(s) not cleaned up by the plugin during unload: LnoSachsenToolbar" - der defensive Workaround in init_ui() (Suche nach übrig gebliebenen Toolbars) kaschierte das nur beim nächsten Laden, nicht beim Unload selbst. remove_all() ruft jetzt deleteLater() auf beide Objekte, bevor die Referenzen verworfen werden - dieselbe Behandlung, die init_ui() schon für Altlasten nutzt. deleteLater() ist in PyQt5 und PyQt6 identisch, keine QGIS3/4-Fallunterscheidung nötig. Co-Authored-By: Claude Sonnet 5 --- changelog.txt | 1 + functions/qt_wrapper.py | 6 ++++-- tests/test_navigation.py | 16 +++++++++++++++- ui/navigation.py | 6 ++++++ 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/changelog.txt b/changelog.txt index c6cbabb..efa563c 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,4 @@ +- Bugfix: `Navigation.remove_all()` ruft jetzt `deleteLater()` auf Menü und Toolbar auf, bevor die Referenzen verworfen werden – vorher blieb die Toolbar beim Unload als Kind von main_window bestehen und der Plugin Reloader meldete beim Neuladen "removing duplicated widget(s) not cleaned up by the plugin during unload: LnoSachsenToolbar" (#72) - Kategorie „vielleicht testen" abgeschlossen: 30 weitere Funktionen bekamen Tests (os_wrapper, qgiscore_wrapper, qgisui_wrapper, DataGrabber, pruef_ergebnis, Navigation, SettingsTab, PrintTab – Zustandslogik, Signal-Handling, Atlas-/Theme-Verwaltung), `PrintTab._build_ui` bekam den Vermerk „Test ausgesetzt, zunächst Refactor" (#67) - Testkatalog-Klassifizierung in Docstrings festgehalten: 43 als „vernachlässigbar" eingestufte Funktionen tragen jetzt den Vermerk „von Test abgesehen, weil vernachlässigbar", 41 Print-Bereich-Funktionen (`print_logic.py`, `print_layout.py`, `print_layout_baufreigabe.py`) den Vermerk „Test ausgesetzt, zunächst Refactor" – keine Verhaltensänderung - Tests für weitere zentrale Funktionen ergänzt: Verfahrensgebiet-Zustandsverwaltung, Dock-Verwaltung, Pruefmanager-/Dateipruefer-Entscheidungswege, Fundament-Wrapper (Pfade, Landkreise, Nachrichten, Layer-Existenz/-Stil, GPKG-Layer-Prüfung, QGIS-3/4-Geometriekompatibilität) und Projekteigenschaften-Tab (#65) diff --git a/functions/qt_wrapper.py b/functions/qt_wrapper.py index a085b52..c103004 100644 --- a/functions/qt_wrapper.py +++ b/functions/qt_wrapper.py @@ -491,18 +491,20 @@ except (ImportError, AttributeError): class _MockMenu: def __init__(self, *args, **kwargs): self._actions = [] def addAction(self, action): self._actions.append(action) - def removeAction(self, action): + def removeAction(self, action): if action in self._actions: self._actions.remove(action) def clear(self): self._actions.clear() def menuAction(self): return self + def deleteLater(self) -> None: pass class _MockToolBar: def __init__(self, *args, **kwargs): self._actions = [] def setObjectName(self, name: str) -> None: pass def addAction(self, action): self._actions.append(action) - def removeAction(self, action): + def removeAction(self, action): if action in self._actions: self._actions.remove(action) def clear(self): self._actions.clear() + def deleteLater(self) -> None: pass class _MockActionGroup: def __init__(self, *args, **kwargs): self._actions = [] diff --git a/tests/test_navigation.py b/tests/test_navigation.py index 3eef259..deac75f 100644 --- a/tests/test_navigation.py +++ b/tests/test_navigation.py @@ -163,7 +163,7 @@ class RemoveAllTests(unittest.TestCase): def test_entfernt_menu_toolbar_und_setzt_zustand_zurueck(self, mock_remove_menu, mock_remove_toolbar): """Prüft, dass remove_all Menü und Toolbar über die Wrapper entfernt und den Navigationszustand zurücksetzt.""" nav = Navigation() - fake_menu, fake_toolbar = object(), object() + fake_menu, fake_toolbar = Mock(), Mock() nav.menu = fake_menu nav.toolbar = fake_toolbar nav.actions = [(1, "a1")] @@ -178,6 +178,20 @@ class RemoveAllTests(unittest.TestCase): self.assertEqual(nav.actions, []) self.assertIsNone(nav.plugin_group) + @patch("sn_basis.ui.navigation.remove_toolbar") + @patch("sn_basis.ui.navigation.remove_menu") + def test_loescht_menu_und_toolbar_objekt_damit_kein_duplikat_uebrig_bleibt(self, mock_remove_menu, mock_remove_toolbar): + """Prüft, dass remove_all deleteLater() auf Menü und Toolbar aufruft, damit sie den Qt-Objektbaum tatsächlich verlassen (Issue #72).""" + nav = Navigation() + fake_menu, fake_toolbar = Mock(), Mock() + nav.menu = fake_menu + nav.toolbar = fake_toolbar + + nav.remove_all() + + fake_menu.deleteLater.assert_called_once() + fake_toolbar.deleteLater.assert_called_once() + @patch("sn_basis.ui.navigation.remove_toolbar") @patch("sn_basis.ui.navigation.remove_menu") def test_ohne_menu_und_toolbar_ruft_wrapper_nicht_auf(self, mock_remove_menu, mock_remove_toolbar): diff --git a/ui/navigation.py b/ui/navigation.py index 84010fb..8d7a84f 100644 --- a/ui/navigation.py +++ b/ui/navigation.py @@ -114,12 +114,18 @@ class Navigation: self.actions = [(p, a) for p, a in self.actions if a != action] def remove_all(self): + # remove_menu()/remove_toolbar() nehmen Menü/Toolbar nur aus der sichtbaren Leiste – + # das Qt-Objekt bleibt sonst als Kind von main_window bestehen (Issue #72, vom Plugin + # Reloader als "duplicated widget(s) not cleaned up" gemeldet). deleteLater() räumt es + # tatsächlich auf, analog zum bestehenden Workaround in init_ui(). if self.menu: remove_menu(self.menu) + self.menu.deleteLater() self.menu = None if self.toolbar: remove_toolbar(self.toolbar) + self.toolbar.deleteLater() self.toolbar = None self.actions.clear()