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:
@@ -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
|
||||
"""
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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"""
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user