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:
@@ -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:
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
from .dock import BECDock
|
||||
from .dock_area import BECDockArea
|
||||
@@ -1 +0,0 @@
|
||||
{'files': ['dock_area.py']}
|
||||
@@ -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()
|
||||
@@ -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_())
|
||||
@@ -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_())
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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_")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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): ...
|
||||
|
||||
Reference in New Issue
Block a user