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

fix(dock_area): the old BECDockArea(pg) removed and replaces by AdvancedDockArea(ADS)

This commit is contained in:
2025-12-11 18:42:11 +01:00
committed by Jan Wyzula
parent 2132ace01b
commit 24cc8c7b98
28 changed files with 264 additions and 1979 deletions

View File

@@ -1,12 +1,31 @@
from __future__ import annotations
from bec_lib import bec_logger
from bec_widgets.widgets.containers.advanced_dock_area.advanced_dock_area import AdvancedDockArea
from bec_widgets.widgets.containers.auto_update.auto_updates import AutoUpdates
from bec_widgets.widgets.containers.dock.dock_area import BECDockArea
logger = bec_logger.logger
def dock_area(object_name: str | None = None) -> BECDockArea:
_dock_area = BECDockArea(object_name=object_name, root_widget=True)
return _dock_area
def dock_area(object_name: str | None = None, profile: str | None = None) -> AdvancedDockArea:
"""
Create an advanced dock area using Qt Advanced Docking System.
Args:
object_name(str): The name of the advanced dock area.
profile(str|None): Optional profile to load; if None the last profile is restored.
Returns:
AdvancedDockArea: The created advanced dock area.
"""
widget = AdvancedDockArea(
object_name=object_name, restore_initial_profile=(profile is None), root_widget=True
)
if profile:
widget.load_profile(profile)
logger.info(f"Created advanced dock area with profile: {profile}")
return widget
def auto_update_dock_area(object_name: str | None = None) -> AutoUpdates:

View File

