1
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2026-05-05 06:16:32 +02:00

feat: add plugin widgets menu to BECDockArea toolbar

Agent-Logs-Url: https://github.com/bec-project/bec_widgets/sessions/872a29f8-acdf-42e5-94b9-bad871aef4a0

Co-authored-by: wyzula-jan <133381102+wyzula-jan@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-04-30 08:26:41 +00:00
committed by GitHub
parent 2d2647c20b
commit 9b070f2031
2 changed files with 108 additions and 3 deletions
@@ -21,6 +21,7 @@ import bec_widgets.widgets.containers.qt_ads as QtAds
from bec_widgets import BECWidget, SafeProperty, SafeSlot
from bec_widgets.applications.views.view import ViewTourSteps
from bec_widgets.utils.bec_dispatcher import BECDispatcher
from bec_widgets.utils.bec_plugin_helper import get_all_plugin_widgets
from bec_widgets.utils.colors import apply_theme
from bec_widgets.utils.rpc_decorator import rpc_timeout
from bec_widgets.utils.rpc_widget_handler import widget_handler
@@ -398,6 +399,19 @@ class BECDockArea(DockAreaWidget):
_build_menu("menu_devices", "Add Device Control ", device_actions)
_build_menu("menu_utils", "Add Utils ", util_actions)
# Build plugin widget menu (only shown when plugin widgets are available)
plugin_widgets_dict = get_all_plugin_widgets().as_dict()
plugin_actions: dict[str, tuple[str, str, str]] = {
widget_name: (
getattr(widget_cls, "ICON_NAME", "widgets"),
f"Add {widget_name}",
widget_name,
)
for widget_name, widget_cls in plugin_widgets_dict.items()
}
if plugin_actions:
_build_menu("menu_plugins", "Add Plugins ", plugin_actions)
# Create flat toolbar bundles for each widget type
def _build_flat_bundles(category: str, mapping: dict[str, tuple[str, str, str]]):
bundle = ToolbarBundle(f"flat_{category}", self.toolbar.components)
@@ -468,14 +482,16 @@ class BECDockArea(DockAreaWidget):
bda.add_action("dark_mode")
self.toolbar.add_bundle(bda)
self._apply_toolbar_layout()
# Store mappings on self for use in _hook_toolbar
# Store mappings on self for use in _hook_toolbar and _apply_toolbar_layout
self._ACTION_MAPPINGS = {
"menu_plots": plot_actions,
"menu_devices": device_actions,
"menu_utils": util_actions,
}
if plugin_actions:
self._ACTION_MAPPINGS["menu_plugins"] = plugin_actions
self._apply_toolbar_layout()
def _hook_toolbar(self):
def _connect_menu(menu_key: str):
@@ -501,6 +517,8 @@ class BECDockArea(DockAreaWidget):
_connect_menu("menu_plots")
_connect_menu("menu_devices")
_connect_menu("menu_utils")
if "menu_plugins" in self._ACTION_MAPPINGS:
_connect_menu("menu_plugins")
def _connect_flat_actions(mapping: dict[str, tuple[str, str, str]]):
for action_id, (_, _, widget_type) in mapping.items():
@@ -1109,6 +1127,10 @@ class BECDockArea(DockAreaWidget):
"menu_plots",
"menu_devices",
"menu_utils",
]
if "menu_plugins" in getattr(self, "_ACTION_MAPPINGS", {}):
bundles.append("menu_plugins")
bundles += [
"spacer_bundle",
"workspace",
"dock_actions",
+83
View File
@@ -1024,6 +1024,89 @@ class TestToolbarFunctionality:
# Verify save was called with the filename
mock_screenshot.save.assert_called_once_with(str(screenshot_path))
def test_plugin_menu_not_shown_when_no_plugins(self, qtbot, mocked_client):
"""Test that the plugin menu is not shown when there are no plugin widgets."""
with patch(
"bec_widgets.widgets.containers.dock_area.dock_area.get_all_plugin_widgets"
) as mock_plugins:
from bec_widgets.utils.plugin_utils import BECClassContainer
mock_plugins.return_value = BECClassContainer()
widget = BECDockArea(client=mocked_client)
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
assert "menu_plugins" not in widget._ACTION_MAPPINGS
# Verify no "menu_plugins" bundle exists in the toolbar
assert "menu_plugins" not in widget.toolbar.bundles
def test_plugin_menu_shown_when_plugins_available(self, qtbot, mocked_client):
"""Test that the plugin menu is shown when plugin widgets are available."""
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.plugin_utils import BECClassContainer, BECClassInfo
from qtpy.QtWidgets import QWidget as _QWidget
class FakePluginWidget(BECWidget, _QWidget):
ICON_NAME = "star"
PLUGIN = True
def __init__(self, parent=None, **kwargs):
super().__init__(parent=parent, **kwargs)
container = BECClassContainer(
[BECClassInfo(name="FakePluginWidget", module="fake", file="fake.py", obj=FakePluginWidget)]
)
with patch(
"bec_widgets.widgets.containers.dock_area.dock_area.get_all_plugin_widgets"
) as mock_plugins:
mock_plugins.return_value = container
# Make as_dict() return our fake widget
container_mock = MagicMock()
container_mock.as_dict.return_value = {"FakePluginWidget": FakePluginWidget}
mock_plugins.return_value = container_mock
widget = BECDockArea(client=mocked_client)
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
assert "menu_plugins" in widget._ACTION_MAPPINGS
assert "FakePluginWidget" in widget._ACTION_MAPPINGS["menu_plugins"]
plugin_entry = widget._ACTION_MAPPINGS["menu_plugins"]["FakePluginWidget"]
assert plugin_entry[0] == "star" # icon name
assert plugin_entry[1] == "Add FakePluginWidget" # tooltip
assert plugin_entry[2] == "FakePluginWidget" # widget type name
assert "menu_plugins" in widget.toolbar.bundles
def test_plugin_menu_action_triggers_new(self, qtbot, mocked_client):
"""Test that clicking a plugin menu action creates a new dock with the plugin widget."""
from bec_widgets.utils.bec_widget import BECWidget
from qtpy.QtWidgets import QWidget as _QWidget
class FakePluginWidget(BECWidget, _QWidget):
ICON_NAME = "star"
PLUGIN = True
def __init__(self, parent=None, **kwargs):
super().__init__(parent=parent, **kwargs)
with patch(
"bec_widgets.widgets.containers.dock_area.dock_area.get_all_plugin_widgets"
) as mock_plugins:
container_mock = MagicMock()
container_mock.as_dict.return_value = {"FakePluginWidget": FakePluginWidget}
mock_plugins.return_value = container_mock
widget = BECDockArea(client=mocked_client)
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
with patch.object(widget, "new") as mock_new:
menu_plugins = widget.toolbar.components.get_action("menu_plugins")
action = menu_plugins.actions["FakePluginWidget"].action
action.trigger()
mock_new.assert_called_once_with(widget="FakePluginWidget")
class TestDockSettingsDialog:
"""Test dock settings dialog functionality."""