From 1493d8e99d09599d4186f975daeab9cee494c61a Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Tue, 17 Feb 2026 13:21:28 +0100 Subject: [PATCH] fix(view): based on BECWidgets --- bec_widgets/applications/main_app.py | 41 +++++++++-------- .../views/developer_view/developer_view.py | 11 +++-- .../device_manager_view.py | 7 +-- .../views/dock_area_view/dock_area_view.py | 5 +- bec_widgets/applications/views/view.py | 36 +++++++++++++-- bec_widgets/cli/client.py | 46 +++++++++++++++++++ tests/unit_tests/test_main_app.py | 29 +++++++++--- 7 files changed, 136 insertions(+), 39 deletions(-) diff --git a/bec_widgets/applications/main_app.py b/bec_widgets/applications/main_app.py index 064b076d..b37322ef 100644 --- a/bec_widgets/applications/main_app.py +++ b/bec_widgets/applications/main_app.py @@ -10,6 +10,7 @@ from bec_widgets.applications.views.device_manager_view.device_manager_view impo from bec_widgets.applications.views.dock_area_view.dock_area_view import DockAreaView from bec_widgets.applications.views.view import ViewBase, WaveformViewInline, WaveformViewPopup from bec_widgets.utils.colors import apply_theme +from bec_widgets.utils.name_utils import sanitize_namespace from bec_widgets.utils.guided_tour import GuidedTour from bec_widgets.utils.screen_utils import ( apply_centered_size, @@ -63,17 +64,10 @@ class BECMainApp(BECMainWindow): self.device_manager = DeviceManagerView(self) self.developer_view = DeveloperView(self) - self.add_view( - icon="widgets", - title="Dock Area", - id="dock_area", - widget=self.dock_area, - mini_text="Docks", - ) + self.add_view(icon="widgets", title="Dock Area", widget=self.dock_area, mini_text="Docks") self.add_view( icon="display_settings", title="Device Manager", - id="device_manager", widget=self.device_manager, mini_text="DM", ) @@ -81,30 +75,28 @@ class BECMainApp(BECMainWindow): icon="code_blocks", title="IDE", widget=self.developer_view, - id="developer_view", + mini_text="IDE", exclusive=True, ) if self._show_examples: self.add_section("Examples", "examples") waveform_view_popup = WaveformViewPopup( - parent=self, id="waveform_view_popup", title="Waveform Plot" + parent=self, view_id="waveform_view_popup", title="Waveform Plot" ) waveform_view_stack = WaveformViewInline( - parent=self, id="waveform_view_stack", title="Waveform Plot" + parent=self, view_id="waveform_view_stack", title="Waveform Plot" ) self.add_view( icon="show_chart", title="Waveform With Popup", - id="waveform_popup", widget=waveform_view_popup, mini_text="Popup", ) self.add_view( icon="show_chart", title="Waveform InLine Stack", - id="waveform_stack", widget=waveform_view_stack, mini_text="Stack", ) @@ -130,7 +122,7 @@ class BECMainApp(BECMainWindow): *, icon: str, title: str, - id: str, + view_id: str | None = None, widget: QWidget, mini_text: str | None = None, position: int | None = None, @@ -144,7 +136,8 @@ class BECMainApp(BECMainWindow): Args: icon(str): Icon name for the nav item. title(str): Title for the nav item. - id(str): Unique ID for the view/item. + view_id(str, optional): Unique ID for the view/item. If omitted, uses mini_text; + if mini_text is also omitted, uses title. widget(QWidget): The widget to add to the stack. mini_text(str, optional): Short text for the nav item when sidebar is collapsed. position(int, optional): Position to insert the nav item. @@ -157,10 +150,11 @@ class BECMainApp(BECMainWindow): """ + resolved_id = sanitize_namespace(view_id or mini_text or title) item = self.sidebar.add_item( icon=icon, title=title, - id=id, + id=resolved_id, mini_text=mini_text, position=position, from_top=from_top, @@ -170,13 +164,15 @@ class BECMainApp(BECMainWindow): # Wrap plain widgets into a ViewBase so enter/exit hooks are available if isinstance(widget, ViewBase): view_widget = widget - view_widget.view_id = id + view_widget.view_id = resolved_id view_widget.view_title = title else: - view_widget = ViewBase(content=widget, parent=self, id=id, title=title) + view_widget = ViewBase(content=widget, parent=self, view_id=resolved_id, title=title) + + view_widget.change_object_name(resolved_id) idx = self.stack.addWidget(view_widget) - self._view_index[id] = idx + self._view_index[resolved_id] = idx return item def set_current(self, id: str) -> None: @@ -357,6 +353,13 @@ class BECMainApp(BECMainWindow): tour_action.setShortcut("F1") # Add keyboard shortcut help_menu.addAction(tour_action) + def cleanup(self): + for view_id, idx in self._view_index.items(): + view = self.stack.widget(idx) + view.close() + view.deleteLater() + super().cleanup() + def main(): # pragma: no cover """ diff --git a/bec_widgets/applications/views/developer_view/developer_view.py b/bec_widgets/applications/views/developer_view/developer_view.py index 9a135dbb..38b27d43 100644 --- a/bec_widgets/applications/views/developer_view/developer_view.py +++ b/bec_widgets/applications/views/developer_view/developer_view.py @@ -14,10 +14,11 @@ class DeveloperView(ViewBase): parent: QWidget | None = None, content: QWidget | None = None, *, - id: str | None = None, + view_id: str | None = None, title: str | None = None, + **kwargs, ): - super().__init__(parent=parent, content=content, id=id, title=title) + super().__init__(parent=parent, content=content, view_id=view_id, title=title, **kwargs) self.developer_widget = DeveloperWidget(parent=self) self.set_content(self.developer_widget) @@ -125,7 +126,11 @@ if __name__ == "__main__": _app.resize(width, height) developer_view = DeveloperView() _app.add_view( - icon="code_blocks", title="IDE", widget=developer_view, id="developer_view", exclusive=True + icon="code_blocks", + title="IDE", + widget=developer_view, + view_id="developer_view", + exclusive=True, ) _app.show() # developer_view.show() diff --git a/bec_widgets/applications/views/device_manager_view/device_manager_view.py b/bec_widgets/applications/views/device_manager_view/device_manager_view.py index d8a71501..84fc0623 100644 --- a/bec_widgets/applications/views/device_manager_view/device_manager_view.py +++ b/bec_widgets/applications/views/device_manager_view/device_manager_view.py @@ -20,10 +20,11 @@ class DeviceManagerView(ViewBase): parent: QWidget | None = None, content: QWidget | None = None, *, - id: str | None = None, + view_id: str | None = None, title: str | None = None, + **kwargs, ): - super().__init__(parent=parent, content=content, id=id, title=title) + super().__init__(parent=parent, content=content, view_id=view_id, title=title, **kwargs) self.device_manager_widget = DeviceManagerWidget(parent=self) self.set_content(self.device_manager_widget) @@ -170,7 +171,7 @@ if __name__ == "__main__": # pragma: no cover _app.add_view( icon="display_settings", title="Device Manager", - id="device_manager", + view_id="device_manager", widget=device_manager_view.device_manager_widget, mini_text="DM", ) diff --git a/bec_widgets/applications/views/dock_area_view/dock_area_view.py b/bec_widgets/applications/views/dock_area_view/dock_area_view.py index 322a6073..bf08da51 100644 --- a/bec_widgets/applications/views/dock_area_view/dock_area_view.py +++ b/bec_widgets/applications/views/dock_area_view/dock_area_view.py @@ -14,10 +14,11 @@ class DockAreaView(ViewBase): parent: QWidget | None = None, content: QWidget | None = None, *, - id: str | None = None, + view_id: str | None = None, title: str | None = None, + **kwargs, ): - super().__init__(parent=parent, content=content, id=id, title=title) + super().__init__(parent=parent, content=content, view_id=view_id, title=title, **kwargs) self.dock_area = BECDockArea( self, profile_namespace="bec", auto_profile_namespace=False, object_name="DockArea" ) diff --git a/bec_widgets/applications/views/view.py b/bec_widgets/applications/views/view.py index be5823a6..446fd58c 100644 --- a/bec_widgets/applications/views/view.py +++ b/bec_widgets/applications/views/view.py @@ -17,6 +17,7 @@ from qtpy.QtWidgets import ( QWidget, ) +from bec_widgets import BECWidget from bec_widgets.utils.error_popups import SafeSlot from bec_widgets.widgets.control.device_input.device_combobox.device_combobox import DeviceComboBox from bec_widgets.widgets.control.device_input.signal_combobox.signal_combobox import SignalComboBox @@ -35,7 +36,7 @@ class ViewTourSteps(BaseModel): step_ids: List[str] -class ViewBase(QWidget): +class ViewBase(BECWidget, QWidget): """Wrapper for a content widget used inside the main app's stacked view. Subclasses can implement `on_enter` and `on_exit` to run custom logic when the view becomes visible or is about to be hidden. @@ -43,21 +44,26 @@ class ViewBase(QWidget): Args: content (QWidget): The actual view widget to display. parent (QWidget | None): Parent widget. - id (str | None): Optional view id, useful for debugging or introspection. + view_id (str | None): Optional view view_id, useful for debugging or introspection. title (str | None): Optional human-readable title. """ + RPC = True + PLUGIN = False + USER_ACCESS = ["activate"] + def __init__( self, parent: QWidget | None = None, content: QWidget | None = None, *, - id: str | None = None, + view_id: str | None = None, title: str | None = None, + **kwargs, ): - super().__init__(parent=parent) + super().__init__(parent=parent, **kwargs) self.content: QWidget | None = None - self.view_id = id + self.view_id = view_id self.view_title = title lay = QVBoxLayout(self) @@ -91,6 +97,26 @@ class ViewBase(QWidget): """ return True + @SafeSlot() + def activate(self) -> None: + """Switch the parent application to this view.""" + if not self.view_id: + raise ValueError("Cannot switch view without a view_id.") + + parent = self.parent() + while parent is not None: + if hasattr(parent, "set_current"): + parent.set_current(self.view_id) + return + parent = parent.parent() + raise RuntimeError("Could not find a parent application with set_current().") + + def cleanup(self): + if self.content is not None: + self.content.close() + self.content.deleteLater() + super().cleanup() + def register_tour_steps(self, guided_tour, main_app) -> ViewTourSteps | None: """Register this view's components with the guided tour. diff --git a/bec_widgets/cli/client.py b/bec_widgets/cli/client.py index 33d17dbc..d5608f3d 100644 --- a/bec_widgets/cli/client.py +++ b/bec_widgets/cli/client.py @@ -1072,6 +1072,26 @@ class DeviceInputBase(RPCBase): """ +class DeviceManagerView(RPCBase): + """A view for users to manage devices within the application.""" + + @rpc_call + def activate(self) -> "None": + """ + Switch the parent application to this view. + """ + + +class DockAreaView(RPCBase): + """Modular dock area view for arranging and managing multiple dockable widgets.""" + + @rpc_call + def activate(self) -> "None": + """ + Switch the parent application to this view. + """ + + class DockAreaWidget(RPCBase): """Lightweight dock area that exposes the core Qt ADS docking helpers without any""" @@ -5539,6 +5559,16 @@ class TextBox(RPCBase): """ +class ViewBase(RPCBase): + """Wrapper for a content widget used inside the main app's stacked view.""" + + @rpc_call + def activate(self) -> "None": + """ + Switch the parent application to this view. + """ + + class Waveform(RPCBase): """Widget for plotting waveforms.""" @@ -6110,6 +6140,22 @@ class Waveform(RPCBase): """ +class WaveformViewInline(RPCBase): + @rpc_call + def activate(self) -> "None": + """ + Switch the parent application to this view. + """ + + +class WaveformViewPopup(RPCBase): + @rpc_call + def activate(self) -> "None": + """ + Switch the parent application to this view. + """ + + class WebConsole(RPCBase): """A simple widget to display a website""" diff --git a/tests/unit_tests/test_main_app.py b/tests/unit_tests/test_main_app.py index 1fa0c503..ec26507e 100644 --- a/tests/unit_tests/test_main_app.py +++ b/tests/unit_tests/test_main_app.py @@ -54,16 +54,16 @@ def app_with_spies(qtbot, mocked_client): app.add_section("Tests", id="tests") - v1 = SpyView(id="v1", title="V1") - v2 = SpyView(id="v2", title="V2") - vv = SpyVetoView(id="vv", title="VV") + v1 = SpyView(view_id="v1", title="V1") + v2 = SpyView(view_id="v2", title="V2") + vv = SpyVetoView(view_id="vv", title="VV") - app.add_view(icon="widgets", title="View 1", id="v1", widget=v1, mini_text="v1") - app.add_view(icon="widgets", title="View 2", id="v2", widget=v2, mini_text="v2") - app.add_view(icon="widgets", title="Veto View", id="vv", widget=vv, mini_text="vv") + app.add_view(icon="widgets", title="View 1", view_id="v1", widget=v1, mini_text="v1") + app.add_view(icon="widgets", title="View 2", view_id="v2", widget=v2, mini_text="v2") + app.add_view(icon="widgets", title="Veto View", view_id="vv", widget=vv, mini_text="vv") # Start from dock_area (default) to avoid extra enter/exit counts on spies - assert app.stack.currentIndex() == app._view_index["dock_area"] + assert app.stack.currentIndex() == app._view_index["Docks"] return app, v1, v2, vv @@ -115,6 +115,21 @@ def test_on_exit_veto_prevents_switch_until_allowed(app_with_spies, qtbot): assert v1.enter_calls >= 1 +def test_added_view_gets_short_object_name(app_with_spies): + app, v1, _, _ = app_with_spies + assert v1.object_name == "v1" + assert app._view_index["v1"] >= 0 + + +def test_view_switch_method_switches_to_target(app_with_spies, qtbot): + app, v1, _, _ = app_with_spies + app.set_current("dock_area") + qtbot.wait(10) + v1.activate() + qtbot.wait(10) + assert app.stack.currentIndex() == app._view_index["v1"] + + def test_guided_tour_is_initialized(app_with_spies): """Test that the guided tour is initialized in the main app.""" app, _, _, _ = app_with_spies