diff --git a/bec_widgets/applications/bw_launch.py b/bec_widgets/applications/bw_launch.py index 33500a1d..3872532a 100644 --- a/bec_widgets/applications/bw_launch.py +++ b/bec_widgets/applications/bw_launch.py @@ -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: diff --git a/bec_widgets/applications/launch_window.py b/bec_widgets/applications/launch_window.py index 05c4f6d9..1a458abb 100644 --- a/bec_widgets/applications/launch_window.py +++ b/bec_widgets/applications/launch_window.py @@ -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()) diff --git a/bec_widgets/cli/client.py b/bec_widgets/cli/client.py index 46797a0b..82cfada8 100644 --- a/bec_widgets/cli/client.py +++ b/bec_widgets/cli/client.py @@ -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): diff --git a/bec_widgets/examples/jupyter_console/jupyter_console_window.py b/bec_widgets/examples/jupyter_console/jupyter_console_window.py index ed0dc035..5cb0fefd 100644 --- a/bec_widgets/examples/jupyter_console/jupyter_console_window.py +++ b/bec_widgets/examples/jupyter_console/jupyter_console_window.py @@ -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() diff --git a/bec_widgets/utils/bec_connector.py b/bec_widgets/utils/bec_connector.py index 9c820c7a..6b7ef0c5 100644 --- a/bec_widgets/utils/bec_connector.py +++ b/bec_widgets/utils/bec_connector.py @@ -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 diff --git a/bec_widgets/utils/bec_widget.py b/bec_widgets/utils/bec_widget.py index dd94d0c8..5b6bb392 100644 --- a/bec_widgets/utils/bec_widget.py +++ b/bec_widgets/utils/bec_widget.py @@ -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: diff --git a/bec_widgets/widgets/containers/advanced_dock_area/advanced_dock_area.py b/bec_widgets/widgets/containers/advanced_dock_area/advanced_dock_area.py index 5c223e1b..a112d26d 100644 --- a/bec_widgets/widgets/containers/advanced_dock_area/advanced_dock_area.py +++ b/bec_widgets/widgets/containers/advanced_dock_area/advanced_dock_area.py @@ -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( diff --git a/bec_widgets/widgets/containers/auto_update/auto_updates.py b/bec_widgets/widgets/containers/auto_update/auto_updates.py index ed477726..9f75c799 100644 --- a/bec_widgets/widgets/containers/auto_update/auto_updates.py +++ b/bec_widgets/widgets/containers/auto_update/auto_updates.py @@ -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( diff --git a/bec_widgets/widgets/containers/dock/__init__.py b/bec_widgets/widgets/containers/dock/__init__.py deleted file mode 100644 index d83dbe5f..00000000 --- a/bec_widgets/widgets/containers/dock/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .dock import BECDock -from .dock_area import BECDockArea diff --git a/bec_widgets/widgets/containers/dock/bec_dock_area.pyproject b/bec_widgets/widgets/containers/dock/bec_dock_area.pyproject deleted file mode 100644 index e12ce031..00000000 --- a/bec_widgets/widgets/containers/dock/bec_dock_area.pyproject +++ /dev/null @@ -1 +0,0 @@ -{'files': ['dock_area.py']} \ No newline at end of file diff --git a/bec_widgets/widgets/containers/dock/bec_dock_area_plugin.py b/bec_widgets/widgets/containers/dock/bec_dock_area_plugin.py deleted file mode 100644 index fb507cc6..00000000 --- a/bec_widgets/widgets/containers/dock/bec_dock_area_plugin.py +++ /dev/null @@ -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 = """ - - - - -""" - - -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() diff --git a/bec_widgets/widgets/containers/dock/dock.py b/bec_widgets/widgets/containers/dock/dock.py deleted file mode 100644 index 07f81a45..00000000 --- a/bec_widgets/widgets/containers/dock/dock.py +++ /dev/null @@ -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_()) diff --git a/bec_widgets/widgets/containers/dock/dock_area.py b/bec_widgets/widgets/containers/dock/dock_area.py deleted file mode 100644 index 50210b24..00000000 --- a/bec_widgets/widgets/containers/dock/dock_area.py +++ /dev/null @@ -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_()) diff --git a/bec_widgets/widgets/containers/dock/register_bec_dock_area.py b/bec_widgets/widgets/containers/dock/register_bec_dock_area.py deleted file mode 100644 index 2c33f79d..00000000 --- a/bec_widgets/widgets/containers/dock/register_bec_dock_area.py +++ /dev/null @@ -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() diff --git a/bec_widgets/widgets/services/device_browser/device_item/device_signal_display.py b/bec_widgets/widgets/services/device_browser/device_item/device_signal_display.py index 392c0a6d..d783a215 100644 --- a/bec_widgets/widgets/services/device_browser/device_item/device_signal_display.py +++ b/bec_widgets/widgets/services/device_browser/device_item/device_signal_display.py @@ -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) diff --git a/tests/end-2-end/conftest.py b/tests/end-2-end/conftest.py index 7b35c984..dc4a123d 100644 --- a/tests/end-2-end/conftest.py +++ b/tests/end-2-end/conftest.py @@ -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() diff --git a/tests/end-2-end/test_bec_dock_rpc_e2e.py b/tests/end-2-end/test_bec_dock_rpc_e2e.py index a6d00d73..180d4806 100644 --- a/tests/end-2-end/test_bec_dock_rpc_e2e.py +++ b/tests/end-2-end/test_bec_dock_rpc_e2e.py @@ -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) diff --git a/tests/end-2-end/test_bec_gui_ipython.py b/tests/end-2-end/test_bec_gui_ipython.py index 8ec35282..84493af7 100644 --- a/tests/end-2-end/test_bec_gui_ipython.py +++ b/tests/end-2-end/test_bec_gui_ipython.py @@ -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_") diff --git a/tests/end-2-end/test_plotting_framework_e2e.py b/tests/end-2-end/test_plotting_framework_e2e.py index ebed9f25..f453f627 100644 --- a/tests/end-2-end/test_plotting_framework_e2e.py +++ b/tests/end-2-end/test_plotting_framework_e2e.py @@ -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 diff --git a/tests/end-2-end/test_rpc_register_e2e.py b/tests/end-2-end/test_rpc_register_e2e.py index 2f8d8f37..5eb52198 100644 --- a/tests/end-2-end/test_rpc_register_e2e.py +++ b/tests/end-2-end/test_rpc_register_e2e.py @@ -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 diff --git a/tests/end-2-end/test_rpc_widgets_e2e.py b/tests/end-2-end/test_rpc_widgets_e2e.py index b513068d..185adfd5 100644 --- a/tests/end-2-end/test_rpc_widgets_e2e.py +++ b/tests/end-2-end/test_rpc_widgets_e2e.py @@ -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 diff --git a/tests/end-2-end/user_interaction/conftest.py b/tests/end-2-end/user_interaction/conftest.py index f34e7f66..f7de1d08 100644 --- a/tests/end-2-end/user_interaction/conftest.py +++ b/tests/end-2-end/user_interaction/conftest.py @@ -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() diff --git a/tests/end-2-end/user_interaction/test_user_interaction_e2e.py b/tests/end-2-end/user_interaction/test_user_interaction_e2e.py index 2ae82b98..076b32d9 100644 --- a/tests/end-2-end/user_interaction/test_user_interaction_e2e.py +++ b/tests/end-2-end/user_interaction/test_user_interaction_e2e.py @@ -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 diff --git a/tests/unit_tests/test_bec_dock.py b/tests/unit_tests/test_bec_dock.py deleted file mode 100644 index 2f117ae3..00000000 --- a/tests/unit_tests/test_bec_dock.py +++ /dev/null @@ -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() diff --git a/tests/unit_tests/test_client_plugin_widgets.py b/tests/unit_tests/test_client_plugin_widgets.py index 161fe2d8..d438031f 100644 --- a/tests/unit_tests/test_client_plugin_widgets.py +++ b/tests/unit_tests/test_client_plugin_widgets.py @@ -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 diff --git a/tests/unit_tests/test_client_utils.py b/tests/unit_tests/test_client_utils.py index 05b7d61e..058ccdc7 100644 --- a/tests/unit_tests/test_client_utils.py +++ b/tests/unit_tests/test_client_utils.py @@ -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", diff --git a/tests/unit_tests/test_plugin_utils.py b/tests/unit_tests/test_plugin_utils.py index 5650b531..ef9c456e 100644 --- a/tests/unit_tests/test_plugin_utils.py +++ b/tests/unit_tests/test_plugin_utils.py @@ -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 diff --git a/tests/unit_tests/test_rpc_widget_handler.py b/tests/unit_tests/test_rpc_widget_handler.py index 1f2fc768..558e44e5 100644 --- a/tests/unit_tests/test_rpc_widget_handler.py +++ b/tests/unit_tests/test_rpc_widget_handler.py @@ -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): ...