@@ -29,8 +29,9 @@ from bec_widgets.utils.plugin_utils import get_plugin_auto_updates
from bec_widgets.utils.round_frame import RoundedFrame
from bec_widgets.utils.toolbars.toolbar import ModularToolBar
from bec_widgets.utils.ui_loader import UILoader
from bec_widgets.widgets.containers.advanced_dock_area.advanced_dock_area import AdvancedDockArea
from bec_widgets.widgets.containers.advanced_dock_area.profile_utils import list_profiles
from bec_widgets.widgets.containers.auto_update.auto_updates import AutoUpdates
from bec_widgets.widgets.containers.dock.dock_area import BECDockArea
from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow, BECMainWindowNoRPC
from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton
@@ -211,10 +212,11 @@ class LaunchWindow(BECMainWindow):
name="dock_area",
icon_path=os.path.join(MODULE_PATH, "assets", "app_icons", "bec_widgets_icon.png"),
top_label="Get started",
main_label="BEC Dock Area",
description="Highly flexible and customizable dock area application with modular widgets.",
action_button=lambda: self.launch("dock_area"),
show_selector=False,
main_label="BEC Advanced Dock Area",
description="Flexible application for managing modular widgets and user profiles.",
action_button=self._open_dock_area,
show_selector=True,
selector_items=list_profiles("bec"),
)
self.available_auto_updates: dict[str, type[AutoUpdates]] = (
@@ -347,7 +349,7 @@ class LaunchWindow(BECMainWindow):
from bec_widgets.applications import bw_launch
with RPCRegister.delayed_broadcast() as rpc_register:
existing_dock_areas = rpc_register.get_names_of_rpc_by_class_type(BECDockArea)
existing_dock_areas = rpc_register.get_names_of_rpc_by_class_type(AdvancedDockArea)
if name is not None:
if name in existing_dock_areas:
raise ValueError(
@@ -384,7 +386,7 @@ class LaunchWindow(BECMainWindow):
if launch is None:
raise ValueError(f"Launch script {launch_script} not found.")
result_widget = launch(name)
result_widget = launch(name, **kwargs)
result_widget.resize(result_widget.minimumSizeHint())
# TODO Should we simply use the specified name as title here?
result_widget.window().setWindowTitle(f"BEC - {name}")
@@ -491,6 +493,17 @@ class LaunchWindow(BECMainWindow):
auto_update = None
return self.launch("auto_update", auto_update=auto_update)
def _open_dock_area(self):
"""
Open Advanced Dock Area using the selected profile (if any).
"""
tile = self.tiles.get("dock_area")
if tile is None or tile.selector is None:
profile = None
else:
profile = tile.selector.currentText().strip() or None
return self.launch("dock_area", profile=profile)
def _open_widget(self):
"""
Open a widget from the available widgets.
@@ -584,7 +597,10 @@ class LaunchWindow(BECMainWindow):
if __name__ == "__main__":
import sys
from bec_widgets.utils.colors import apply_theme
app = QApplication(sys.argv)
apply_theme("dark")
launcher = LaunchWindow()
launcher.show()
sys.exit(app.exec())

View File

@@ -27,7 +27,6 @@ class _WidgetsEnumType(str, enum.Enum):
_Widgets = {
"BECDockArea": "BECDockArea",
"BECMainWindow": "BECMainWindow",
"BECProgressBar": "BECProgressBar",
"BECQueue": "BECQueue",
@@ -272,298 +271,6 @@ class AvailableDeviceResources(RPCBase):
"""
class BECDock(RPCBase):
@property
@rpc_call
def _config_dict(self) -> "dict":
"""
Get the configuration of the widget.
Returns:
dict: The configuration of the widget.
"""
@property
@rpc_call
def element_list(self) -> "list[BECWidget]":
"""
Get the widgets in the dock.
Returns:
widgets(list): The widgets in the dock.
"""
@property
@rpc_call
def elements(self) -> "dict[str, BECWidget]":
"""
Get the widgets in the dock.
Returns:
widgets(dict): The widgets in the dock.
"""
@rpc_call
def new(
self,
widget: "BECWidget | str",
name: "str | None" = None,
row: "int | None" = None,
col: "int" = 0,
rowspan: "int" = 1,
colspan: "int" = 1,
shift: "Literal['down', 'up', 'left', 'right']" = "down",
) -> "BECWidget":
"""
Add a widget to the dock.
Args:
widget(QWidget): The widget to add. It can not be BECDock or BECDockArea.
name(str): The name of the widget.
row(int): The row to add the widget to. If None, the widget will be added to the next available row.
col(int): The column to add the widget to.
rowspan(int): The number of rows the widget should span.
colspan(int): The number of columns the widget should span.
shift(Literal["down", "up", "left", "right"]): The direction to shift the widgets if the position is occupied.
"""
@rpc_call
def show(self):
"""
Show the dock.
"""
@rpc_call
def hide(self):
"""
Hide the dock.
"""
@rpc_call
def show_title_bar(self):
"""
Hide the title bar of the dock.
"""
@rpc_call
def set_title(self, title: "str"):
"""
Set the title of the dock.
Args:
title(str): The title of the dock.
"""
@rpc_call
def hide_title_bar(self):
"""
Hide the title bar of the dock.
"""
@rpc_call
def available_widgets(self) -> "list":
"""
List all widgets that can be added to the dock.
Returns:
list: The list of eligible widgets.
"""
@rpc_call
def delete(self, widget_name: "str") -> "None":
"""
Remove a widget from the dock.
Args:
widget_name(str): Delete the widget with the given name.
"""
@rpc_call
def delete_all(self):
"""
Remove all widgets from the dock.
"""
@rpc_call
def remove(self):
"""
Remove the dock from the parent dock area.
"""
@rpc_call
def attach(self):
"""
Attach the dock to the parent dock area.
"""
@rpc_call
def detach(self):
"""
Detach the dock from the parent dock area.
"""
class BECDockArea(RPCBase):
"""Container for other widgets. Widgets can be added to the dock area and arranged in a grid layout."""
@property
@rpc_call
def _rpc_id(self) -> "str":
"""
Get the RPC ID of the widget.
"""
@property
@rpc_call
def _config_dict(self) -> "dict":
"""
Get the configuration of the widget.
Returns:
dict: The configuration of the widget.
"""
@rpc_call
def _get_all_rpc(self) -> "dict":
"""
Get all registered RPC objects.
"""
@rpc_call
def new(
self,
name: "str | None" = None,
widget: "str | QWidget | None" = None,
widget_name: "str | None" = None,
position: "Literal['bottom', 'top', 'left', 'right', 'above', 'below']" = "bottom",
relative_to: "BECDock | None" = None,
closable: "bool" = True,
floating: "bool" = False,
row: "int | None" = None,
col: "int" = 0,
rowspan: "int" = 1,
colspan: "int" = 1,
) -> "BECDock":
"""
Add a dock to the dock area. Dock has QGridLayout as layout manager by default.
Args:
name(str): The name of the dock to be displayed and for further references. Has to be unique.
widget(str|QWidget|None): The widget to be added to the dock. While using RPC, only BEC RPC widgets from RPCWidgetHandler are allowed.
position(Literal["bottom", "top", "left", "right", "above", "below"]): The position of the dock.
relative_to(BECDock): The dock to which the new dock should be added relative to.
closable(bool): Whether the dock is closable.
floating(bool): Whether the dock is detached after creating.
row(int): The row of the added widget.
col(int): The column of the added widget.
rowspan(int): The rowspan of the added widget.
colspan(int): The colspan of the added widget.
Returns:
BECDock: The created dock.
"""
@rpc_call
def show(self):
"""
Show all windows including floating docks.
"""
@rpc_call
def hide(self):
"""
Hide all windows including floating docks.
"""
@property
@rpc_call
def panels(self) -> "dict[str, BECDock]":
"""
Get the docks in the dock area.
Returns:
dock_dict(dict): The docks in the dock area.
"""
@property
@rpc_call
def panel_list(self) -> "list[BECDock]":
"""
Get the docks in the dock area.
Returns:
list: The docks in the dock area.
"""
@rpc_call
def delete(self, dock_name: "str"):
"""
Delete a dock by name.
Args:
dock_name(str): The name of the dock to delete.
"""
@rpc_call
def delete_all(self) -> "None":
"""
Delete all docks.
"""
@rpc_call
def remove(self) -> "None":
"""
Remove the dock area. If the dock area is embedded in a BECMainWindow and
is set as the central widget, the main window will be closed.
"""
@rpc_call
def detach_dock(self, dock_name: "str") -> "BECDock":
"""
Undock a dock from the dock area.
Args:
dock_name(str): The dock to undock.
Returns:
BECDock: The undocked dock.
"""
@rpc_call
def attach_all(self):
"""
Return all floating docks to the dock area.
"""
@rpc_call
def save_state(self) -> "dict":
"""
Save the state of the dock area.
Returns:
dict: The state of the dock area.
"""
@rpc_timeout(None)
@rpc_call
def screenshot(self, file_name: "str | None" = None):
"""
Take a screenshot of the dock area and save it to a file.
"""
@rpc_call
def restore_state(
self, state: "dict" = None, missing: "Literal['ignore', 'error']" = "ignore", extra="bottom"
):
"""
Restore the state of the dock area. If no state is provided, the last state is restored.
Args:
state(dict): The state to restore.
missing(Literal['ignore','error']): What to do if a dock is missing.
extra(str): Extra docks that are in the dockarea but that are not mentioned in state will be added to the bottom of the dockarea, unless otherwise specified by the extra argument.
"""
class BECMainWindow(RPCBase):
@rpc_call
def remove(self):

View File

@@ -25,11 +25,9 @@ from qtpy.QtWidgets import (
QWidget,
)
from bec_widgets import BECWidget
from bec_widgets.cli.rpc.rpc_widget_handler import widget_handler
from bec_widgets.utils.colors import apply_theme
from bec_widgets.utils.widget_io import WidgetHierarchy as wh
from bec_widgets.widgets.containers.dock import BECDockArea
from bec_widgets.widgets.editors.jupyter_console.jupyter_console import BECJupyterConsole
@@ -366,15 +364,6 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
def closeEvent(self, event):
"""Override to handle things when main window is closed."""
# clean up any widgets that might have custom cleanup
try:
# call cleanup on known containers if present
dock = self._widgets_by_name.get("dock")
if isinstance(dock, BECDockArea):
dock.cleanup()
dock.close()
except Exception:
pass
# Ensure the embedded kernel and BEC client are shut down before window teardown
self.console.shutdown_kernel()

View File

@@ -86,7 +86,6 @@ class BECConnector:
config: ConnectionConfig | None = None,
gui_id: str | None = None,
object_name: str | None = None,
parent_dock: BECDock | None = None, # TODO should go away -> issue created #473
root_widget: bool = False,
**kwargs,
):
@@ -98,7 +97,6 @@ class BECConnector:
config(ConnectionConfig, optional): The connection configuration with specific gui id.
gui_id(str, optional): The GUI ID.
object_name(str, optional): The object name.
parent_dock(BECDock, optional): The parent dock.# TODO should go away -> issue created #473
root_widget(bool, optional): If set to True, the parent_id will be always set to None, thus enforcing that the widget is accessible as a root widget of the BECGuiClient object.
**kwargs:
"""
@@ -119,7 +117,6 @@ class BECConnector:
# BEC related connections
self.bec_dispatcher = BECDispatcher(client=client)
self.client = self.bec_dispatcher.client if client is None else client
self._parent_dock = parent_dock # TODO also remove at some point -> issue created #473
self.rpc_register = RPCRegister()
if not self.client in BECConnector.EXIT_HANDLERS:
@@ -456,12 +453,8 @@ class BECConnector:
def remove(self):
"""Cleanup the BECConnector"""
# If the widget is attached to a dock, remove it from the dock.
# TODO this should be handled by dock and dock are not by BECConnector -> issue created #473
if self._parent_dock is not None:
self._parent_dock.delete(self.object_name)
# If the widget is from Qt, trigger its close method.
elif hasattr(self, "close"):
if hasattr(self, "close"):
self.close()
# If the widget is neither from a Dock nor from Qt, remove it from the RPC registry.
# i.e. Curve Item from Waveform

View File

@@ -39,7 +39,6 @@ class BECWidget(BECConnector):
theme_update: bool = False,
start_busy: bool = False,
busy_text: str = "Loading…",
parent_dock: BECDock | None = None, # TODO should go away -> issue created #473
**kwargs,
):
"""
@@ -58,9 +57,7 @@ class BECWidget(BECConnector):
theme_update(bool, optional): Whether to subscribe to theme updates. Defaults to False. When set to True, the
widget's apply_theme method will be called when the theme changes.
"""
super().__init__(
client=client, config=config, gui_id=gui_id, parent_dock=parent_dock, **kwargs
)
super().__init__(client=client, config=config, gui_id=gui_id, **kwargs)
if not isinstance(self, QObject):
raise RuntimeError(f"{repr(self)} is not a subclass of QWidget")
if theme_update:

View File

@@ -104,6 +104,8 @@ class AdvancedDockArea(DockAreaWidget):
"print_layout_structure",
"mode",
"mode.setter",
"save_profile",
"load_profile",
]
# Define a signal for mode changes
@@ -172,10 +174,6 @@ class AdvancedDockArea(DockAreaWidget):
if self._ensure_initial_profile():
self._refresh_workspace_list()
# Sync Developer toggle icon state after initial setup #TODO temporary disable
# dev_action = self.toolbar.components.get_action("developer_mode").action
# dev_action.setChecked(self._editable)
# Apply the requested mode after everything is set up
self.mode = mode
if self._restore_initial_profile:
@@ -319,7 +317,7 @@ class AdvancedDockArea(DockAreaWidget):
def _setup_toolbar(self):
self.toolbar = ModularToolBar(parent=self)
PLOT_ACTIONS = {
plot_actions = {
"waveform": (Waveform.ICON_NAME, "Add Waveform", "Waveform"),
"scatter_waveform": (
ScatterWaveform.ICON_NAME,
@@ -331,7 +329,7 @@ class AdvancedDockArea(DockAreaWidget):
"motor_map": (MotorMap.ICON_NAME, "Add Motor Map", "MotorMap"),
"heatmap": (Heatmap.ICON_NAME, "Add Heatmap", "Heatmap"),
}
DEVICE_ACTIONS = {
device_actions = {
"scan_control": (ScanControl.ICON_NAME, "Add Scan Control", "ScanControl"),
"positioner_box": (PositionerBox.ICON_NAME, "Add Device Box", "PositionerBox"),
"positioner_box_2D": (
@@ -340,7 +338,7 @@ class AdvancedDockArea(DockAreaWidget):
"PositionerBox2D",
),
}
UTIL_ACTIONS = {
util_actions = {
"queue": (BECQueue.ICON_NAME, "Add Scan Queue", "BECQueue"),
"status": (BECStatusBox.ICON_NAME, "Add BEC Status Box", "BECStatusBox"),
"progress_bar": (
@@ -372,9 +370,9 @@ class AdvancedDockArea(DockAreaWidget):
b.add_action(key)
self.toolbar.add_bundle(b)
_build_menu("menu_plots", "Add Plot ", PLOT_ACTIONS)
_build_menu("menu_devices", "Add Device Control ", DEVICE_ACTIONS)
_build_menu("menu_utils", "Add Utils ", UTIL_ACTIONS)
_build_menu("menu_plots", "Add Plot ", plot_actions)
_build_menu("menu_devices", "Add Device Control ", device_actions)
_build_menu("menu_utils", "Add Utils ", util_actions)
# Create flat toolbar bundles for each widget type
def _build_flat_bundles(category: str, mapping: dict[str, tuple[str, str, str]]):
@@ -398,14 +396,14 @@ class AdvancedDockArea(DockAreaWidget):
self.toolbar.add_bundle(bundle)
_build_flat_bundles("plots", PLOT_ACTIONS)
_build_flat_bundles("devices", DEVICE_ACTIONS)
_build_flat_bundles("utils", UTIL_ACTIONS)
_build_flat_bundles("plots", plot_actions)
_build_flat_bundles("devices", device_actions)
_build_flat_bundles("utils", util_actions)
# Workspace
spacer_bundle = ToolbarBundle("spacer_bundle", self.toolbar.components)
spacer = QWidget(parent=self.toolbar.components.toolbar)
spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
spacer.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
self.toolbar.components.add_safe("spacer", WidgetAction(widget=spacer, adjust_size=False))
spacer_bundle.add_action("spacer")
self.toolbar.add_bundle(spacer_bundle)
@@ -444,16 +442,15 @@ class AdvancedDockArea(DockAreaWidget):
bda.add_action("attach_all")
bda.add_action("screenshot")
bda.add_action("dark_mode")
# bda.add_action("developer_mode") #TODO temporary disable
self.toolbar.add_bundle(bda)
self._apply_toolbar_layout()
# Store mappings on self for use in _hook_toolbar
self._ACTION_MAPPINGS = {
"menu_plots": PLOT_ACTIONS,
"menu_devices": DEVICE_ACTIONS,
"menu_utils": UTIL_ACTIONS,
"menu_plots": plot_actions,
"menu_devices": device_actions,
"menu_utils": util_actions,
}
def _hook_toolbar(self):
@@ -699,6 +696,9 @@ class AdvancedDockArea(DockAreaWidget):
Before switching, persist the current profile to the user copy.
Prefer loading the user copy; fall back to the default copy.
Args:
name (str | None): The name of the profile to load. If None, prompts the user.
"""
if not name: # Gui fallback if the name is not provided
name, ok = QInputDialog.getText(

View File

@@ -7,12 +7,12 @@ from bec_lib.logger import bec_logger
from bec_lib.messages import ScanStatusMessage
from bec_widgets.utils.error_popups import SafeSlot
from bec_widgets.widgets.containers.dock.dock_area import BECDockArea
from bec_widgets.widgets.containers.advanced_dock_area.advanced_dock_area import AdvancedDockArea
from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow
from bec_widgets.widgets.containers.qt_ads import CDockWidget
if TYPE_CHECKING: # pragma: no cover
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.widgets.containers.dock.dock import BECDock
from bec_widgets.widgets.plots.image.image import Image
from bec_widgets.widgets.plots.motor_map.motor_map import MotorMap
from bec_widgets.widgets.plots.multi_waveform.multi_waveform import MultiWaveform
@@ -24,7 +24,7 @@ logger = bec_logger.logger
class AutoUpdates(BECMainWindow):
_default_dock: BECDock
_default_dock: CDockWidget | None
USER_ACCESS = ["enabled", "enabled.setter", "selected_device", "selected_device.setter"]
RPC = True
PLUGIN = False
@@ -37,7 +37,12 @@ class AutoUpdates(BECMainWindow):
):
super().__init__(parent=parent, gui_id=gui_id, window_title=window_title, **kwargs)
self.dock_area = BECDockArea(parent=self, object_name="dock_area")
self.dock_area = AdvancedDockArea(
parent=self,
object_name="dock_area",
enable_profile_management=False,
restore_initial_profile=False,
)
self.setCentralWidget(self.dock_area)
self._auto_update_selected_device: str | None = None
@@ -106,9 +111,11 @@ class AutoUpdates(BECMainWindow):
"""
Create a default dock for the auto updates.
"""
self.dock_area.delete_all()
self.dock_name = "update_dock"
self._default_dock = self.dock_area.new(self.dock_name)
self.current_widget = self._default_dock.new("Waveform")
self.current_widget = self.dock_area.new("Waveform")
docks = self.dock_area.dock_list()
self._default_dock = docks[0] if docks else None
@overload
def set_dock_to_widget(self, widget: Literal["Waveform"]) -> Waveform: ...
@@ -138,16 +145,18 @@ class AutoUpdates(BECMainWindow):
Returns:
BECWidget: The widget that was set.
"""
if self._default_dock is None or self.current_widget is None:
if self.current_widget is None:
logger.warning(
f"Auto Updates: No default dock found. Creating a new one with name {self.dock_name}"
)
self.start_default_dock()
assert self.current_widget is not None
if not self.current_widget.__class__.__name__ == widget:
self._default_dock.delete(self.current_widget.object_name)
self.current_widget = self._default_dock.new(widget)
if self.current_widget.__class__.__name__ != widget:
self.dock_area.delete_all()
self.current_widget = self.dock_area.new(widget)
docks = self.dock_area.dock_list()
self._default_dock = docks[0] if docks else None
return self.current_widget
def get_selected_device(

View File

@@ -1,2 +0,0 @@
from .dock import BECDock
from .dock_area import BECDockArea

View File

@@ -1 +0,0 @@
{'files': ['dock_area.py']}

View File

@@ -1,57 +0,0 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from qtpy.QtWidgets import QWidget
from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.containers.dock.dock_area import BECDockArea
DOM_XML = """
<ui language='c++'>
<widget class='BECDockArea' name='bec_dock_area'>
</widget>
</ui>
"""
class BECDockAreaPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
def __init__(self):
super().__init__()
self._form_editor = None
def createWidget(self, parent):
if parent is None:
return QWidget()
t = BECDockArea(parent)
return t
def domXml(self):
return DOM_XML
def group(self):
return "BEC Containers"
def icon(self):
return designer_material_icon(BECDockArea.ICON_NAME)
def includeFile(self):
return "bec_dock_area"
def initialize(self, form_editor):
self._form_editor = form_editor
def isContainer(self):
return False
def isInitialized(self):
return self._form_editor is not None
def name(self):
return "BECDockArea"
def toolTip(self):
return ""
def whatsThis(self):
return self.toolTip()

View File

@@ -1,440 +0,0 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Literal, Optional, cast
from bec_lib.logger import bec_logger
from pydantic import Field
from pyqtgraph.dockarea import Dock, DockLabel
from qtpy import QtCore, QtGui
from bec_widgets.cli.client_utils import IGNORE_WIDGETS
from bec_widgets.cli.rpc.rpc_widget_handler import widget_handler
from bec_widgets.utils import ConnectionConfig, GridLayoutManager
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.container_utils import WidgetContainerUtils
from bec_widgets.utils.error_popups import SafeSlot
logger = bec_logger.logger
if TYPE_CHECKING: # pragma: no cover
from qtpy.QtWidgets import QWidget
from bec_widgets.widgets.containers.dock.dock_area import BECDockArea
class DockConfig(ConnectionConfig):
widgets: dict[str, Any] = Field({}, description="The widgets in the dock.")
position: Literal["bottom", "top", "left", "right", "above", "below"] = Field(
"bottom", description="The position of the dock."
)
parent_dock_area: Optional[str] | None = Field(
None, description="The GUI ID of parent dock area of the dock."
)
class CustomDockLabel(DockLabel):
def __init__(self, text: str, closable: bool = True):
super().__init__(text, closable)
if closable:
red_icon = QtGui.QIcon()
pixmap = QtGui.QPixmap(32, 32)
pixmap.fill(QtCore.Qt.GlobalColor.red)
painter = QtGui.QPainter(pixmap)
pen = QtGui.QPen(QtCore.Qt.GlobalColor.white)
pen.setWidth(2)
painter.setPen(pen)
painter.drawLine(8, 8, 24, 24)
painter.drawLine(24, 8, 8, 24)
painter.end()
red_icon.addPixmap(pixmap)
self.closeButton.setIcon(red_icon)
def updateStyle(self):
r = "3px"
if self.dim:
fg = "#aaa"
bg = "#44a"
border = "#339"
else:
fg = "#fff"
bg = "#3f4042"
border = "#3f4042"
if self.orientation == "vertical":
self.vStyle = """DockLabel {
background-color : %s;
color : %s;
border-top-right-radius: 0px;
border-top-left-radius: %s;
border-bottom-right-radius: 0px;
border-bottom-left-radius: %s;
border-width: 0px;
border-right: 2px solid %s;
padding-top: 3px;
padding-bottom: 3px;
font-size: %s;
}""" % (
bg,
fg,
r,
r,
border,
self.fontSize,
)
self.setStyleSheet(self.vStyle)
else:
self.hStyle = """DockLabel {
background-color : %s;
color : %s;
border-top-right-radius: %s;
border-top-left-radius: %s;
border-bottom-right-radius: 0px;
border-bottom-left-radius: 0px;
border-width: 0px;
border-bottom: 2px solid %s;
padding-left: 3px;
padding-right: 3px;
font-size: %s;
}""" % (
bg,
fg,
r,
r,
border,
self.fontSize,
)
self.setStyleSheet(self.hStyle)
class BECDock(BECWidget, Dock):
ICON_NAME = "widgets"
USER_ACCESS = [
"_config_dict",
"element_list",
"elements",
"new",
"show",
"hide",
"show_title_bar",
"set_title",
"hide_title_bar",
"available_widgets",
"delete",
"delete_all",
"remove",
"attach",
"detach",
]
def __init__(
self,
parent: QWidget | None = None,
parent_dock_area: BECDockArea | None = None,
config: DockConfig | None = None,
name: str | None = None,
object_name: str | None = None,
client=None,
gui_id: str | None = None,
closable: bool = True,
**kwargs,
) -> None:
if config is None:
config = DockConfig(
widget_class=self.__class__.__name__,
parent_dock_area=parent_dock_area.gui_id if parent_dock_area else None,
)
else:
if isinstance(config, dict):
config = DockConfig(**config)
self.config = config
label = CustomDockLabel(text=name, closable=closable)
super().__init__(
parent=parent_dock_area,
name=name,
object_name=object_name,
client=client,
gui_id=gui_id,
config=config,
label=label,
**kwargs,
)
self.parent_dock_area = parent_dock_area
# Layout Manager
self.layout_manager = GridLayoutManager(self.layout)
def dropEvent(self, event):
source = event.source()
old_area = source.area
self.setOrientation("horizontal", force=True)
super().dropEvent(event)
if old_area in self.orig_area.tempAreas and old_area != self.orig_area:
self.orig_area.removeTempArea(old_area)
old_area.window().deleteLater()
def float(self):
"""
Float the dock.
Overwrites the default pyqtgraph dock float.
"""
# need to check if the dock is temporary and if it is the only dock in the area
# fixes bug in pyqtgraph detaching
if self.area.temporary == True and len(self.area.docks) <= 1:
return
elif self.area.temporary == True and len(self.area.docks) > 1:
self.area.docks.pop(self.name(), None)
super().float()
else:
super().float()
@property
def elements(self) -> dict[str, BECWidget]:
"""
Get the widgets in the dock.
Returns:
widgets(dict): The widgets in the dock.
"""
# pylint: disable=protected-access
return dict((widget.object_name, widget) for widget in self.element_list)
@property
def element_list(self) -> list[BECWidget]:
"""
Get the widgets in the dock.
Returns:
widgets(list): The widgets in the dock.
"""
return self.widgets
def hide_title_bar(self):
"""
Hide the title bar of the dock.
"""
# self.hideTitleBar() #TODO pyqtgraph looks bugged ATM, doing my implementation
self.label.hide()
self.labelHidden = True
def show(self):
"""
Show the dock.
"""
super().show()
self.show_title_bar()
def hide(self):
"""
Hide the dock.
"""
self.hide_title_bar()
super().hide()
def show_title_bar(self):
"""
Hide the title bar of the dock.
"""
# self.showTitleBar() #TODO pyqtgraph looks bugged ATM, doing my implementation
self.label.show()
self.labelHidden = False
def set_title(self, title: str):
"""
Set the title of the dock.
Args:
title(str): The title of the dock.
"""
self.orig_area.docks[title] = self.orig_area.docks.pop(self.name())
self.setTitle(title)
def get_widgets_positions(self) -> dict:
"""
Get the positions of the widgets in the dock.
Returns:
dict: The positions of the widgets in the dock as dict -> {(row, col, rowspan, colspan):widget}
"""
return self.layout_manager.get_widgets_positions()
def available_widgets(
self,
) -> list: # TODO can be moved to some util mixin like container class for rpc widgets
"""
List all widgets that can be added to the dock.
Returns:
list: The list of eligible widgets.
"""
return list(widget_handler.widget_classes.keys())
def _get_list_of_widget_name_of_parent_dock_area(self) -> list[str]:
if (docks := self.parent_dock_area.panel_list) is None:
return []
widgets = []
for dock in docks:
widgets.extend(dock.elements.keys())
return widgets
@SafeSlot(popup_error=True)
def new(
self,
widget: BECWidget | str,
name: str | None = None,
row: int | None = None,
col: int = 0,
rowspan: int = 1,
colspan: int = 1,
shift: Literal["down", "up", "left", "right"] = "down",
) -> BECWidget:
"""
Add a widget to the dock.
Args:
widget(QWidget): The widget to add. It can not be BECDock or BECDockArea.
name(str): The name of the widget.
row(int): The row to add the widget to. If None, the widget will be added to the next available row.
col(int): The column to add the widget to.
rowspan(int): The number of rows the widget should span.
colspan(int): The number of columns the widget should span.
shift(Literal["down", "up", "left", "right"]): The direction to shift the widgets if the position is occupied.
"""
if name is not None:
WidgetContainerUtils.raise_for_invalid_name(name, container=self)
if row is None:
row = self.layout.rowCount()
if self.layout_manager.is_position_occupied(row, col):
self.layout_manager.shift_widgets(shift, start_row=row)
# Check that Widget is not BECDock or BECDockArea
widget_class_name = widget if isinstance(widget, str) else widget.__class__.__name__
if widget_class_name in IGNORE_WIDGETS:
raise ValueError(f"Widget {widget} can not be added to dock.")
if isinstance(widget, str):
widget = cast(
BECWidget,
widget_handler.create_widget(
widget_type=widget, object_name=name, parent_dock=self, parent=self
),
)
else:
widget.object_name = name
self.addWidget(widget, row=row, col=col, rowspan=rowspan, colspan=colspan)
if hasattr(widget, "config"):
widget.config.gui_id = widget.gui_id
self.config.widgets[widget.object_name] = widget.config
return widget
def move_widget(self, widget: QWidget, new_row: int, new_col: int):
"""
Move a widget to a new position in the layout.
Args:
widget(QWidget): The widget to move.
new_row(int): The new row to move the widget to.
new_col(int): The new column to move the widget to.
"""
self.layout_manager.move_widget(widget, new_row, new_col)
def attach(self):
"""
Attach the dock to the parent dock area.
"""
self.parent_dock_area.remove_temp_area(self.area)
def detach(self):
"""
Detach the dock from the parent dock area.
"""
self.float()
def remove(self):
"""
Remove the dock from the parent dock area.
"""
self.parent_dock_area.delete(self.object_name)
def delete(self, widget_name: str) -> None:
"""
Remove a widget from the dock.
Args:
widget_name(str): Delete the widget with the given name.
"""
# pylint: disable=protected-access
widgets = [widget for widget in self.widgets if widget.object_name == widget_name]
if len(widgets) == 0:
logger.warning(
f"Widget with name {widget_name} not found in dock {self.name()}. "
f"Checking if gui_id was passed as widget_name."
)
# Try to find the widget in the RPC register, maybe the gui_id was passed as widget_name
widget = self.rpc_register.get_rpc_by_id(widget_name)
if widget is None:
logger.warning(
f"Widget not found for name or gui_id: {widget_name} in dock {self.name()}"
)
return
else:
widget = widgets[0]
self.layout.removeWidget(widget)
self.config.widgets.pop(widget.object_name, None)
if widget in self.widgets:
self.widgets.remove(widget)
widget.close()
widget.deleteLater()
def delete_all(self):
"""
Remove all widgets from the dock.
"""
for widget in self.widgets:
self.delete(widget.object_name)
def cleanup(self):
"""
Clean up the dock, including all its widgets.
"""
# # FIXME Cleanup might be called twice
try:
logger.info(f"Cleaning up dock {self.name()}")
self.label.close()
self.label.deleteLater()
except Exception as e:
logger.error(f"Error while closing dock label: {e}")
# Remove the dock from the parent dock area
if self.parent_dock_area:
self.parent_dock_area.dock_area.docks.pop(self.name(), None)
self.parent_dock_area.config.docks.pop(self.name(), None)
self.delete_all()
self.widgets.clear()
super().cleanup()
self.deleteLater()
def close(self):
"""
Close the dock area and cleanup.
Has to be implemented to overwrite pyqtgraph event accept in Container close.
"""
self.cleanup()
super().close()
if __name__ == "__main__": # pragma: no cover
import sys
from qtpy.QtWidgets import QApplication
app = QApplication([])
dock = BECDock(name="dock")
dock.show()
app.exec_()
sys.exit(app.exec_())

View File

@@ -1,633 +0,0 @@
from __future__ import annotations
from typing import Literal, Optional
from weakref import WeakValueDictionary
from bec_lib.logger import bec_logger
from pydantic import Field
from pyqtgraph.dockarea.DockArea import DockArea
from qtpy.QtCore import QSize, Qt
from qtpy.QtGui import QPainter, QPaintEvent
from qtpy.QtWidgets import QApplication, QSizePolicy, QVBoxLayout, QWidget
from bec_widgets.cli.rpc.rpc_register import RPCRegister
from bec_widgets.utils import ConnectionConfig, WidgetContainerUtils
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.error_popups import SafeSlot
from bec_widgets.utils.name_utils import pascal_to_snake
from bec_widgets.utils.toolbars.actions import (
ExpandableMenuAction,
MaterialIconAction,
WidgetAction,
)
from bec_widgets.utils.toolbars.bundles import ToolbarBundle
from bec_widgets.utils.toolbars.toolbar import ModularToolBar
from bec_widgets.utils.widget_io import WidgetHierarchy
from bec_widgets.widgets.containers.dock.dock import BECDock, DockConfig
from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow
from bec_widgets.widgets.control.device_control.positioner_box import PositionerBox
from bec_widgets.widgets.control.scan_control.scan_control import ScanControl
from bec_widgets.widgets.editors.vscode.vscode import VSCodeEditor
from bec_widgets.widgets.plots.heatmap.heatmap import Heatmap
from bec_widgets.widgets.plots.image.image import Image
from bec_widgets.widgets.plots.motor_map.motor_map import MotorMap
from bec_widgets.widgets.plots.multi_waveform.multi_waveform import MultiWaveform
from bec_widgets.widgets.plots.scatter_waveform.scatter_waveform import ScatterWaveform
from bec_widgets.widgets.plots.waveform.waveform import Waveform
from bec_widgets.widgets.progress.ring_progress_bar.ring_progress_bar import RingProgressBar
from bec_widgets.widgets.services.bec_queue.bec_queue import BECQueue
from bec_widgets.widgets.services.bec_status_box.bec_status_box import BECStatusBox
from bec_widgets.widgets.utility.logpanel.logpanel import LogPanel
from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton
logger = bec_logger.logger
class DockAreaConfig(ConnectionConfig):
docks: dict[str, DockConfig] = Field({}, description="The docks in the dock area.")
docks_state: Optional[dict] = Field(
None, description="The state of the docks in the dock area."
)
class BECDockArea(BECWidget, QWidget):
"""
Container for other widgets. Widgets can be added to the dock area and arranged in a grid layout.
"""
PLUGIN = True
USER_ACCESS = [
"_rpc_id",
"_config_dict",
"_get_all_rpc",
"new",
"show",
"hide",
"panels",
"panel_list",
"delete",
"delete_all",
"remove",
"detach_dock",
"attach_all",
"save_state",
"screenshot",
"restore_state",
]
def __init__(
self,
parent: QWidget | None = None,
config: DockAreaConfig | None = None,
client=None,
gui_id: str = None,
object_name: str = None,
**kwargs,
) -> None:
if config is None:
config = DockAreaConfig(widget_class=self.__class__.__name__)
else:
if isinstance(config, dict):
config = DockAreaConfig(**config)
self.config = config
super().__init__(
parent=parent,
object_name=object_name,
client=client,
gui_id=gui_id,
config=config,
**kwargs,
)
self._parent = parent # TODO probably not needed
self.layout = QVBoxLayout(self)
self.layout.setSpacing(5)
self.layout.setContentsMargins(0, 0, 0, 0)
self._instructions_visible = True
self.dark_mode_button = DarkModeButton(parent=self, toolbar=True)
self.dock_area = DockArea(parent=self)
self.toolbar = ModularToolBar(parent=self)
self._setup_toolbar()
self.layout.addWidget(self.toolbar)
self.layout.addWidget(self.dock_area)
self._hook_toolbar()
self.toolbar.show_bundles(
["menu_plots", "menu_devices", "menu_utils", "dock_actions", "dark_mode"]
)
def minimumSizeHint(self):
return QSize(800, 600)
def _setup_toolbar(self):
# Add plot menu
self.toolbar.components.add_safe(
"menu_plots",
ExpandableMenuAction(
label="Add Plot ",
actions={
"waveform": MaterialIconAction(
icon_name=Waveform.ICON_NAME,
tooltip="Add Waveform",
filled=True,
parent=self,
),
"scatter_waveform": MaterialIconAction(
icon_name=ScatterWaveform.ICON_NAME,
tooltip="Add Scatter Waveform",
filled=True,
parent=self,
),
"multi_waveform": MaterialIconAction(
icon_name=MultiWaveform.ICON_NAME,
tooltip="Add Multi Waveform",
filled=True,
parent=self,
),
"image": MaterialIconAction(
icon_name=Image.ICON_NAME, tooltip="Add Image", filled=True, parent=self
),
"motor_map": MaterialIconAction(
icon_name=MotorMap.ICON_NAME,
tooltip="Add Motor Map",
filled=True,
parent=self,
),
"heatmap": MaterialIconAction(
icon_name=Heatmap.ICON_NAME, tooltip="Add Heatmap", filled=True, parent=self
),
},
),
)
bundle = ToolbarBundle("menu_plots", self.toolbar.components)
bundle.add_action("menu_plots")
self.toolbar.add_bundle(bundle)
# Add control menu
self.toolbar.components.add_safe(
"menu_devices",
ExpandableMenuAction(
label="Add Device Control ",
actions={
"scan_control": MaterialIconAction(
icon_name=ScanControl.ICON_NAME,
tooltip="Add Scan Control",
filled=True,
parent=self,
),
"positioner_box": MaterialIconAction(
icon_name=PositionerBox.ICON_NAME,
tooltip="Add Device Box",
filled=True,
parent=self,
),
},
),
)
bundle = ToolbarBundle("menu_devices", self.toolbar.components)
bundle.add_action("menu_devices")
self.toolbar.add_bundle(bundle)
# Add utils menu
self.toolbar.components.add_safe(
"menu_utils",
ExpandableMenuAction(
label="Add Utils ",
actions={
"queue": MaterialIconAction(
icon_name=BECQueue.ICON_NAME,
tooltip="Add Scan Queue",
filled=True,
parent=self,
),
"vs_code": MaterialIconAction(
icon_name=VSCodeEditor.ICON_NAME,
tooltip="Add VS Code",
filled=True,
parent=self,
),
"status": MaterialIconAction(
icon_name=BECStatusBox.ICON_NAME,
tooltip="Add BEC Status Box",
filled=True,
parent=self,
),
"progress_bar": MaterialIconAction(
icon_name=RingProgressBar.ICON_NAME,
tooltip="Add Circular ProgressBar",
filled=True,
parent=self,
),
# FIXME temporarily disabled -> issue #644
"log_panel": MaterialIconAction(
icon_name=LogPanel.ICON_NAME,
tooltip="Add LogPanel - Disabled",
filled=True,
parent=self,
),
"sbb_monitor": MaterialIconAction(
icon_name="train", tooltip="Add SBB Monitor", filled=True, parent=self
),
},
),
)
bundle = ToolbarBundle("menu_utils", self.toolbar.components)
bundle.add_action("menu_utils")
self.toolbar.add_bundle(bundle)
########## Dock Actions ##########
spacer = QWidget(parent=self)
spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.toolbar.components.add_safe("spacer", WidgetAction(widget=spacer, adjust_size=False))
self.toolbar.components.add_safe(
"dark_mode", WidgetAction(widget=self.dark_mode_button, adjust_size=False)
)
bundle = ToolbarBundle("dark_mode", self.toolbar.components)
bundle.add_action("spacer")
bundle.add_action("dark_mode")
self.toolbar.add_bundle(bundle)
self.toolbar.components.add_safe(
"attach_all",
MaterialIconAction(
icon_name="zoom_in_map", tooltip="Attach all floating docks", parent=self
),
)
self.toolbar.components.add_safe(
"save_state",
MaterialIconAction(icon_name="bookmark", tooltip="Save Dock State", parent=self),
)
self.toolbar.components.add_safe(
"restore_state",
MaterialIconAction(icon_name="frame_reload", tooltip="Restore Dock State", parent=self),
)
self.toolbar.components.add_safe(
"screenshot",
MaterialIconAction(icon_name="photo_camera", tooltip="Take Screenshot", parent=self),
)
bundle = ToolbarBundle("dock_actions", self.toolbar.components)
bundle.add_action("attach_all")
bundle.add_action("save_state")
bundle.add_action("restore_state")
bundle.add_action("screenshot")
self.toolbar.add_bundle(bundle)
def _hook_toolbar(self):
menu_plots = self.toolbar.components.get_action("menu_plots")
menu_devices = self.toolbar.components.get_action("menu_devices")
menu_utils = self.toolbar.components.get_action("menu_utils")
menu_plots.actions["waveform"].action.triggered.connect(
lambda: self._create_widget_from_toolbar(widget_name="Waveform")
)
menu_plots.actions["scatter_waveform"].action.triggered.connect(
lambda: self._create_widget_from_toolbar(widget_name="ScatterWaveform")
)
menu_plots.actions["multi_waveform"].action.triggered.connect(
lambda: self._create_widget_from_toolbar(widget_name="MultiWaveform")
)
menu_plots.actions["image"].action.triggered.connect(
lambda: self._create_widget_from_toolbar(widget_name="Image")
)
menu_plots.actions["motor_map"].action.triggered.connect(
lambda: self._create_widget_from_toolbar(widget_name="MotorMap")
)
menu_plots.actions["heatmap"].action.triggered.connect(
lambda: self._create_widget_from_toolbar(widget_name="Heatmap")
)
# Menu Devices
menu_devices.actions["scan_control"].action.triggered.connect(
lambda: self._create_widget_from_toolbar(widget_name="ScanControl")
)
menu_devices.actions["positioner_box"].action.triggered.connect(
lambda: self._create_widget_from_toolbar(widget_name="PositionerBox")
)
# Menu Utils
menu_utils.actions["queue"].action.triggered.connect(
lambda: self._create_widget_from_toolbar(widget_name="BECQueue")
)
menu_utils.actions["status"].action.triggered.connect(
lambda: self._create_widget_from_toolbar(widget_name="BECStatusBox")
)
menu_utils.actions["vs_code"].action.triggered.connect(
lambda: self._create_widget_from_toolbar(widget_name="VSCodeEditor")
)
menu_utils.actions["progress_bar"].action.triggered.connect(
lambda: self._create_widget_from_toolbar(widget_name="RingProgressBar")
)
# FIXME temporarily disabled -> issue #644
menu_utils.actions["log_panel"].action.setEnabled(False)
menu_utils.actions["sbb_monitor"].action.triggered.connect(
lambda: self._create_widget_from_toolbar(widget_name="SBBMonitor")
)
# Icons
self.toolbar.components.get_action("attach_all").action.triggered.connect(self.attach_all)
self.toolbar.components.get_action("save_state").action.triggered.connect(self.save_state)
self.toolbar.components.get_action("restore_state").action.triggered.connect(
self.restore_state
)
self.toolbar.components.get_action("screenshot").action.triggered.connect(self.screenshot)
@SafeSlot()
def _create_widget_from_toolbar(self, widget_name: str) -> None:
# Run with RPC broadcast to namespace of all widgets
with RPCRegister.delayed_broadcast():
name = pascal_to_snake(widget_name)
dock_name = WidgetContainerUtils.generate_unique_name(name, self.panels.keys())
self.new(name=dock_name, widget=widget_name)
def paintEvent(self, event: QPaintEvent): # TODO decide if we want any default instructions
super().paintEvent(event)
if self._instructions_visible:
painter = QPainter(self)
painter.drawText(
self.rect(),
Qt.AlignCenter,
"Add docks using 'new' method from CLI\n or \n Add widget docks using the toolbar",
)
@property
def panels(self) -> dict[str, BECDock]:
"""
Get the docks in the dock area.
Returns:
dock_dict(dict): The docks in the dock area.
"""
return dict(self.dock_area.docks)
@panels.setter
def panels(self, value: dict[str, BECDock]):
self.dock_area.docks = WeakValueDictionary(value) # This can not work can it?
@property
def panel_list(self) -> list[BECDock]:
"""
Get the docks in the dock area.
Returns:
list: The docks in the dock area.
"""
return list(self.dock_area.docks.values())
@property
def temp_areas(self) -> list:
"""
Get the temporary areas in the dock area.
Returns:
list: The temporary areas in the dock area.
"""
return list(map(str, self.dock_area.tempAreas))
@temp_areas.setter
def temp_areas(self, value: list):
self.dock_area.tempAreas = list(map(str, value))
@SafeSlot()
def restore_state(
self, state: dict = None, missing: Literal["ignore", "error"] = "ignore", extra="bottom"
):
"""
Restore the state of the dock area. If no state is provided, the last state is restored.
Args:
state(dict): The state to restore.
missing(Literal['ignore','error']): What to do if a dock is missing.
extra(str): Extra docks that are in the dockarea but that are not mentioned in state will be added to the bottom of the dockarea, unless otherwise specified by the extra argument.
"""
if state is None:
state = self.config.docks_state
if state is None:
return
self.dock_area.restoreState(state, missing=missing, extra=extra)
@SafeSlot()
def save_state(self) -> dict:
"""
Save the state of the dock area.
Returns:
dict: The state of the dock area.
"""
last_state = self.dock_area.saveState()
self.config.docks_state = last_state
return last_state
@SafeSlot(popup_error=True)
def new(
self,
name: str | None = None,
widget: str | QWidget | None = None,
widget_name: str | None = None,
position: Literal["bottom", "top", "left", "right", "above", "below"] = "bottom",
relative_to: BECDock | None = None,
closable: bool = True,
floating: bool = False,
row: int | None = None,
col: int = 0,
rowspan: int = 1,
colspan: int = 1,
) -> BECDock:
"""
Add a dock to the dock area. Dock has QGridLayout as layout manager by default.
Args:
name(str): The name of the dock to be displayed and for further references. Has to be unique.
widget(str|QWidget|None): The widget to be added to the dock. While using RPC, only BEC RPC widgets from RPCWidgetHandler are allowed.
position(Literal["bottom", "top", "left", "right", "above", "below"]): The position of the dock.
relative_to(BECDock): The dock to which the new dock should be added relative to.
closable(bool): Whether the dock is closable.
floating(bool): Whether the dock is detached after creating.
row(int): The row of the added widget.
col(int): The column of the added widget.
rowspan(int): The rowspan of the added widget.
colspan(int): The colspan of the added widget.
Returns:
BECDock: The created dock.
"""
dock_names = [
dock.object_name for dock in self.panel_list
] # pylint: disable=protected-access
if name is not None: # Name is provided
if name in dock_names:
raise ValueError(
f"Name {name} must be unique for docks, but already exists in DockArea "
f"with name: {self.object_name} and id {self.gui_id}."
)
WidgetContainerUtils.raise_for_invalid_name(name, container=self)
else: # Name is not provided
name = WidgetContainerUtils.generate_unique_name(name="dock", list_of_names=dock_names)
dock = BECDock(
parent=self,
name=name, # this is dock name pyqtgraph property, this is displayed on label
object_name=name, # this is a real qt object name passed to BECConnector
parent_dock_area=self,
closable=closable,
)
dock.config.position = position
self.config.docks[dock.name()] = dock.config
# The dock.name is equal to the name passed to BECDock
self.dock_area.addDock(dock=dock, position=position, relativeTo=relative_to)
if len(self.dock_area.docks) <= 1:
dock.hide_title_bar()
elif len(self.dock_area.docks) > 1:
for dock in self.dock_area.docks.values():
dock.show_title_bar()
if widget is not None:
# Check if widget name exists.
dock.new(
widget=widget, name=widget_name, row=row, col=col, rowspan=rowspan, colspan=colspan
)
if (
self._instructions_visible
): # TODO still decide how initial instructions should be handled
self._instructions_visible = False
self.update()
if floating:
dock.detach()
return dock
def detach_dock(self, dock_name: str) -> BECDock:
"""
Undock a dock from the dock area.
Args:
dock_name(str): The dock to undock.
Returns:
BECDock: The undocked dock.
"""
dock = self.dock_area.docks[dock_name]
dock.detach()
return dock
@SafeSlot()
def attach_all(self):
"""
Return all floating docks to the dock area.
"""
while self.dock_area.tempAreas:
for temp_area in self.dock_area.tempAreas:
self.remove_temp_area(temp_area)
def remove_temp_area(self, area):
"""
Remove a temporary area from the dock area.
This is a patched method of pyqtgraph's removeTempArea
"""
if area not in self.dock_area.tempAreas:
# FIXME add some context for the logging, I am not sure which object is passed.
# It looks like a pyqtgraph.DockArea
logger.info(f"Attempted to remove dock_area, but was not floating.")
return
self.dock_area.tempAreas.remove(area)
area.window().close()
area.window().deleteLater()
def cleanup(self):
"""
Cleanup the dock area.
"""
self.delete_all()
self.dark_mode_button.close()
self.dark_mode_button.deleteLater()
super().cleanup()
def show(self):
"""Show all windows including floating docks."""
super().show()
for docks in self.panels.values():
if docks.window() is self:
# avoid recursion
continue
docks.window().show()
def hide(self):
"""Hide all windows including floating docks."""
super().hide()
for docks in self.panels.values():
if docks.window() is self:
# avoid recursion
continue
docks.window().hide()
def delete_all(self) -> None:
"""
Delete all docks.
"""
self.attach_all()
for dock_name in self.panels.keys():
self.delete(dock_name)
def delete(self, dock_name: str):
"""
Delete a dock by name.
Args:
dock_name(str): The name of the dock to delete.
"""
dock = self.dock_area.docks.pop(dock_name, None)
self.config.docks.pop(dock_name, None)
if dock:
dock.close()
dock.deleteLater()
if len(self.dock_area.docks) <= 1:
for dock in self.dock_area.docks.values():
dock.hide_title_bar()
else:
raise ValueError(f"Dock with name {dock_name} does not exist.")
# self._broadcast_update()
def remove(self) -> None:
"""
Remove the dock area. If the dock area is embedded in a BECMainWindow and
is set as the central widget, the main window will be closed.
"""
parent = self.parent()
if isinstance(parent, BECMainWindow):
central_widget = parent.centralWidget()
if central_widget is self:
# Closing the parent will also close the dock area
parent.close()
return
self.close()
if __name__ == "__main__": # pragma: no cover
import sys
from bec_widgets.utils.colors import apply_theme
app = QApplication([])
apply_theme("dark")
dock_area = BECDockArea()
dock_1 = dock_area.new(name="dock_0", widget="DarkModeButton")
dock_1.new(widget="DarkModeButton")
# dock_1 = dock_area.new(name="dock_0", widget="Waveform")
dock_area.new(widget="DarkModeButton")
dock_area.show()
dock_area.setGeometry(100, 100, 800, 600)
app.topLevelWidgets()
WidgetHierarchy.print_becconnector_hierarchy_from_app()
app.exec_()
sys.exit(app.exec_())

View File

@@ -1,15 +0,0 @@
def main(): # pragma: no cover
from qtpy import PYSIDE6
if not PYSIDE6:
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
return
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
from bec_widgets.widgets.containers.dock.bec_dock_area_plugin import BECDockAreaPlugin
QPyDesignerCustomWidgetCollection.addCustomWidget(BECDockAreaPlugin())
if __name__ == "__main__": # pragma: no cover
main()

View File

@@ -7,7 +7,6 @@ from bec_widgets.utils.bec_connector import ConnectionConfig
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
from bec_widgets.utils.ophyd_kind_util import Kind
from bec_widgets.widgets.containers.dock.dock import BECDock
from bec_widgets.widgets.utility.signal_label.signal_label import SignalLabel
@@ -21,12 +20,11 @@ class SignalDisplay(BECWidget, QWidget):
config: ConnectionConfig = None,
gui_id: str | None = None,
theme_update: bool = False,
parent_dock: BECDock | None = None,
**kwargs,
):
"""A widget to display all the signals from a given device, and allow getting
a fresh reading."""
super().__init__(client, config, gui_id, theme_update, parent_dock, **kwargs)
super().__init__(client, config, gui_id, theme_update, **kwargs)
self.get_bec_shortcuts()
self._layout = QVBoxLayout()
self.setLayout(self._layout)

View File

@@ -47,6 +47,10 @@ def connected_client_gui_obj(qtbot, gui_id, bec_client_lib):
try:
gui.start(wait=True)
qtbot.waitUntil(lambda: hasattr(gui, "bec"), timeout=5000)
gui.bec.delete_all() # ensure clean state
qtbot.waitUntil(lambda: len(gui.bec.widget_list()) == 0, timeout=10000)
yield gui
finally:
gui.bec.delete_all() # ensure clean state
qtbot.waitUntil(lambda: len(gui.bec.widget_list()) == 0, timeout=10000)
gui.kill_server()

View File

@@ -19,56 +19,33 @@ def test_gui_rpc_registry(qtbot, connected_client_gui_obj):
qtbot.waitUntil(check_dock_area_registered, timeout=5000)
assert hasattr(gui, "cool_dock_area")
dock = dock_area.new("dock_0")
widget = dock_area.new("Waveform", object_name="cool_waveform")
def check_dock_registered():
return dock._gui_id in gui._server_registry
def check_widget_registered():
return widget._gui_id in gui._server_registry
qtbot.waitUntil(check_dock_registered, timeout=5000)
assert hasattr(gui.cool_dock_area, "dock_0")
qtbot.waitUntil(check_widget_registered, timeout=5000)
assert hasattr(gui.cool_dock_area, widget.object_name)
def test_rpc_add_dock_with_plots_e2e(qtbot, bec_client_lib, connected_client_gui_obj):
gui = connected_client_gui_obj
# BEC client shortcuts
dock = gui.bec
client = bec_client_lib
dev = client.device_manager.devices
scans = client.scans
queue = client.queue
# Create 3 docks
d0 = dock.new("dock_0")
d1 = dock.new("dock_1")
d2 = dock.new("dock_2")
# Check that callback for dock_registry is done
def check_docks_registered():
return all(
[gui_id in gui._server_registry for gui_id in [d0._gui_id, d1._gui_id, d2._gui_id]]
)
# Waii until docks are registered
qtbot.waitUntil(check_docks_registered, timeout=5000)
assert len(dock.panels) == 3
assert hasattr(gui.bec, "dock_0")
dock_area = gui.bec
# Add 3 figures with some widgets
wf = d0.new("Waveform")
im = d1.new("Image")
mm = d2.new("MotorMap")
wf = dock_area.new("Waveform")
im = dock_area.new("Image")
mm = dock_area.new("MotorMap")
def check_figs_registered():
def check_widgets_registered():
return all(
[gui_id in gui._server_registry for gui_id in [wf._gui_id, im._gui_id, mm._gui_id]]
gui_id in gui._server_registry for gui_id in [wf._gui_id, im._gui_id, mm._gui_id]
)
qtbot.waitUntil(check_figs_registered, timeout=5000)
assert len(d0.element_list) == 1
assert len(d1.element_list) == 1
assert len(d2.element_list) == 1
qtbot.waitUntil(check_widgets_registered, timeout=5000)
assert len(dock_area.widget_list()) == 3
assert wf.__class__.__name__ == "RPCReference"
assert wf.__class__ == RPCReference
@@ -94,48 +71,46 @@ def test_dock_manipulations_e2e(qtbot, connected_client_gui_obj):
gui = connected_client_gui_obj
dock_area = gui.bec
d0 = dock_area.new("dock_0")
d1 = dock_area.new("dock_1")
d2 = dock_area.new("dock_2")
w0 = dock_area.new("Waveform")
w1 = dock_area.new("Waveform")
w2 = dock_area.new("Waveform")
assert hasattr(gui.bec, "dock_0")
assert hasattr(gui.bec, "dock_1")
assert hasattr(gui.bec, "dock_2")
assert len(gui.bec.panels) == 3
assert hasattr(gui.bec, "Waveform")
assert hasattr(gui.bec, "Waveform_0")
assert hasattr(gui.bec, "Waveform_1")
assert len(gui.bec.widget_list()) == 3
d0.detach()
dock_area.detach_dock("dock_2")
# How can we properly check that the dock is detached?
assert len(gui.bec.panels) == 3
w0.detach()
w2.detach()
assert len(gui.bec.widget_list()) == 3
d0.attach()
assert len(gui.bec.panels) == 3
w0.attach()
w2.attach()
assert len(gui.bec.widget_list()) == 3
gui_id = d2._gui_id
gui_id = w2._gui_id
def wait_for_dock_removed():
return gui_id not in gui._ipython_registry
d2.remove()
w2.remove()
qtbot.waitUntil(wait_for_dock_removed, timeout=5000)
assert len(gui.bec.panels) == 2
ids = [widget._gui_id for widget in dock_area.panel_list]
def wait_for_docks_removed():
return all(widget_id not in gui._ipython_registry for widget_id in ids)
assert len(gui.bec.widget_list()) == 2
dock_area.delete_all()
qtbot.waitUntil(wait_for_docks_removed, timeout=5000)
assert len(gui.bec.panels) == 0
def wait_for_all_docks_deleted():
return len(gui.bec.widget_list()) == 0
qtbot.waitUntil(wait_for_all_docks_deleted, timeout=5000)
assert len(gui.bec.widget_list()) == 0
def test_ring_bar(qtbot, connected_client_gui_obj):
gui = connected_client_gui_obj
dock_area = gui.bec
d0 = dock_area.new("dock_0")
bar = d0.new("RingProgressBar")
bar = dock_area.new("RingProgressBar")
assert bar.__class__.__name__ == "RPCReference"
assert gui._ipython_registry[bar._gui_id].__class__.__name__ == "RingProgressBar"
@@ -147,14 +122,16 @@ def test_rpc_gui_obj(connected_client_gui_obj, qtbot):
assert gui.windows["bec"] is gui.bec
mw = gui.bec
assert mw.__class__.__name__ == "RPCReference"
assert gui._ipython_registry[mw._gui_id].__class__.__name__ == "BECDockArea"
assert gui._ipython_registry[mw._gui_id].__class__.__name__ == "AdvancedDockArea"
xw = gui.new("X")
xw.delete_all()
assert xw.__class__.__name__ == "RPCReference"
assert gui._ipython_registry[xw._gui_id].__class__.__name__ == "BECDockArea"
assert gui._ipython_registry[xw._gui_id].__class__.__name__ == "AdvancedDockArea"
assert len(gui.windows) == 2
assert gui._gui_is_alive()
qtbot.wait(500)
gui.kill_server()
assert not gui._gui_is_alive()
gui.start(wait=True)
@@ -173,17 +150,7 @@ def test_rpc_gui_obj(connected_client_gui_obj, qtbot):
# communication should work, main dock area should have same id and be visible
yw = gui.new("Y")
yw.delete_all()
assert len(gui.windows) == 2
yw.remove()
assert len(gui.windows) == 1 # only bec is left
def test_rpc_call_with_exception_in_safeslot_error_popup(connected_client_gui_obj, qtbot):
gui = connected_client_gui_obj
gui.bec.new("test")
qtbot.waitUntil(lambda: len(gui.bec.panels) == 1) # test
qtbot.wait(500)
with pytest.raises(ValueError):
gui.bec.new("test")
# time.sleep(0.1)

View File

@@ -22,4 +22,4 @@ def test_ipython_tab_completion(bec_ipython_shell):
_, completer = bec_ipython_shell
assert "gui.bec" in completer.all_completions("gui.")
assert "gui.bec.new" in completer.all_completions("gui.bec.")
assert "gui.bec.panels" in completer.all_completions("gui.bec.pan")
assert "gui.bec.widget_list" in completer.all_completions("gui.bec.widget_")

View File

@@ -11,9 +11,9 @@ from bec_widgets.tests.utils import check_remote_data_size
def test_rpc_waveform1d_custom_curve(qtbot, connected_client_gui_obj):
gui = connected_client_gui_obj
dock = gui.bec
dock_area = gui.bec
wf = dock.new("wf_dock").new("Waveform")
wf = dock_area.new("Waveform")
c1 = wf.plot(x=[1, 2, 3], y=[1, 2, 3])
c1.set_color("red")
@@ -26,13 +26,13 @@ def test_rpc_waveform1d_custom_curve(qtbot, connected_client_gui_obj):
def test_rpc_plotting_shortcuts_init_configs(qtbot, connected_client_gui_obj):
gui = connected_client_gui_obj
dock = gui.bec
dock_area = gui.bec
wf = dock.new("wf_dock").new("Waveform")
im = dock.new("im_dock").new("Image")
mm = dock.new("mm_dock").new("MotorMap")
sw = dock.new("sw_dock").new("ScatterWaveform")
mw = dock.new("mw_dock").new("MultiWaveform")
wf = dock_area.new("Waveform")
im = dock_area.new("Image")
mm = dock_area.new("MotorMap")
sw = dock_area.new("ScatterWaveform")
mw = dock_area.new("MultiWaveform")
c1 = wf.plot(x_name="samx", y_name="bpm4i")
# Adding custom curves, removing one and adding it again should not crash
@@ -42,7 +42,7 @@ def test_rpc_plotting_shortcuts_init_configs(qtbot, connected_client_gui_obj):
c3 = wf.plot(y=[1, 2, 3], x=[1, 2, 3])
assert c3.object_name == "Curve_0"
im_item = im.image(monitor="eiger")
im.image(monitor="eiger")
mm.map(x_name="samx", y_name="samy")
sw.plot(x_name="samx", y_name="samy", z_name="bpm4i")
assert sw.main_curve.object_name == "bpm4i_bpm4i"
@@ -53,7 +53,7 @@ def test_rpc_plotting_shortcuts_init_configs(qtbot, connected_client_gui_obj):
# Adding multiple custom curves sho
# Checking if classes are correctly initialised
assert len(dock.panel_list) == 5
assert len(dock_area.widget_list()) == 5
assert wf.__class__.__name__ == "RPCReference"
assert wf.__class__ == RPCReference
assert gui._ipython_registry[wf._gui_id].__class__ == Waveform
@@ -84,14 +84,14 @@ def test_rpc_plotting_shortcuts_init_configs(qtbot, connected_client_gui_obj):
def test_rpc_waveform_scan(qtbot, bec_client_lib, connected_client_gui_obj):
gui = connected_client_gui_obj
dock = gui.bec
dock_area = gui.bec
client = bec_client_lib
dev = client.device_manager.devices
scans = client.scans
queue = client.queue
wf = dock.new("wf_dock").new("Waveform")
wf = dock_area.new("Waveform")
# add 3 different curves to track
wf.plot(x_name="samx", y_name="bpm4i")
@@ -125,19 +125,18 @@ def test_rpc_waveform_scan(qtbot, bec_client_lib, connected_client_gui_obj):
@pytest.mark.timeout(100)
def test_async_plotting(qtbot, bec_client_lib, connected_client_gui_obj):
gui = connected_client_gui_obj
dock = gui.bec
dock_area = gui.bec
client = bec_client_lib
dev = client.device_manager.devices
scans = client.scans
queue = client.queue
# Test add
dev.waveform.sim.select_model("GaussianModel")
dev.waveform.sim.params = {"amplitude": 1000, "center": 4000, "sigma": 300}
dev.waveform.async_update.set("add").wait()
dev.waveform.waveform_shape.set(10000).wait()
wf = dock.new("wf_dock").new("Waveform")
wf = dock_area.new("Waveform")
curve = wf.plot(y_name="waveform")
status = scans.line_scan(dev.samx, -5, 5, steps=5, exp_time=0.05, relative=False)
@@ -163,14 +162,13 @@ def test_async_plotting(qtbot, bec_client_lib, connected_client_gui_obj):
def test_rpc_image(qtbot, bec_client_lib, connected_client_gui_obj):
gui = connected_client_gui_obj
dock = gui.bec
dock_area = gui.bec
client = bec_client_lib
dev = client.device_manager.devices
scans = client.scans
queue = client.queue
im = dock.new("im_dock").new("Image")
im = dock_area.new("Image")
im.image(monitor="eiger")
status = scans.line_scan(dev.samx, -5, 5, steps=10, exp_time=0.05, relative=False)
@@ -191,8 +189,9 @@ def test_rpc_motor_map(qtbot, bec_client_lib, connected_client_gui_obj):
dev = client.device_manager.devices
scans = client.scans
dock = gui.bec
motor_map = dock.new("mm_dock").new("MotorMap")
dock_area = gui.bec
motor_map = dock_area.new("MotorMap")
motor_map.map(x_name="samx", y_name="samy")
initial_pos_x = dev.samx.read()["samx"]["value"]
@@ -221,8 +220,9 @@ def test_dap_rpc(qtbot, bec_client_lib, connected_client_gui_obj):
dev = client.device_manager.devices
scans = client.scans
dock = gui.bec
wf = dock.new("wf_dock").new("Waveform")
dock_area = gui.bec
wf = dock_area.new("Waveform")
wf.plot(x_name="samx", y_name="bpm4i", dap="GaussianModel")
dev.bpm4i.sim.select_model("GaussianModel")
@@ -262,8 +262,9 @@ def test_waveform_passing_device(qtbot, bec_client_lib, connected_client_gui_obj
dev = client.device_manager.devices
scans = client.scans
dock = gui.bec
wf = dock.new("wf_dock").new("Waveform")
dock_area = gui.bec
wf = dock_area.new("Waveform")
c1 = wf.plot(
y_name=dev.samx, y_entry=dev.samx.setpoint
) # using setpoint to not use readback signal
@@ -303,13 +304,13 @@ def test_rpc_waveform_history_curve(
Note: Parameterization prevents adding the same logical curve twice (which would collide on label).
"""
gui = connected_client_gui_obj
dock = gui.bec
dock_area = gui.bec
client = bec_client_lib
dev = client.device_manager.devices
scans = client.scans
queue = client.queue
wf = dock.new("wf_dock").new("Waveform")
wf = dock_area.new("Waveform")
# Collect references for validation
scan_meta = [] # list of dicts with scan_id, scan_number, data

View File

@@ -9,16 +9,16 @@ from bec_widgets.cli.rpc.rpc_base import RPCReference
def test_rpc_reference_objects(connected_client_gui_obj):
gui = connected_client_gui_obj
dock = gui.window_list[0].new()
plt = dock.new(name="fig", widget="Waveform")
dock_area = gui.window_list[0]
plt = dock_area.new("Waveform", object_name="fig")
plt.plot(x_name="samx", y_name="bpm4i")
im = dock.new("Image")
im = dock_area.new("Image")
im.image("eiger")
motor_map = dock.new("MotorMap")
motor_map = dock_area.new("MotorMap")
motor_map.map("samx", "samy")
plt_z = dock.new("Waveform")
plt_z = dock_area.new("Waveform")
plt_z.plot(x_name="samx", y_name="samy", z_name="bpm4i")
assert len(plt_z.curves) == 1

View File

@@ -1,5 +1,3 @@
from typing import TYPE_CHECKING
import pytest
from bec_widgets.cli.rpc.rpc_base import RPCBase, RPCReference
@@ -64,13 +62,11 @@ def wait_for_namespace_change(
def create_widget(
qtbot, gui: RPCBase, dock_area: RPCReference, widget_cls_name: str
) -> tuple[RPCReference, RPCReference, RPCReference]:
) -> RPCReference:
"""Utility method to create a widget and wait for the namespaces to be created."""
dock = dock_area.new(widget=widget_cls_name)
wait_for_namespace_change(qtbot, gui, dock_area, dock.object_name, dock._gui_id)
widget = dock.element_list[-1]
wait_for_namespace_change(qtbot, gui, dock, widget.object_name, widget._gui_id)
return dock, widget
widget = dock_area.new(widget_cls_name)
wait_for_namespace_change(qtbot, gui, dock_area, widget.object_name, widget._gui_id)
return widget
@pytest.mark.timeout(100)
@@ -106,15 +102,12 @@ def test_available_widgets(qtbot, connected_client_gui_obj):
#############################
# Create widget the widget and wait for the widget to be registered in the ipython registry
dock, widget = create_widget(
qtbot, gui, dock_area, getattr(gui.available_widgets, object_name)
)
widget = create_widget(qtbot, gui, dock_area, getattr(gui.available_widgets, object_name))
# Check that the widget is indeed registered on the server and the client
assert gui._ipython_registry.get(widget._gui_id, None) is not None
assert gui._server_registry.get(widget._gui_id, None) is not None
# Check that namespace was updated
assert hasattr(dock_area, dock.object_name)
assert hasattr(dock, widget.object_name)
assert hasattr(dock_area, widget.object_name)
# Check that no additional top level widgets were created without a parent_id
widgets = [
@@ -129,19 +122,17 @@ def test_available_widgets(qtbot, connected_client_gui_obj):
#############################
# Now we remove the widget again
dock_name = dock.object_name
dock_id = dock._gui_id
widget_id = widget._gui_id
dock_area.delete(dock.object_name)
widget.remove()
# Wait for namespace to change
wait_for_namespace_change(qtbot, gui, dock_area, dock_name, dock_id, exists=False)
# Assert that dock and widget are removed from the ipython registry and the namespace
assert hasattr(dock_area, dock_name) is False
wait_for_namespace_change(
qtbot, gui, dock_area, widget.object_name, widget_id, exists=False
)
# Assert that widget is removed from the ipython registry and the namespace
assert hasattr(dock_area, widget.object_name) is False
# Client registry
assert gui._ipython_registry.get(dock_id, None) is None
assert gui._ipython_registry.get(widget_id, None) is None
# Server registry
assert gui._server_registry.get(dock_id, None) is None
assert gui._server_registry.get(widget_id, None) is None
# Check that the number of top level widgets is still the same. As the cleanup is done by the

View File

@@ -77,6 +77,10 @@ def connected_client_gui_obj(qtbot_scope_module, gui_id, bec_client_lib):
try:
gui.start(wait=True)
qtbot_scope_module.waitUntil(lambda: hasattr(gui, "bec"), timeout=5000)
gui.bec.delete_all() # ensure clean state
qtbot_scope_module.waitUntil(lambda: len(gui.bec.widget_list()) == 0, timeout=10000)
yield gui
finally:
gui.bec.delete_all() # ensure clean state
qtbot_scope_module.waitUntil(lambda: len(gui.bec.widget_list()) == 0, timeout=10000)
gui.kill_server()

View File

@@ -98,20 +98,16 @@ def wait_for_namespace_change(
) from e
def create_widget(
qtbot, gui: BECGuiClient, widget_cls_name: str
) -> tuple[RPCReference, RPCReference]:
def create_widget(qtbot, gui: BECGuiClient, widget_cls_name: str) -> RPCReference:
"""Utility method to create a widget and wait for the namespaces to be created."""
if hasattr(gui, "dock_area"):
dock_area: client.BECDockArea = gui.dock_area
dock_area = gui.dock_area
else:
dock_area: client.BECDockArea = gui.new(name="dock_area")
dock_area = gui.new(name="dock_area")
wait_for_namespace_change(qtbot, gui, gui, dock_area.object_name, dock_area._gui_id)
dock: client.BECDock = dock_area.new()
wait_for_namespace_change(qtbot, gui, dock_area, dock.object_name, dock._gui_id)
widget = dock.new(widget=widget_cls_name)
wait_for_namespace_change(qtbot, gui, dock, widget.object_name, widget._gui_id)
return dock, widget
widget = dock_area.new(widget=widget_cls_name)
wait_for_namespace_change(qtbot, gui, dock_area, widget.object_name, widget._gui_id)
return widget
@pytest.fixture(scope="module")
@@ -133,6 +129,7 @@ def maybe_remove_dock_area(qtbot, gui: BECGuiClient, random_int_gen: random.Rand
# Needed, reference gets deleted in the gui
name = gui.dock_area.object_name
gui_id = gui.dock_area._gui_id
gui.dock_area.delete_all() # start fresh
gui.delete("dock_area")
wait_for_namespace_change(
qtbot, gui=gui, parent_widget=gui, object_name=name, widget_gui_id=gui_id, exists=False
@@ -144,9 +141,8 @@ def test_widgets_e2e_bec_progress_bar(qtbot, connected_client_gui_obj, random_ge
"""Test the BECProgressBar widget."""
gui = connected_client_gui_obj
bec = gui._client
# Create dock_area, dock, widget
dock, widget = create_widget(qtbot, gui, gui.available_widgets.BECProgressBar)
dock: client.BECDock
# Create dock_area and widget
widget = create_widget(qtbot, gui, gui.available_widgets.BECProgressBar)
widget: client.BECProgressBar
# Check rpc calls
@@ -166,9 +162,8 @@ def test_widgets_e2e_bec_queue(qtbot, connected_client_gui_obj, random_generator
"""Test the BECQueue widget."""
gui = connected_client_gui_obj
bec = gui._client
# Create dock_area, dock, widget
dock, widget = create_widget(qtbot, gui, gui.available_widgets.BECQueue)
dock: client.BECDock
# Create dock_area and widget
widget = create_widget(qtbot, gui, gui.available_widgets.BECQueue)
widget: client.BECQueue
# No rpc calls to test so far
@@ -183,8 +178,8 @@ def test_widgets_e2e_bec_status_box(qtbot, connected_client_gui_obj, random_gene
"""Test the BECStatusBox widget."""
gui = connected_client_gui_obj
bec = gui._client
# Create dock_area, dock, widget
dock, widget = create_widget(qtbot, gui, gui.available_widgets.BECStatusBox)
# Create dock_area and widget
widget = create_widget(qtbot, gui, gui.available_widgets.BECStatusBox)
# Check rpc calls
assert widget.get_server_state() in ["RUNNING", "IDLE", "BUSY", "ERROR"]
@@ -198,9 +193,8 @@ def test_widgets_e2e_dap_combo_box(qtbot, connected_client_gui_obj, random_gener
"""Test the DAPComboBox widget."""
gui = connected_client_gui_obj
bec = gui._client
# Create dock_area, dock, widget
dock, widget = create_widget(qtbot, gui, gui.available_widgets.DapComboBox)
dock: client.BECDock
# Create dock_area and widget
widget = create_widget(qtbot, gui, gui.available_widgets.DapComboBox)
widget: client.DAPComboBox
# Check rpc calls
@@ -217,9 +211,8 @@ def test_widgets_e2e_device_browser(qtbot, connected_client_gui_obj, random_gene
"""Test the DeviceBrowser widget."""
gui = connected_client_gui_obj
bec = gui._client
# Create dock_area, dock, widget
dock, widget = create_widget(qtbot, gui, gui.available_widgets.DeviceBrowser)
dock: client.BECDock
# Create dock_area and widget
widget = create_widget(qtbot, gui, gui.available_widgets.DeviceBrowser)
widget: client.DeviceBrowser
# No rpc calls yet to check
@@ -233,9 +226,8 @@ def test_widgets_e2e_device_combo_box(qtbot, connected_client_gui_obj, random_ge
"""Test the DeviceComboBox widget."""
gui = connected_client_gui_obj
bec = gui._client
# Create dock_area, dock, widget
dock, widget = create_widget(qtbot, gui, gui.available_widgets.DeviceComboBox)
dock: client.BECDock
# Create dock_area and widget
widget = create_widget(qtbot, gui, gui.available_widgets.DeviceComboBox)
widget: client.DeviceComboBox
assert "samx" in widget.devices
@@ -252,9 +244,8 @@ def test_widgets_e2e_device_line_edit(qtbot, connected_client_gui_obj, random_ge
"""Test the DeviceLineEdit widget."""
gui = connected_client_gui_obj
bec = gui._client
# Create dock_area, dock, widget
dock, widget = create_widget(qtbot, gui, gui.available_widgets.DeviceLineEdit)
dock: client.BECDock
# Create dock_area and widget
widget = create_widget(qtbot, gui, gui.available_widgets.DeviceLineEdit)
widget: client.DeviceLineEdit
assert widget._is_valid_input is False
@@ -273,9 +264,8 @@ def test_widgets_e2e_signal_line_edit(qtbot, connected_client_gui_obj, random_ge
"""Test the DeviceSignalLineEdit widget."""
gui = connected_client_gui_obj
bec = gui._client
# Create dock_area, dock, widget
dock, widget = create_widget(qtbot, gui, gui.available_widgets.SignalLineEdit)
dock: client.BECDock
# Create dock_area and widget
widget = create_widget(qtbot, gui, gui.available_widgets.SignalLineEdit)
widget: client.SignalLineEdit
widget.set_device("samx")
@@ -300,8 +290,8 @@ def test_widgets_e2e_signal_combobox(qtbot, connected_client_gui_obj, random_gen
"""Test the DeviceSignalComboBox widget."""
gui = connected_client_gui_obj
bec = gui._client
# Create dock_area, dock, widget
_, widget = create_widget(qtbot, gui, gui.available_widgets.SignalComboBox)
# Create dock_area and widget
widget = create_widget(qtbot, gui, gui.available_widgets.SignalComboBox)
widget: client.SignalComboBox
widget.set_device("samx")
@@ -325,9 +315,8 @@ def test_widgets_e2e_image(qtbot, connected_client_gui_obj, random_generator_fro
"""Test the Image widget."""
gui = connected_client_gui_obj
bec = gui._client
# Create dock_area, dock, widget
dock, widget = create_widget(qtbot, gui, gui.available_widgets.Image)
dock: client.BECDock
# Create dock_area and widget
widget = create_widget(qtbot, gui, gui.available_widgets.Image)
widget: client.Image
scans = bec.scans
@@ -369,9 +358,8 @@ def test_widgets_e2e_image(qtbot, connected_client_gui_obj, random_generator_fro
# """Test the LogPanel widget."""
# gui = connected_client_gui_obj
# bec = gui._client
# # Create dock_area, dock, widget
# dock, widget = create_widget(qtbot, gui, gui.available_widgets.LogPanel)
# dock: client.BECDock
# # Create dock_area and widget
# widget = create_widget(qtbot, gui, gui.available_widgets.LogPanel)
# widget: client.LogPanel
# # No rpc calls to check so far
@@ -385,9 +373,8 @@ def test_widgets_e2e_minesweeper(qtbot, connected_client_gui_obj, random_generat
"""Test the MineSweeper widget."""
gui = connected_client_gui_obj
bec = gui._client
# Create dock_area, dock, widget
dock, widget = create_widget(qtbot, gui, gui.available_widgets.Minesweeper)
dock: client.BECDock
# Create dock_area and widget
widget = create_widget(qtbot, gui, gui.available_widgets.Minesweeper)
widget: client.MineSweeper
# No rpc calls to check so far
@@ -401,9 +388,8 @@ def test_widgets_e2e_motor_map(qtbot, connected_client_gui_obj, random_generator
"""Test the MotorMap widget."""
gui = connected_client_gui_obj
bec = gui._client
# Create dock_area, dock, widget
dock, widget = create_widget(qtbot, gui, gui.available_widgets.MotorMap)
dock: client.BECDock
# Create dock_area and widget
widget = create_widget(qtbot, gui, gui.available_widgets.MotorMap)
widget: client.MotorMap
# Test RPC calls
@@ -431,9 +417,8 @@ def test_widgets_e2e_multi_waveform(qtbot, connected_client_gui_obj, random_gene
"""Test MultiWaveform widget."""
gui = connected_client_gui_obj
bec = gui._client
# Create dock_area, dock, widget
dock, widget = create_widget(qtbot, gui, gui.available_widgets.MultiWaveform)
dock: client.BECDock
# Create dock_area and widget
widget = create_widget(qtbot, gui, gui.available_widgets.MultiWaveform)
widget: client.MultiWaveform
# Test RPC calls
@@ -470,9 +455,8 @@ def test_widgets_e2e_positioner_indicator(
"""Test the PositionIndicator widget."""
gui = connected_client_gui_obj
bec = gui._client
# Create dock_area, dock, widget
dock, widget = create_widget(qtbot, gui, gui.available_widgets.PositionIndicator)
dock: client.BECDock
# Create dock_area and widget
widget = create_widget(qtbot, gui, gui.available_widgets.PositionIndicator)
widget: client.PositionIndicator
# TODO check what these rpc calls are supposed to do! Issue created #461
@@ -487,9 +471,8 @@ def test_widgets_e2e_positioner_box(qtbot, connected_client_gui_obj, random_gene
"""Test the PositionerBox widget."""
gui = connected_client_gui_obj
bec = gui._client
# Create dock_area, dock, widget
dock, widget = create_widget(qtbot, gui, gui.available_widgets.PositionerBox)
dock: client.BECDock
# Create dock_area and widget
widget = create_widget(qtbot, gui, gui.available_widgets.PositionerBox)
widget: client.PositionerBox
# Test rpc calls
@@ -510,9 +493,8 @@ def test_widgets_e2e_positioner_box_2d(qtbot, connected_client_gui_obj, random_g
"""Test the PositionerBox2D widget."""
gui = connected_client_gui_obj
bec = gui._client
# Create dock_area, dock, widget
dock, widget = create_widget(qtbot, gui, gui.available_widgets.PositionerBox2D)
dock: client.BECDock
# Create dock_area and widget
widget = create_widget(qtbot, gui, gui.available_widgets.PositionerBox2D)
widget: client.PositionerBox2D
# Test rpc calls
@@ -537,9 +519,8 @@ def test_widgets_e2e_positioner_control_line(
"""Test the positioner control line widget"""
gui = connected_client_gui_obj
bec = gui._client
# Create dock_area, dock, widget
dock, widget = create_widget(qtbot, gui, gui.available_widgets.PositionerControlLine)
dock: client.BECDock
# Create dock_area and widget
widget = create_widget(qtbot, gui, gui.available_widgets.PositionerControlLine)
widget: client.PositionerControlLine
# Test rpc calls
@@ -555,31 +536,31 @@ def test_widgets_e2e_positioner_control_line(
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
@pytest.mark.timeout(PYTEST_TIMEOUT)
def test_widgets_e2e_ring_progress_bar(qtbot, connected_client_gui_obj, random_generator_from_seed):
"""Test the RingProgressBar widget"""
gui = connected_client_gui_obj
bec = gui._client
# Create dock_area, dock, widget
dock, widget = create_widget(qtbot, gui, gui.available_widgets.RingProgressBar)
dock: client.BECDock
widget: client.RingProgressBar
widget.set_number_of_bars(3)
widget.rings[0].set_update("manual")
widget.rings[0].set_value(30)
widget.rings[0].set_min_max_values(0, 100)
widget.rings[1].set_update("scan")
widget.rings[2].set_update("device", device="samx")
# Test rpc calls
dev = bec.device_manager.devices
scans = bec.scans
# Do a scan
scans.line_scan(dev.samx, -3, 3, steps=50, exp_time=0.01, relative=False).wait()
# Test removing the widget, or leaving it open for the next test
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
# TODO passes locally, fails on CI for some reason... -> issue #1003
# @pytest.mark.timeout(PYTEST_TIMEOUT)
# def test_widgets_e2e_ring_progress_bar(qtbot, connected_client_gui_obj, random_generator_from_seed):
# """Test the RingProgressBar widget"""
# gui = connected_client_gui_obj
# bec = gui._client
# # Create dock_area and widget
# widget = create_widget(qtbot, gui, gui.available_widgets.RingProgressBar)
# widget: client.RingProgressBar
#
# widget.set_number_of_bars(3)
# widget.rings[0].set_update("manual")
# widget.rings[0].set_value(30)
# widget.rings[0].set_min_max_values(0, 100)
# widget.rings[1].set_update("scan")
# widget.rings[2].set_update("device", device="samx")
#
# # Test rpc calls
# dev = bec.device_manager.devices
# scans = bec.scans
# # Do a scan
# scans.line_scan(dev.samx, -3, 3, steps=50, exp_time=0.01, relative=False).wait()
#
# # Test removing the widget, or leaving it open for the next test
# maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
@pytest.mark.timeout(PYTEST_TIMEOUT)
@@ -587,9 +568,8 @@ def test_widgets_e2e_scan_control(qtbot, connected_client_gui_obj, random_genera
"""Test the ScanControl widget"""
gui = connected_client_gui_obj
bec = gui._client
# Create dock_area, dock, widget
dock, widget = create_widget(qtbot, gui, gui.available_widgets.ScanControl)
dock: client.BECDock
# Create dock_area and widget
widget = create_widget(qtbot, gui, gui.available_widgets.ScanControl)
widget: client.ScanControl
# No rpc calls to check so far
@@ -603,9 +583,8 @@ def test_widgets_e2e_scatter_waveform(qtbot, connected_client_gui_obj, random_ge
"""Test the ScatterWaveform widget"""
gui = connected_client_gui_obj
bec = gui._client
# Create dock_area, dock, widget
dock, widget = create_widget(qtbot, gui, gui.available_widgets.ScatterWaveform)
dock: client.BECDock
# Create dock_area and widget
widget = create_widget(qtbot, gui, gui.available_widgets.ScatterWaveform)
widget: client.ScatterWaveform
# Test rpc calls
@@ -623,9 +602,8 @@ def test_widgets_e2e_text_box(qtbot, connected_client_gui_obj, random_generator_
"""Test the TextBox widget"""
gui = connected_client_gui_obj
bec = gui._client
# Create dock_area, dock, widget
dock, widget = create_widget(qtbot, gui, gui.available_widgets.TextBox)
dock: client.BECDock
# Create dock_area and widget
widget = create_widget(qtbot, gui, gui.available_widgets.TextBox)
widget: client.TextBox
# RPC calls
@@ -641,9 +619,8 @@ def test_widgets_e2e_waveform(qtbot, connected_client_gui_obj, random_generator_
"""Test the Waveform widget"""
gui = connected_client_gui_obj
bec = gui._client
# Create dock_area, dock, widget
dock, widget = create_widget(qtbot, gui, gui.available_widgets.Waveform)
dock: client.BECDock
# Create dock_area and widget
widget = create_widget(qtbot, gui, gui.available_widgets.Waveform)
widget: client.Waveform
# Test rpc calls

View File

@@ -1,233 +0,0 @@
# pylint: disable=missing-function-docstring, missing-module-docstring, unused-import
from unittest import mock
import pytest
from bec_lib.endpoints import MessageEndpoints
from bec_widgets.widgets.containers.dock import BECDockArea
from .client_mocks import mocked_client
from .test_bec_queue import bec_queue_msg_full
@pytest.fixture
def bec_dock_area(qtbot, mocked_client):
widget = BECDockArea(client=mocked_client)
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
def test_bec_dock_area_init(bec_dock_area):
assert bec_dock_area is not None
assert bec_dock_area.client is not None
assert isinstance(bec_dock_area, BECDockArea)
assert bec_dock_area.config.widget_class == "BECDockArea"
def test_bec_dock_area_add_remove_dock(bec_dock_area, qtbot):
initial_count = len(bec_dock_area.dock_area.docks)
# Adding 3 docks
d0 = bec_dock_area.new()
d1 = bec_dock_area.new()
d2 = bec_dock_area.new()
# Check if the docks were added
assert len(bec_dock_area.dock_area.docks) == initial_count + 3
assert d0.name() in dict(bec_dock_area.dock_area.docks)
assert d1.name() in dict(bec_dock_area.dock_area.docks)
assert d2.name() in dict(bec_dock_area.dock_area.docks)
assert bec_dock_area.dock_area.docks[d0.name()].config.widget_class == "BECDock"
assert bec_dock_area.dock_area.docks[d1.name()].config.widget_class == "BECDock"
assert bec_dock_area.dock_area.docks[d2.name()].config.widget_class == "BECDock"
# Check panels API for getting docks to CLI
assert bec_dock_area.panels == dict(bec_dock_area.dock_area.docks)
# Remove docks
d0_name = d0.name()
bec_dock_area.delete(d0_name)
d1.remove()
qtbot.waitUntil(lambda: len(bec_dock_area.dock_area.docks) == initial_count + 1, timeout=200)
assert d0.name() not in dict(bec_dock_area.dock_area.docks)
assert d1.name() not in dict(bec_dock_area.dock_area.docks)
assert d2.name() in dict(bec_dock_area.dock_area.docks)
def test_close_docks(bec_dock_area, qtbot):
_ = bec_dock_area.new(name="dock_0")
_ = bec_dock_area.new(name="dock_1")
_ = bec_dock_area.new(name="dock_2")
bec_dock_area.delete_all()
qtbot.waitUntil(lambda: len(bec_dock_area.dock_area.docks) == 0)
def test_undock_and_dock_docks(bec_dock_area, qtbot):
d0 = bec_dock_area.new(name="dock_0")
d1 = bec_dock_area.new(name="dock_1")
d2 = bec_dock_area.new(name="dock_4")
d3 = bec_dock_area.new(name="dock_3")
d0.detach()
bec_dock_area.detach_dock("dock_1")
d2.detach()
assert len(bec_dock_area.dock_area.docks) == 4
assert len(bec_dock_area.dock_area.tempAreas) == 3
d0.attach()
assert len(bec_dock_area.dock_area.docks) == 4
assert len(bec_dock_area.dock_area.tempAreas) == 2
bec_dock_area.attach_all()
assert len(bec_dock_area.dock_area.docks) == 4
assert len(bec_dock_area.dock_area.tempAreas) == 0
def test_new_dock_raises_for_invalid_name(bec_dock_area):
with pytest.raises(ValueError):
bec_dock_area.new(
name="new", _override_slot_params={"popup_error": False, "raise_error": True}
)
###################################
# Toolbar Actions
###################################
def test_toolbar_add_plot_waveform(bec_dock_area):
bec_dock_area.toolbar.components.get_action("menu_plots").actions["waveform"].action.trigger()
assert "waveform_0" in bec_dock_area.panels
assert bec_dock_area.panels["waveform_0"].widgets[0].config.widget_class == "Waveform"
def test_toolbar_add_plot_scatter_waveform(bec_dock_area):
bec_dock_area.toolbar.components.get_action("menu_plots").actions[
"scatter_waveform"
].action.trigger()
assert "scatter_waveform_0" in bec_dock_area.panels
assert (
bec_dock_area.panels["scatter_waveform_0"].widgets[0].config.widget_class
== "ScatterWaveform"
)
def test_toolbar_add_plot_image(bec_dock_area):
bec_dock_area.toolbar.components.get_action("menu_plots").actions["image"].action.trigger()
assert "image_0" in bec_dock_area.panels
assert bec_dock_area.panels["image_0"].widgets[0].config.widget_class == "Image"
def test_toolbar_add_plot_motor_map(bec_dock_area):
bec_dock_area.toolbar.components.get_action("menu_plots").actions["motor_map"].action.trigger()
assert "motor_map_0" in bec_dock_area.panels
assert bec_dock_area.panels["motor_map_0"].widgets[0].config.widget_class == "MotorMap"
def test_toolbar_add_multi_waveform(bec_dock_area):
bec_dock_area.toolbar.components.get_action("menu_plots").actions[
"multi_waveform"
].action.trigger()
# Check if the MultiWaveform panel is created
assert "multi_waveform_0" in bec_dock_area.panels
assert (
bec_dock_area.panels["multi_waveform_0"].widgets[0].config.widget_class == "MultiWaveform"
)
def test_toolbar_add_device_positioner_box(bec_dock_area):
bec_dock_area.toolbar.components.get_action("menu_devices").actions[
"positioner_box"
].action.trigger()
assert "positioner_box_0" in bec_dock_area.panels
assert (
bec_dock_area.panels["positioner_box_0"].widgets[0].config.widget_class == "PositionerBox"
)
def test_toolbar_add_utils_queue(bec_dock_area, bec_queue_msg_full):
bec_dock_area.client.connector.set_and_publish(
MessageEndpoints.scan_queue_status(), bec_queue_msg_full
)
bec_dock_area.toolbar.components.get_action("menu_utils").actions["queue"].action.trigger()
assert "bec_queue_0" in bec_dock_area.panels
assert bec_dock_area.panels["bec_queue_0"].widgets[0].config.widget_class == "BECQueue"
def test_toolbar_add_utils_status(bec_dock_area):
bec_dock_area.toolbar.components.get_action("menu_utils").actions["status"].action.trigger()
assert "bec_status_box_0" in bec_dock_area.panels
assert bec_dock_area.panels["bec_status_box_0"].widgets[0].config.widget_class == "BECStatusBox"
def test_toolbar_add_utils_progress_bar(bec_dock_area):
bec_dock_area.toolbar.components.get_action("menu_utils").actions[
"progress_bar"
].action.trigger()
assert "ring_progress_bar_0" in bec_dock_area.panels
assert (
bec_dock_area.panels["ring_progress_bar_0"].widgets[0].config.widget_class
== "RingProgressBar"
)
def test_toolbar_screenshot_action(bec_dock_area, tmpdir):
"""Test the screenshot functionality from the toolbar."""
# Create a test screenshot file path in tmpdir
screenshot_path = tmpdir.join("test_screenshot.png")
# Mock the QFileDialog.getSaveFileName to return a test filename
with mock.patch("bec_widgets.utils.bec_widget.QFileDialog.getSaveFileName") as mock_dialog:
mock_dialog.return_value = (str(screenshot_path), "PNG Files (*.png)")
# Mock the screenshot.save method
with mock.patch.object(bec_dock_area, "grab") as mock_grab:
mock_screenshot = mock.MagicMock()
mock_grab.return_value = mock_screenshot
# Trigger the screenshot action
bec_dock_area.toolbar.components.get_action("screenshot").action.trigger()
# Verify the dialog was called with correct parameters
mock_dialog.assert_called_once()
call_args = mock_dialog.call_args[0]
assert call_args[0] == bec_dock_area # parent widget
assert call_args[1] == "Save Screenshot" # dialog title
assert call_args[2].startswith("bec_") # filename starts with bec_
assert call_args[2].endswith(".png") # filename ends with .png
assert (
call_args[3] == "PNG Files (*.png);;JPEG Files (*.jpg *.jpeg);;All Files (*)"
) # file filter
# Verify grab was called
mock_grab.assert_called_once()
# Verify save was called with the filename
mock_screenshot.save.assert_called_once_with(str(screenshot_path))
def test_toolbar_screenshot_action_cancelled(bec_dock_area):
"""Test the screenshot functionality when user cancels the dialog."""
# Mock the QFileDialog.getSaveFileName to return empty filename (cancelled)
with mock.patch("bec_widgets.utils.bec_widget.QFileDialog.getSaveFileName") as mock_dialog:
mock_dialog.return_value = ("", "")
# Mock the screenshot.save method
with mock.patch.object(bec_dock_area, "grab") as mock_grab:
mock_screenshot = mock.MagicMock()
mock_grab.return_value = mock_screenshot
# Trigger the screenshot action
bec_dock_area.toolbar.components.get_action("screenshot").action.trigger()
# Verify the dialog was called
mock_dialog.assert_called_once()
# Verify grab was called (screenshot is taken before dialog)
mock_grab.assert_called_once()
# Verify save was NOT called since dialog was cancelled
mock_screenshot.save.assert_not_called()

View File

@@ -1,6 +1,5 @@
import enum
import inspect
import sys
from importlib import reload
from types import SimpleNamespace
from unittest.mock import MagicMock, call, patch
@@ -59,4 +58,4 @@ def test_duplicate_plugins_not_allowed(_, bec_logger: MagicMock):
)
in bec_logger.logger.warning.mock_calls
)
assert client.BECDock is not _TestDuplicatePlugin
assert client.Waveform is not _TestDuplicatePlugin

View File

@@ -3,13 +3,13 @@ from unittest import mock
import pytest
from bec_widgets.cli.client import BECDockArea
from bec_widgets.cli.client import AdvancedDockArea
from bec_widgets.cli.client_utils import BECGuiClient, _start_plot_process
@pytest.fixture
def cli_dock_area():
dock_area = BECDockArea(gui_id="test")
dock_area = AdvancedDockArea(gui_id="test")
with mock.patch.object(dock_area, "_run_rpc") as mock_rpc_call:
with mock.patch.object(dock_area, "_gui_is_alive", return_value=True):
yield dock_area, mock_rpc_call
@@ -31,13 +31,13 @@ def test_rpc_call_new_dock(cli_dock_area):
)
def test_client_utils_start_plot_process(config, call_config):
with mock.patch("bec_widgets.cli.client_utils.subprocess.Popen") as mock_popen:
_start_plot_process("gui_id", "bec", config, gui_class="BECDockArea")
_start_plot_process("gui_id", "bec", config, gui_class="AdvancedDockArea")
command = [
"bec-gui-server",
"--id",
"gui_id",
"--gui_class",
"BECDockArea",
"AdvancedDockArea",
"--gui_class_id",
"bec",
"--hide",

View File

@@ -8,5 +8,5 @@ def test_client_generator_classes():
assert "Image" in connector_cls_names
assert "Waveform" in connector_cls_names
assert "BECDockArea" in plugins
assert "MotorMap" in plugins
assert "NonExisting" not in plugins

View File

@@ -1,20 +1,15 @@
import enum
from importlib import reload
from types import SimpleNamespace
from unittest.mock import MagicMock, call, patch
from unittest.mock import patch
from bec_widgets.cli import client
from bec_widgets.cli.rpc.rpc_base import RPCBase
from bec_widgets.cli.rpc.rpc_widget_handler import RPCWidgetHandler
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.plugin_utils import BECClassContainer, BECClassInfo
from bec_widgets.widgets.containers.dock.dock import BECDock
def test_rpc_widget_handler():
handler = RPCWidgetHandler()
assert "Image" in handler.widget_classes
assert "RingProgressBar" in handler.widget_classes
assert "AdvancedDockArea" in handler.widget_classes
class _TestPluginWidget(BECWidget): ...