1
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2026-03-04 16:02:51 +01:00

fix(view): based on BECWidgets

This commit is contained in:
2026-02-17 13:21:28 +01:00
committed by Jan Wyzula
parent 91f2a442a3
commit 1493d8e99d
7 changed files with 136 additions and 39 deletions

View File

@@ -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
"""

View File

@@ -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()

View File

@@ -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",
)

View File

@@ -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"
)

View File

@@ -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.

View File

@@ -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"""

View File

@@ -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