From a632f35c40e8323378f2464a6a82a484edf4ff33 Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Wed, 25 Feb 2026 15:12:26 +0100 Subject: [PATCH] fix(dock_area): tabbed dock have correct parent --- .../containers/dock_area/basic_dock_area.py | 70 ++++++++++++++----- tests/unit_tests/test_dock_area.py | 25 +++++++ 2 files changed, 77 insertions(+), 18 deletions(-) diff --git a/bec_widgets/widgets/containers/dock_area/basic_dock_area.py b/bec_widgets/widgets/containers/dock_area/basic_dock_area.py index 6d174fc3..940bace9 100644 --- a/bec_widgets/widgets/containers/dock_area/basic_dock_area.py +++ b/bec_widgets/widgets/containers/dock_area/basic_dock_area.py @@ -113,6 +113,7 @@ class DockAreaWidget(BECWidget, QWidget): ) self._root_layout.addWidget(self.dock_manager, 1) + self._install_manager_parent_guards() ################################################################################ # Dock Utility Helpers @@ -255,6 +256,54 @@ class DockAreaWidget(BECWidget, QWidget): return lambda dock: self._default_close_handler(dock, widget) + def _install_manager_parent_guards(self) -> None: + """ + Track ADS structural changes so drag/drop-created tab areas keep stable parenting. + """ + self.dock_manager.dockAreaCreated.connect(self._normalize_all_dock_parents) + self.dock_manager.dockWidgetAdded.connect(self._normalize_all_dock_parents) + self.dock_manager.stateRestored.connect(self._normalize_all_dock_parents) + self.dock_manager.restoringState.connect(self._normalize_all_dock_parents) + self.dock_manager.focusedDockWidgetChanged.connect(self._normalize_all_dock_parents) + self._normalize_all_dock_parents() + + def _iter_all_dock_areas(self) -> list[CDockAreaWidget]: + """Return all dock areas from all known dock containers.""" + areas: list[CDockAreaWidget] = [] + for i in range(self.dock_manager.dockAreaCount()): + area = self.dock_manager.dockArea(i) + if area is None or not isValid(area): + continue + areas.append(area) + return areas + + def _connect_dock_area_parent_guards(self) -> None: + """Bind area-level tab/view events to parent normalization.""" + for area in self._iter_all_dock_areas(): + try: + area.currentChanged.connect( + self._normalize_all_dock_parents, Qt.ConnectionType.UniqueConnection + ) + area.viewToggled.connect( + self._normalize_all_dock_parents, Qt.ConnectionType.UniqueConnection + ) + except TypeError: + area.currentChanged.connect(self._normalize_all_dock_parents) + area.viewToggled.connect(self._normalize_all_dock_parents) + + def _normalize_all_dock_parents(self, *_args) -> None: + """ + Ensure each dock has a stable parent after tab switches, re-docking, or restore. + """ + self._connect_dock_area_parent_guards() + for dock in self.dock_list(): + if dock is None or not isValid(dock): + continue + area_widget = dock.dockAreaWidget() + target_parent = area_widget if area_widget is not None else self.dock_manager + if dock.parent() is not target_parent: + dock.setParent(target_parent) + def _make_dock( self, widget: QWidget, @@ -357,6 +406,7 @@ class DockAreaWidget(BECWidget, QWidget): self._apply_floating_state_to_dock(dock, floating_state) if resolved_icon is not None: dock.setIcon(resolved_icon) + self._normalize_all_dock_parents() return dock def _delete_dock(self, dock: CDockWidget) -> None: @@ -1335,29 +1385,13 @@ class DockAreaWidget(BECWidget, QWidget): dock = self._create_dock_from_spec(spec) return dock if return_dock else widget - def _iter_all_docks(self) -> list[CDockWidget]: - """Return all docks, including those hosted in floating containers.""" - docks = list(self.dock_manager.dockWidgets()) - seen = {id(d) for d in docks} - for container in self.dock_manager.floatingWidgets(): - if container is None: - continue - for dock in container.dockWidgets(): - if dock is None: - continue - if id(dock) in seen: - continue - docks.append(dock) - seen.add(id(dock)) - return docks - def dock_map(self) -> dict[str, CDockWidget]: """Return the dock widgets map as dictionary with names as keys.""" - return {dock.objectName(): dock for dock in self._iter_all_docks() if dock.objectName()} + return self.dock_manager.dockWidgetsMap() def dock_list(self) -> list[CDockWidget]: """Return the list of dock widgets.""" - return self._iter_all_docks() + return list(self.dock_map().values()) def widget_map(self, bec_widgets_only: bool = True) -> dict[str, QWidget]: """ diff --git a/tests/unit_tests/test_dock_area.py b/tests/unit_tests/test_dock_area.py index 4c53574a..0222e14f 100644 --- a/tests/unit_tests/test_dock_area.py +++ b/tests/unit_tests/test_dock_area.py @@ -331,6 +331,31 @@ class TestBasicDockArea: assert manifest_entries[1]["object_name"] == "anchored_widget" assert manifest_entries[1]["floating"] is False + def test_tabbed_docks_keep_parent_after_tab_switch(self, basic_dock_area, qtbot): + first = QWidget(parent=basic_dock_area) + first.setObjectName("tab_parent_first") + second = QWidget(parent=basic_dock_area) + second.setObjectName("tab_parent_second") + + first_dock = basic_dock_area.new(first, return_dock=True) + second_dock = basic_dock_area.new(second, return_dock=True, tab_with=first_dock) + + dock_area = first_dock.dockAreaWidget() + assert dock_area is not None + qtbot.waitUntil(lambda: second_dock.dockAreaWidget() is dock_area, timeout=1000) + + dock_area.setCurrentDockWidget(second_dock) + qtbot.waitUntil( + lambda: first_dock.parent() is dock_area and second_dock.parent() is dock_area, + timeout=1000, + ) + + dock_area.setCurrentDockWidget(first_dock) + qtbot.waitUntil( + lambda: first_dock.parent() is dock_area and second_dock.parent() is dock_area, + timeout=1000, + ) + def test_splitter_weight_coercion_supports_aliases(self, basic_dock_area): weights = {"default": 0.5, "left": 2, "center": 3, "right": 4}