From 8f44213ecccca882f22b8738baef28b68d99c381 Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Tue, 13 Jan 2026 12:05:55 +0100 Subject: [PATCH] fix(advanced_dock_area): remove widget from dock area by object name --- bec_widgets/cli/client.py | 66 +++++++++++++++++++ .../advanced_dock_area/advanced_dock_area.py | 1 + .../advanced_dock_area/basic_dock_area.py | 26 ++++++++ tests/unit_tests/test_advanced_dock_area.py | 28 ++++++++ 4 files changed, 121 insertions(+) diff --git a/bec_widgets/cli/client.py b/bec_widgets/cli/client.py index 8367674c..0320c613 100644 --- a/bec_widgets/cli/client.py +++ b/bec_widgets/cli/client.py @@ -113,6 +113,7 @@ class AdvancedDockArea(RPCBase): title_buttons: "Mapping[str, bool] | Sequence[str] | str | None" = None, show_settings_action: "bool | None" = None, promote_central: "bool" = False, + object_name: "str | None" = None, **widget_kwargs, ) -> "QWidget | CDockWidget | BECWidget": """ @@ -155,6 +156,25 @@ class AdvancedDockArea(RPCBase): Delete all docks and their associated widgets. """ + @rpc_call + def remove_widget(self, object_name: "str") -> "bool": + """ + Remove a widget from the dock area by its object name. + + Args: + object_name: The object name of the widget to remove. + + Returns: + bool: True if the widget was found and removed, False otherwise. + + Raises: + ValueError: If no widget with the given object name is found. + + Example: + >>> dock_area.remove_widget("my_widget") + True + """ + @rpc_call def set_layout_ratios( self, @@ -1058,6 +1078,7 @@ class DockAreaWidget(RPCBase): promote_central: "bool" = False, dock_icon: "QIcon | None" = None, apply_widget_icon: "bool" = True, + object_name: "str | None" = None, **widget_kwargs, ) -> "QWidget | CDockWidget | BECWidget": """ @@ -1093,6 +1114,9 @@ class DockAreaWidget(RPCBase): the widget's ``ICON_NAME`` attribute is used when available. apply_widget_icon(bool): When False, skip automatically resolving the icon from the widget's ``ICON_NAME`` (useful for callers who want no icon and do not pass one explicitly). + object_name(str | None): Optional object name to assign to the created widget. + **widget_kwargs: Additional keyword arguments passed to the widget constructor + when creating by type name. Returns: The widget instance by default, or the created `CDockWidget` when `return_dock` is True. @@ -1134,6 +1158,25 @@ class DockAreaWidget(RPCBase): Delete all docks and their associated widgets. """ + @rpc_call + def remove_widget(self, object_name: "str") -> "bool": + """ + Remove a widget from the dock area by its object name. + + Args: + object_name: The object name of the widget to remove. + + Returns: + bool: True if the widget was found and removed, False otherwise. + + Raises: + ValueError: If no widget with the given object name is found. + + Example: + >>> dock_area.remove_widget("my_widget") + True + """ + @rpc_call def set_layout_ratios( self, @@ -2723,6 +2766,7 @@ class MonacoDock(RPCBase): promote_central: "bool" = False, dock_icon: "QIcon | None" = None, apply_widget_icon: "bool" = True, + object_name: "str | None" = None, **widget_kwargs, ) -> "QWidget | CDockWidget | BECWidget": """ @@ -2758,6 +2802,9 @@ class MonacoDock(RPCBase): the widget's ``ICON_NAME`` attribute is used when available. apply_widget_icon(bool): When False, skip automatically resolving the icon from the widget's ``ICON_NAME`` (useful for callers who want no icon and do not pass one explicitly). + object_name(str | None): Optional object name to assign to the created widget. + **widget_kwargs: Additional keyword arguments passed to the widget constructor + when creating by type name. Returns: The widget instance by default, or the created `CDockWidget` when `return_dock` is True. @@ -2799,6 +2846,25 @@ class MonacoDock(RPCBase): Delete all docks and their associated widgets. """ + @rpc_call + def remove_widget(self, object_name: "str") -> "bool": + """ + Remove a widget from the dock area by its object name. + + Args: + object_name: The object name of the widget to remove. + + Returns: + bool: True if the widget was found and removed, False otherwise. + + Raises: + ValueError: If no widget with the given object name is found. + + Example: + >>> dock_area.remove_widget("my_widget") + True + """ + @rpc_call def set_layout_ratios( self, 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 83f91e24..cfc06da0 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 @@ -99,6 +99,7 @@ class AdvancedDockArea(DockAreaWidget): "lock_workspace", "attach_all", "delete_all", + "remove_widget", "set_layout_ratios", "describe_layout", "print_layout_structure", diff --git a/bec_widgets/widgets/containers/advanced_dock_area/basic_dock_area.py b/bec_widgets/widgets/containers/advanced_dock_area/basic_dock_area.py index 1c9c5ed3..5822f501 100644 --- a/bec_widgets/widgets/containers/advanced_dock_area/basic_dock_area.py +++ b/bec_widgets/widgets/containers/advanced_dock_area/basic_dock_area.py @@ -54,6 +54,7 @@ class DockAreaWidget(BECWidget, QWidget): "widget_list", "attach_all", "delete_all", + "remove_widget", "set_layout_ratios", "describe_layout", "print_layout_structure", @@ -1375,6 +1376,31 @@ class DockAreaWidget(BECWidget, QWidget): QtAds.DockWidgetArea.RightDockWidgetArea, dock, target ) + @SafeSlot(str) + def remove_widget(self, object_name: str) -> bool: + """ + Remove a widget from the dock area by its object name. + + Args: + object_name: The object name of the widget to remove. + + Returns: + bool: True if the widget was found and removed, False otherwise. + + Raises: + ValueError: If no widget with the given object name is found. + + Example: + >>> dock_area.remove_widget("my_widget") + True + """ + dock_map = self.dock_map() + dock = dock_map.get(object_name) + if dock is None: + raise ValueError(f"No widget found with object name '{object_name}'.") + self._delete_dock(dock) + return True + @SafeSlot() def delete_all(self): """Delete all docks and their associated widgets.""" diff --git a/tests/unit_tests/test_advanced_dock_area.py b/tests/unit_tests/test_advanced_dock_area.py index f108d39b..d18a5a8a 100644 --- a/tests/unit_tests/test_advanced_dock_area.py +++ b/tests/unit_tests/test_advanced_dock_area.py @@ -250,6 +250,34 @@ class TestBasicDockArea: basic_dock_area.delete_all() assert basic_dock_area.dock_list() == [] + def test_remove_widget_by_object_name(self, basic_dock_area, qtbot): + """Test remove_widget removes widget by object name.""" + panel_one = QWidget(parent=basic_dock_area) + panel_one.setObjectName("panel_one") + panel_two = QWidget(parent=basic_dock_area) + panel_two.setObjectName("panel_two") + + basic_dock_area.new(panel_one, return_dock=True) + basic_dock_area.new(panel_two, return_dock=True) + + assert len(basic_dock_area.dock_list()) == 2 + assert "panel_one" in basic_dock_area.dock_map() + assert "panel_two" in basic_dock_area.dock_map() + + # Remove panel_one + result = basic_dock_area.remove_widget("panel_one") + qtbot.wait(100) + + assert result is True + assert len(basic_dock_area.dock_list()) == 1 + assert "panel_one" not in basic_dock_area.dock_map() + assert "panel_two" in basic_dock_area.dock_map() + + def test_remove_widget_raises_for_unknown_name(self, basic_dock_area): + """Test remove_widget raises ValueError for non-existent widget.""" + with pytest.raises(ValueError, match="No widget found with object name 'nonexistent'"): + basic_dock_area.remove_widget("nonexistent") + def test_manifest_serialization_includes_floating_geometry( self, basic_dock_area, qtbot, tmp_path ):