From 25a0ecdb7b227eff5bf3b3b98bb61af48937ec82 Mon Sep 17 00:00:00 2001 From: Sebastiano Romi Date: Wed, 16 Sep 2026 12:47:07 +0200 Subject: [PATCH] Restore saved placement of late-created plugin panels Signed-off-by: Codex --- src/curvemole/gui/plugin_host.py | 7 ++- tests/test_plugin_panel_layout.py | 80 +++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 tests/test_plugin_panel_layout.py diff --git a/src/curvemole/gui/plugin_host.py b/src/curvemole/gui/plugin_host.py index 9b30fea..ed8f18e 100644 --- a/src/curvemole/gui/plugin_host.py +++ b/src/curvemole/gui/plugin_host.py @@ -142,8 +142,11 @@ def restore(project: Any) -> None: scroll.setWidget(result) dialog.setWidget(scroll) dialog.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose) - window.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, dialog) - window.resizeDocks([dialog], [420], Qt.Orientation.Vertical) + # Auto-opened panels are created after MainWindow.restoreState(). + # Restore their saved dock placement before choosing a first-use default. + if not window.restoreDockWidget(dialog): + window.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, dialog) + window.resizeDocks([dialog], [420], Qt.Orientation.Vertical) dialog.show() self.dialogs[entry.identifier] = dialog dialog.destroyed.connect(lambda: self.dialogs.pop(entry.identifier, None)) diff --git a/tests/test_plugin_panel_layout.py b/tests/test_plugin_panel_layout.py new file mode 100644 index 0000000..e5d79f7 --- /dev/null +++ b/tests/test_plugin_panel_layout.py @@ -0,0 +1,80 @@ +"""Plugin docks created after restoreState must recover their saved placement.""" +from types import SimpleNamespace + +import pytest +from PySide6.QtCore import QCoreApplication, QEvent, Qt +from PySide6.QtWidgets import QApplication, QDockWidget, QMainWindow, QWidget + +from curvemole.core.extensions import Contribution, extensions +from curvemole.gui.plugin_host import PluginHost + + +@pytest.fixture +def panel_windows(monkeypatch): + app = QApplication.instance() or QApplication([]) + entry = Contribution("example", "example:panel", "Example", "panels", + lambda context: QWidget(), auto_show=True) + monkeypatch.setattr(extensions, "entries", {entry.identifier: entry}) + windows = [] + + def create(): + window = QMainWindow() + windows.append(window) + window.resize(1000, 700) + window.setCentralWidget(QWidget()) + for name in ("File", "Data", "Tools", "View"): + window.menuBar().addMenu(name) + window.project = SimpleNamespace(ui_state={}) + window.active_curve_id = None + window.curve_tree = SimpleNamespace(selected_curve_ids=lambda: []) + window.plugin_manager = SimpleNamespace(errors={}) + window._thread = None + window._notify = lambda message: None + anchor = QDockWidget("Anchor", window) + anchor.setObjectName("anchor") + anchor.setWidget(QWidget()) + window.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, anchor) + window.plugin_host = PluginHost(window) + window.show() + return window, anchor + + yield app, entry, create + for window in windows: + window.close() + window.deleteLater() + QCoreApplication.sendPostedEvents(None, QEvent.Type.DeferredDelete) + + +@pytest.mark.parametrize("placement", ["left", "bottom", "tabbed", "floating"]) +def test_auto_panel_restores_after_main_window_state(panel_windows, placement): + app, entry, create = panel_windows + first, anchor = create() + app.processEvents() + dock = first.plugin_host.dialogs[entry.identifier] + assert first.dockWidgetArea(dock) == Qt.DockWidgetArea.RightDockWidgetArea + area = (Qt.DockWidgetArea.BottomDockWidgetArea if placement == "bottom" + else Qt.DockWidgetArea.LeftDockWidgetArea) + first.addDockWidget(area, dock) + if placement == "tabbed": + first.tabifyDockWidget(anchor, dock) + elif placement == "floating": + dock.setFloating(True) + dock.setGeometry(120, 140, 360, 280) + app.processEvents() + geometry = dock.geometry() + state = first.saveState() + first.hide() + + restarted, new_anchor = create() + # Match startup: restoreState runs before the queued auto-show callback. + assert not restarted.plugin_host.dialogs + assert restarted.restoreState(state) + app.processEvents() + restored = restarted.plugin_host.dialogs[entry.identifier] + assert restarted.dockWidgetArea(restored) == area + assert restored.isVisible() + assert restored.isFloating() == (placement == "floating") + if placement == "tabbed": + assert restored in restarted.tabifiedDockWidgets(new_anchor) + elif placement == "floating": + assert restored.geometry() == geometry