From 0756ebb3899847a027fac09233be546fdfc6a275 Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Mon, 18 Aug 2025 23:03:51 +0200 Subject: [PATCH] feat(advanced_dock_area): ads has default direction --- bec_widgets/cli/client.py | 33 ++++-- .../advanced_dock_area/advanced_dock_area.py | 109 ++++++++++++------ tests/unit_tests/test_advanced_dock_area.py | 37 +----- 3 files changed, 99 insertions(+), 80 deletions(-) diff --git a/bec_widgets/cli/client.py b/bec_widgets/cli/client.py index 4450afbd..5f7ec6ec 100644 --- a/bec_widgets/cli/client.py +++ b/bec_widgets/cli/client.py @@ -128,20 +128,21 @@ class AdvancedDockArea(RPCBase): floatable: "bool" = True, movable: "bool" = True, start_floating: "bool" = False, + where: "Literal['left', 'right', 'top', 'bottom'] | None" = None, ) -> "BECWidget": """ - Creates a new widget or reuses an existing one and schedules its dock creation. + Create a new widget (or reuse an instance) and add it as a dock. Args: - widget (BECWidget | str): The widget instance or a string specifying the - type of widget to create. - closable (bool): Whether the dock should be closable. Defaults to True. - floatable (bool): Whether the dock should be floatable. Defaults to True. - movable (bool): Whether the dock should be movable. Defaults to True. - start_floating (bool): Whether to start the dock in a floating state. Defaults to False. - + widget: Widget instance or a string widget type (factory-created). + closable: Whether the dock is closable. + floatable: Whether the dock is floatable. + movable: Whether the dock is movable. + start_floating: Start the dock in a floating state. + where: Preferred area to add the dock: "left" | "right" | "top" | "bottom". + If None, uses the instance default passed at construction time. Returns: - widget: The widget instance. + The widget instance. """ @rpc_call @@ -184,6 +185,20 @@ class AdvancedDockArea(RPCBase): Delete all docks and widgets. """ + @property + @rpc_call + def mode(self) -> "str": + """ + None + """ + + @mode.setter + @rpc_call + def mode(self) -> "str": + """ + None + """ + class AutoUpdates(RPCBase): @property diff --git a/bec_widgets/widgets/containers/advanced_dock_area/advanced_dock_area.py b/bec_widgets/widgets/containers/advanced_dock_area/advanced_dock_area.py index 539ffd24..3cb8d739 100644 --- a/bec_widgets/widgets/containers/advanced_dock_area/advanced_dock_area.py +++ b/bec_widgets/widgets/containers/advanced_dock_area/advanced_dock_area.py @@ -1,11 +1,11 @@ from __future__ import annotations import os -from typing import cast +from typing import cast, Literal import PySide6QtAds as QtAds from PySide6QtAds import CDockManager, CDockWidget -from qtpy.QtCore import Property, QSettings, QSize, Signal +from qtpy.QtCore import QSettings, Signal from qtpy.QtWidgets import ( QApplication, QCheckBox, @@ -202,12 +202,28 @@ class SaveProfileDialog(QDialog): class AdvancedDockArea(BECWidget, QWidget): RPC = True PLUGIN = False - USER_ACCESS = ["new", "widget_map", "widget_list", "lock_workspace", "attach_all", "delete_all"] + USER_ACCESS = [ + "new", + "widget_map", + "widget_list", + "lock_workspace", + "attach_all", + "delete_all", + "mode", + "mode.setter", + ] # Define a signal for mode changes mode_changed = Signal(str) - def __init__(self, parent=None, mode: str = "developer", *args, **kwargs): + def __init__( + self, + parent=None, + mode: str = "developer", + default_add_direction: Literal["left", "right", "top", "bottom"] = "right", + *args, + **kwargs, + ): super().__init__(parent=parent, *args, **kwargs) # Title (as a top-level QWidget it can have a window title) @@ -219,10 +235,10 @@ class AdvancedDockArea(BECWidget, QWidget): self._root_layout.setSpacing(0) # Setting the dock manager with flags - QtAds.CDockManager.setConfigFlag(QtAds.CDockManager.eConfigFlag.FocusHighlighting, True) - QtAds.CDockManager.setConfigFlag( - QtAds.CDockManager.eConfigFlag.RetainTabSizeWhenCloseButtonHidden, True - ) + # QtAds.CDockManager.setConfigFlag(QtAds.CDockManager.eConfigFlag.FocusHighlighting, True) + # QtAds.CDockManager.setConfigFlag( + # QtAds.CDockManager.eConfigFlag.RetainTabSizeWhenCloseButtonHidden, True + # ) self.dock_manager = CDockManager(self) # Dock manager helper variables @@ -230,6 +246,11 @@ class AdvancedDockArea(BECWidget, QWidget): # Initialize mode property first (before toolbar setup) self._mode = "developer" + self._default_add_direction = ( + default_add_direction + if default_add_direction in ("left", "right", "top", "bottom") + else "right" + ) # Toolbar self.dark_mode_button = DarkModeButton(parent=self, toolbar=True) @@ -258,9 +279,6 @@ class AdvancedDockArea(BECWidget, QWidget): # Apply the requested mode after everything is set up self.mode = mode - def minimumSizeHint(self): - return QSize(1200, 800) - def _make_dock( self, widget: QWidget, @@ -332,6 +350,19 @@ class AdvancedDockArea(BECWidget, QWidget): dock.closeDockWidget() dock.deleteDockWidget() + def _area_from_where(self, where: str | None) -> QtAds.DockWidgetArea: + """Return ADS DockWidgetArea from a human-friendly direction string. + If *where* is None, fall back to instance default. + """ + d = (where or getattr(self, "_default_add_direction", "right") or "right").lower() + mapping = { + "left": QtAds.DockWidgetArea.LeftDockWidgetArea, + "right": QtAds.DockWidgetArea.RightDockWidgetArea, + "top": QtAds.DockWidgetArea.TopDockWidgetArea, + "bottom": QtAds.DockWidgetArea.BottomDockWidgetArea, + } + return mapping.get(d, QtAds.DockWidgetArea.RightDockWidgetArea) + ################################################################################ # Toolbar Setup ################################################################################ @@ -553,22 +584,25 @@ class AdvancedDockArea(BECWidget, QWidget): floatable: bool = True, movable: bool = True, start_floating: bool = False, + where: Literal["left", "right", "top", "bottom"] | None = None, ) -> BECWidget: """ - Creates a new widget or reuses an existing one and schedules its dock creation. + Create a new widget (or reuse an instance) and add it as a dock. Args: - widget (BECWidget | str): The widget instance or a string specifying the - type of widget to create. - closable (bool): Whether the dock should be closable. Defaults to True. - floatable (bool): Whether the dock should be floatable. Defaults to True. - movable (bool): Whether the dock should be movable. Defaults to True. - start_floating (bool): Whether to start the dock in a floating state. Defaults to False. - + widget: Widget instance or a string widget type (factory-created). + closable: Whether the dock is closable. + floatable: Whether the dock is floatable. + movable: Whether the dock is movable. + start_floating: Start the dock in a floating state. + where: Preferred area to add the dock: "left" | "right" | "top" | "bottom". + If None, uses the instance default passed at construction time. Returns: - widget: The widget instance. + The widget instance. """ - # 1) Instantiate or look up the widget (this schedules the BECConnector naming logic) + target_area = self._area_from_where(where) + + # 1) Instantiate or look up the widget if isinstance(widget, str): widget = cast(BECWidget, widget_handler.create_widget(widget_type=widget, parent=self)) widget.name_established.connect( @@ -578,8 +612,20 @@ class AdvancedDockArea(BECWidget, QWidget): floatable=floatable, movable=movable, start_floating=start_floating, + area=target_area, ) ) + return widget + + # If a widget instance is passed, dock it immediately + self._create_dock_with_name( + widget=widget, + closable=closable, + floatable=floatable, + movable=movable, + start_floating=start_floating, + area=target_area, + ) return widget def _create_dock_with_name( @@ -589,13 +635,15 @@ class AdvancedDockArea(BECWidget, QWidget): floatable: bool = False, movable: bool = True, start_floating: bool = False, + area: QtAds.DockWidgetArea | None = None, ): + target_area = area or self._area_from_where(None) self._make_dock( widget, closable=closable, floatable=floatable, movable=movable, - area=QtAds.DockWidgetArea.RightDockWidgetArea, + area=target_area, start_floating=start_floating, ) self.dock_manager.setFocus() @@ -906,21 +954,6 @@ class AdvancedDockArea(BECWidget, QWidget): # Fallback to user mode self.toolbar.show_bundles(["spacer_bundle", "workspace", "dock_actions"]) - def switch_to_plot_mode(self): - self.mode = "plot" - - def switch_to_device_mode(self): - self.mode = "device" - - def switch_to_utils_mode(self): - self.mode = "utils" - - def switch_to_developer_mode(self): - self.mode = "developer" - - def switch_to_user_mode(self): - self.mode = "user" - def cleanup(self): """ Cleanup the dock area. @@ -938,7 +971,7 @@ if __name__ == "__main__": app = QApplication(sys.argv) dispatcher = BECDispatcher(gui_id="ads") window = BECMainWindowNoRPC() - ads = AdvancedDockArea(parent=window, mode="developer") + ads = AdvancedDockArea(mode="developer", root_widget=True) window.setCentralWidget(ads) window.show() window.resize(800, 600) diff --git a/tests/unit_tests/test_advanced_dock_area.py b/tests/unit_tests/test_advanced_dock_area.py index 2207a31e..6f5b25e8 100644 --- a/tests/unit_tests/test_advanced_dock_area.py +++ b/tests/unit_tests/test_advanced_dock_area.py @@ -55,11 +55,6 @@ class TestAdvancedDockAreaInit: assert hasattr(advanced_dock_area, "dark_mode_button") assert hasattr(advanced_dock_area, "state_manager") - def test_minimum_size_hint(self, advanced_dock_area): - size_hint = advanced_dock_area.minimumSizeHint() - assert size_hint.width() == 1200 - assert size_hint.height() == 800 - def test_rpc_and_plugin_flags(self): assert AdvancedDockArea.RPC is True assert AdvancedDockArea.PLUGIN is False @@ -97,7 +92,7 @@ class TestDockManagement: assert widget is not None assert hasattr(widget, "name_established") - def test_new_widget_instance(self, advanced_dock_area): + def test_new_widget_instance(self, advanced_dock_area, qtbot): """Test creating dock with existing widget instance.""" from bec_widgets.widgets.plots.waveform.waveform import Waveform @@ -113,8 +108,9 @@ class TestDockManagement: # Should return the same instance assert result == widget_instance - # No new dock created since we passed an instance, not a string - assert len(advanced_dock_area.dock_list()) == initial_count + qtbot.wait(200) + + assert len(advanced_dock_area.dock_list()) == initial_count + 1 def test_dock_map(self, advanced_dock_area, qtbot): """Test dock_map returns correct mapping.""" @@ -784,31 +780,6 @@ class TestModeSwitching: # Check signal was emitted with correct argument assert blocker.args == ["plot"] - def test_switch_to_plot_mode(self, advanced_dock_area): - """Test switch_to_plot_mode method.""" - advanced_dock_area.switch_to_plot_mode() - assert advanced_dock_area.mode == "plot" - - def test_switch_to_device_mode(self, advanced_dock_area): - """Test switch_to_device_mode method.""" - advanced_dock_area.switch_to_device_mode() - assert advanced_dock_area.mode == "device" - - def test_switch_to_utils_mode(self, advanced_dock_area): - """Test switch_to_utils_mode method.""" - advanced_dock_area.switch_to_utils_mode() - assert advanced_dock_area.mode == "utils" - - def test_switch_to_developer_mode(self, advanced_dock_area): - """Test switch_to_developer_mode method.""" - advanced_dock_area.switch_to_developer_mode() - assert advanced_dock_area.mode == "developer" - - def test_switch_to_user_mode(self, advanced_dock_area): - """Test switch_to_user_mode method.""" - advanced_dock_area.switch_to_user_mode() - assert advanced_dock_area.mode == "user" - class TestToolbarModeBundles: """Test toolbar bundle creation and visibility for different modes."""