0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-14 11:41:49 +02:00

fix(widgets/dock): BECDockArea close overwrites the default pyqtgraph Container close + minor improvements

This commit is contained in:
2024-05-01 22:11:06 +02:00
parent fcd6ef0975
commit ceae979f37
24 changed files with 930 additions and 180 deletions

View File

@ -1,2 +1,2 @@
from .auto_updates import AutoUpdates, ScanInfo
from .client import BECFigure
from .client import BECDockArea, BECFigure

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

@ -2,7 +2,7 @@
from typing import Literal, Optional, overload
from bec_widgets.cli.client_utils import BECFigureClientMixin, RPCBase, rpc_call
from bec_widgets.cli.client_utils import BECGuiClientMixin, RPCBase, rpc_call
class BECPlotBase(RPCBase):
@ -393,7 +393,7 @@ class BECWaveform(RPCBase):
"""
class BECFigure(RPCBase, BECFigureClientMixin):
class BECFigure(RPCBase):
@property
@rpc_call
def rpc_id(self) -> "str":
@ -426,7 +426,9 @@ class BECFigure(RPCBase, BECFigureClientMixin):
@rpc_call
def widgets(self) -> "dict":
"""
None
All widgets within the figure with gui ids as keys.
Returns:
dict: All widgets within the figure.
"""
@rpc_call
@ -1310,31 +1312,160 @@ class BECMotorMap(RPCBase):
class BECDock(RPCBase):
@rpc_call
def add_widget(self, widget: "QWidget", row=None, col=0, rowspan=1, colspan=1):
"""
None
"""
@property
@rpc_call
def widget_list(self) -> "list":
"""
Get the widgets in the dock.
Returns:
widgets(list): The widgets in the dock.
"""
@rpc_call
def show_title_bar(self):
"""
Hide the title bar of the dock.
"""
@rpc_call
def hide_title_bar(self):
"""
Hide the title bar of the dock.
"""
@rpc_call
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}
"""
@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 add_widget_bec(
self,
widget_type: "str",
row=None,
col=0,
rowspan=1,
colspan=1,
shift: "Literal['down', 'up', 'left', 'right']" = "down",
):
"""
Add a widget to the dock.
Args:
widget_type(str): The widget to add. Only BEC RPC widgets from RPCWidgetHandler are allowed.
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 list_eligible_widgets(self) -> "list":
"""
List all widgets that can be added to the dock.
Returns:
list: The list of eligible widgets.
"""
@rpc_call
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.
"""
@rpc_call
def remove_widget(self, widget: "QWidget"):
"""
Remove a widget from the dock.
Args:
widget(QWidget): The widget to remove.
"""
@rpc_call
def remove(self):
"""
Remove the dock from the parent dock area.
"""
@rpc_call
def attach(self):
"""
None
"""
@rpc_call
def detach(self):
"""
None
"""
class BECDockArea(RPCBase):
class BECDockArea(RPCBase, BECGuiClientMixin):
@property
@rpc_call
def panels(self) -> "dict":
"""
Get the docks in the dock area.
Returns:
dock_dict(dict): The docks in 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_call
def remove_dock(self, name: "str"):
"""
Remove a dock by name and ensure it is properly closed and cleaned up.
Args:
name(str): The name of the dock to remove.
"""
@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.
"""
@rpc_call
def add_dock(
self,
name: "str" = None,
position: "Literal['bottom', 'top', 'left', 'right', 'above', 'below']" = None,
relative_to: "Optional[BECDock]" = None,
closable: "bool" = False,
prefix: "str" = "dock",
widget: "QWidget" = None,
row: "int" = None,
col: "int" = None,
col: "int" = 0,
rowspan: "int" = 1,
colspan: "int" = 1,
) -> "BECDock":
@ -1345,32 +1476,42 @@ class BECDockArea(RPCBase):
name(str): The name of the dock to be displayed and for further references. Has to be unique.
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.
prefix(str): The prefix for the dock name if no name is provided.
widget(QWidget): The widget to be added to the dock.
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 remove_dock_by_id(self, dock_id: "str"):
"""
None
"""
@rpc_call
def clear_all(self):
"""
None
Close all docks and remove all temp areas.
"""
@property
@rpc_call
def dock_dict(self) -> "dict":
def detach_dock(self, dock_name: "str") -> "BECDock":
"""
None
Undock a dock from the dock area.
Args:
dock(BECDock): 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 get_all_rpc(self) -> "dict":
"""
Get all registered RPC objects.
"""

View File

@ -22,7 +22,7 @@ from bec_widgets.cli.auto_updates import AutoUpdates
from bec_widgets.utils.bec_dispatcher import BECDispatcher
if TYPE_CHECKING:
from bec_widgets.cli.client import BECFigure
from bec_widgets.cli.client import BECDockArea, BECFigure
def rpc_call(func):
@ -56,7 +56,7 @@ def rpc_call(func):
return wrapper
class BECFigureClientMixin:
class BECGuiClientMixin:
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
self._process = None
@ -94,7 +94,7 @@ class BECFigureClientMixin:
)
@staticmethod
def _handle_msg_update(msg: MessageObject, parent: BECFigureClientMixin) -> None:
def _handle_msg_update(msg: MessageObject, parent: BECGuiClientMixin) -> None:
if parent.update_script is not None:
# pylint: disable=protected-access
parent._update_script_msg_parser(msg.value)
@ -139,8 +139,19 @@ class BECFigureClientMixin:
config = self._client._service_config.redis
monitor_module = importlib.import_module("bec_widgets.cli.server")
monitor_path = monitor_module.__file__
gui_class = self.__class__.__name__
command = [sys.executable, "-u", monitor_path, "--id", self._gui_id, "--config", config]
command = [
sys.executable,
"-u",
monitor_path,
"--id",
self._gui_id,
"--config",
config,
"--gui_class",
gui_class,
]
self._process = subprocess.Popen(
command, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)

View File

@ -22,7 +22,7 @@ else:
class ClientGenerator:
def __init__(self):
self.header = """# This file was automatically generated by generate_cli.py\n
from bec_widgets.cli.client_utils import rpc_call, RPCBase, BECFigureClientMixin
from bec_widgets.cli.client_utils import rpc_call, RPCBase, BECGuiClientMixin
from typing import Literal, Optional, overload"""
self.content = ""
@ -53,9 +53,9 @@ from typing import Literal, Optional, overload"""
# from {module} import {class_name}"""
# Generate the content
if cls.__name__ == "BECFigure":
if cls.__name__ == "BECDockArea":
self.content += f"""
class {class_name}(RPCBase, BECFigureClientMixin):"""
class {class_name}(RPCBase, BECGuiClientMixin):"""
else:
self.content += f"""
class {class_name}(RPCBase):"""

View File

@ -0,0 +1,26 @@
from bec_widgets.utils import BECConnector
from bec_widgets.widgets.figure import BECFigure
class RPCWidgetHandler:
"""Handler class for creating widgets from RPC messages."""
widget_classes = {
"BECFigure": BECFigure,
}
@staticmethod
def create_widget(widget_type, **kwargs) -> BECConnector:
"""
Create a widget from an RPC message.
Args:
widget_type(str): The type of the widget.
**kwargs: The keyword arguments for the widget.
Returns:
widget(BECConnector): The created widget.
"""
widget_class = RPCWidgetHandler.widget_classes.get(widget_type)
if widget_class:
return widget_class(**kwargs)
raise ValueError(f"Unknown widget type: {widget_type}")

View File

@ -1,7 +1,7 @@
import inspect
import threading
import time
from typing import Literal
from typing import Literal, Union
from bec_lib import MessageEndpoints, messages
from qtpy.QtCore import QTimer
@ -23,7 +23,7 @@ class BECWidgetsCLIServer:
dispatcher: BECDispatcher = None,
client=None,
config=None,
gui_class: BECFigure | BECDockArea = BECFigure,
gui_class: Union["BECFigure", "BECDockArea"] = BECFigure,
) -> None:
self.dispatcher = BECDispatcher(config=config) if dispatcher is None else dispatcher
self.client = self.dispatcher.client if client is None else client
@ -109,7 +109,7 @@ class BECWidgetsCLIServer:
expire=10,
)
def shutdown(self):
def shutdown(self): # TODO not sure if needed when cleanup is done at level of BECConnector
self._shutdown_event = True
self._heartbeat_timer.stop()
self.client.shutdown()
@ -117,6 +117,7 @@ class BECWidgetsCLIServer:
if __name__ == "__main__": # pragma: no cover
import argparse
import os
import sys
from qtpy.QtCore import QSize
@ -125,8 +126,9 @@ if __name__ == "__main__": # pragma: no cover
app = QApplication(sys.argv)
app.setApplicationName("BEC Figure")
current_path = os.path.dirname(__file__)
icon = QIcon()
icon.addFile("bec_widgets_icon.png", size=QSize(48, 48))
icon.addFile(os.path.join(current_path, "bec_widgets_icon.png"), size=QSize(48, 48))
app.setWindowIcon(icon)
win = QMainWindow()
@ -155,10 +157,10 @@ if __name__ == "__main__": # pragma: no cover
gui_class = BECFigure
server = BECWidgetsCLIServer(gui_id=args.id, config=args.config, gui_class=gui_class)
# server = BECWidgetsCLIServer(gui_id="test", config=args.config, gui_class=gui_class)
fig = server.gui
win.setCentralWidget(fig)
gui = server.gui
win.setCentralWidget(gui)
win.resize(800, 600)
win.show()
app.aboutToQuit.connect(server.shutdown)

View File

@ -49,7 +49,7 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
self.register = RPCRegister()
self.register.add_rpc(self.figure)
print("Registered objects:", dict(self.register.list_all_connections()))
# console push
self.console.kernel_manager.kernel.shell.push(
{
@ -62,6 +62,9 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
"d1": self.d1,
"d2": self.d2,
"d3": self.d3,
"b2a": self.button_2_a,
"b2b": self.button_2_b,
"b2c": self.button_2_c,
"bec": self.figure.client,
"scans": self.figure.client.scans,
"dev": self.figure.client.device_manager.devices,
@ -107,6 +110,9 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
def _init_dock(self):
self.button_1 = QtWidgets.QPushButton("Button 1 ")
self.button_2_a = QtWidgets.QPushButton("Button to be added at place 0,0 in d3")
self.button_2_b = QtWidgets.QPushButton("button after without postions specified")
self.button_2_c = QtWidgets.QPushButton("button super late")
self.button_3 = QtWidgets.QPushButton("Button above Figure ")
self.label_1 = QtWidgets.QLabel("some scan info label with useful information")
@ -123,6 +129,15 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
self.d3.add_widget(self.button_3)
self.d3.add_widget(self.fig_dock3)
self.dock.save_state()
def closeEvent(self, event):
"""Override to handle things when main window is closed."""
self.dock.cleanup()
self.figure.clear_all()
self.figure.client.shutdown()
super().closeEvent(event)
if __name__ == "__main__": # pragma: no cover
import sys
@ -140,4 +155,5 @@ if __name__ == "__main__": # pragma: no cover
win = JupyterConsoleWindow()
win.show()
app.aboutToQuit.connect(win.close)
sys.exit(app.exec_())

View File

@ -148,6 +148,14 @@ class BECConnector:
else:
return self.config
def closeEvent(self, event):
def cleanup(self):
"""Cleanup the widget."""
self.rpc_register.remove_rpc(self)
all_connections = self.rpc_register.list_all_connections()
if len(all_connections) == 0:
print("No more connections. Shutting down GUI BEC client.")
self.client.shutdown()
super().closeEvent(event)
# def closeEvent(self, event):
# self.cleanup()
# super().closeEvent(event)

View File

@ -1,13 +1,18 @@
from __future__ import annotations
from typing import Literal, Optional
from typing import TYPE_CHECKING, Literal, Optional
from pydantic import Field
from pyqtgraph.dockarea import Dock
from qtpy.QtWidgets import QWidget
from bec_widgets.cli.rpc_wigdet_handler import RPCWidgetHandler
from bec_widgets.utils import BECConnector, ConnectionConfig, GridLayoutManager
if TYPE_CHECKING:
from qtpy.QtWidgets import QWidget
from bec_widgets.widgets import BECDockArea
class DockConfig(ConnectionConfig):
widgets: dict[str, ConnectionConfig] = Field({}, description="The widgets in the dock.")
@ -20,18 +25,30 @@ class DockConfig(ConnectionConfig):
class BECDock(BECConnector, Dock):
USER_ACCESS = ["add_widget", "widget_list"]
USER_ACCESS = [
"rpc_id",
"widget_list",
"show_title_bar",
"hide_title_bar",
"get_widgets_positions",
"set_title",
"add_widget_bec",
"list_eligible_widgets",
"move_widget",
"remove_widget",
"remove",
"attach",
"detach",
]
def __init__(
self,
parent: Optional[QWidget] = None,
parent_dock_area: Optional["BECDockArea"] = None,
config: Optional[
DockConfig
] = None, # TODO ATM connection config -> will be changed when I will know what I want to use there
name: Optional[str] = None,
parent: QWidget | None = None,
parent_dock_area: BECDockArea | None = None,
config: DockConfig | None = None,
name: str | None = None,
client=None,
gui_id: Optional[str] = None,
gui_id: str | None = None,
**kwargs,
) -> None:
if config is None:
@ -47,23 +64,124 @@ class BECDock(BECConnector, Dock):
self.parent_dock_area = parent_dock_area
# Signals
self.sigClosed.connect(self._remove_from_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.parent_dock_area.tempAreas and old_area != self.parent_dock_area:
self.parent_dock_area.removeTempArea(old_area)
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 widget_list(self) -> list:
"""
Get the widgets in the dock.
Returns:
widgets(list): The widgets in the dock.
"""
return self.widgets
@widget_list.setter
def widget_list(self, value: list):
self.widgets = value
def get_widgets_positions(self):
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_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.parent_dock_area.docks[title] = self.parent_dock_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 list_eligible_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(RPCWidgetHandler.widget_classes.keys())
def add_widget_bec(
self,
widget_type: str,
row=None,
col=0,
rowspan=1,
colspan=1,
shift: Literal["down", "up", "left", "right"] = "down",
):
"""
Add a widget to the dock.
Args:
widget_type(str): The widget to add. Only BEC RPC widgets from RPCWidgetHandler are allowed.
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 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)
widget = RPCWidgetHandler.create_widget(widget_type)
self.addWidget(widget, row=row, col=col, rowspan=rowspan, colspan=colspan)
return widget
def add_widget(
self,
widget: QWidget,
@ -73,6 +191,17 @@ class BECDock(BECConnector, Dock):
colspan=1,
shift: Literal["down", "up", "left", "right"] = "down",
):
"""
Add a widget to the dock.
Args:
widget(QWidget): The widget to add.
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 row is None:
row = self.layout.rowCount()
@ -81,6 +210,60 @@ class BECDock(BECConnector, Dock):
self.addWidget(widget, row=row, col=col, rowspan=rowspan, colspan=colspan)
def _remove_from_dock_area(self):
"""Remove this dock from the DockArea it lives inside."""
self.parent_dock_area.docks.pop(self.name())
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.removeTempArea(self.area)
def detach(self):
"""
Detach the dock from the parent dock area.
"""
self.float()
def remove_widget(self, widget_rpc_id: str):
"""
Remove a widget from the dock.
Args:
widget_rpc_id(str): The ID of the widget to remove.
"""
widget = self.rpc_register.get_rpc_by_id(widget_rpc_id)
self.layout.removeWidget(widget)
widget.close()
def remove(self):
"""
Remove the dock from the parent dock area.
"""
# self.cleanup()
self.parent_dock_area.remove_dock(self.name())
def cleanup(self):
"""
Clean up the dock, including all its widgets.
"""
for widget in self.widgets:
if hasattr(widget, "cleanup"):
widget.cleanup()
super().cleanup()
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()

View File

@ -1,17 +1,18 @@
from __future__ import annotations
from typing import Literal, Optional
from weakref import WeakValueDictionary
from pydantic import Field
from pyqtgraph.dockarea.DockArea import DockArea
from qtpy.QtCore import Qt
from qtpy.QtGui import QPainter, QPaintEvent
from qtpy.QtWidgets import QWidget
from bec_widgets.utils import BECConnector, ConnectionConfig, WidgetContainerUtils
from .dock import BECDock, DockConfig
# from bec_widgets.widgets import BECDock
class DockAreaConfig(ConnectionConfig):
docks: dict[str, DockConfig] = Field({}, description="The docks in the dock area.")
@ -19,18 +20,23 @@ class DockAreaConfig(ConnectionConfig):
class BECDockArea(BECConnector, DockArea):
USER_ACCESS = [
"panels",
"save_state",
"remove_dock",
"restore_state",
"add_dock",
"remove_dock_by_id",
"clear_all",
"dock_dict",
"detach_dock",
"attach_all",
"get_all_rpc",
]
def __init__(
self,
parent: Optional[QWidget] = None,
config: Optional[DockAreaConfig] = None,
parent: QWidget | None = None,
config: DockAreaConfig | None = None,
client=None,
gui_id: Optional[str] = None,
gui_id: str = None,
) -> None:
if config is None:
config = DockAreaConfig(widget_class=self.__class__.__name__)
@ -41,41 +47,80 @@ class BECDockArea(BECConnector, DockArea):
super().__init__(client=client, config=config, gui_id=gui_id)
DockArea.__init__(self, parent=parent)
self._last_state = None # TODO not sure if this will ever work
self._instructions_visible = True
def paintEvent(self, event: QPaintEvent):
super().paintEvent(event)
if self._instructions_visible:
painter = QPainter(self)
painter.drawText(self.rect(), Qt.AlignCenter, "Add docks using 'add_dock' method")
@property
def dock_dict(self) -> dict:
def panels(self) -> dict:
"""
Get the docks in the dock area.
Returns:
dock_dict(dict): The docks in the dock area.
"""
return dict(self.docks)
@dock_dict.setter
def dock_dict(self, value: dict):
from weakref import WeakValueDictionary
@panels.setter
def panels(self, value: dict):
self.docks = WeakValueDictionary(value)
def remove_dock_by_id(self, dock_id: str):
if dock_id in self.docks:
dock_to_remove = self.docks[dock_id]
dock_to_remove.close()
else:
raise ValueError(f"Dock with id {dock_id} does not exist.")
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._last_state
self.restoreState(state, missing=missing, extra=extra)
def save_state(self) -> dict:
"""
Save the state of the dock area.
Returns:
dict: The state of the dock area.
"""
self._last_state = self.saveState()
return self._last_state
def remove_dock(self, name: str):
for id, dock in self.docks.items():
dock_name = dock.name()
if dock_name == name:
"""
Remove a dock by name and ensure it is properly closed and cleaned up.
Args:
name(str): The name of the dock to remove.
"""
dock = self.docks.pop(name, None)
if dock:
dock.close()
break
if len(self.docks) <= 1:
for dock in self.docks.values():
dock.hide_title_bar()
else:
raise ValueError(f"Dock with name {name} does not exist.")
def add_dock(
self,
name: str = None,
position: Literal["bottom", "top", "left", "right", "above", "below"] = None,
relative_to: Optional[BECDock] = None,
relative_to: BECDock | None = None,
closable: bool = False,
prefix: str = "dock",
widget: QWidget = None,
widget: str | QWidget | None = None,
row: int = None,
col: int = None,
col: int = 0,
rowspan: int = 1,
colspan: int = 1,
) -> BECDock:
@ -86,13 +131,13 @@ class BECDockArea(BECConnector, DockArea):
name(str): The name of the dock to be displayed and for further references. Has to be unique.
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.
prefix(str): The prefix for the dock name if no name is provided.
widget(QWidget): The widget to be added to the dock.
widget(str|QWidget|None): The widget to be added to the dock. While using RPC, only BEC RPC widgets from RPCWidgetHandler are allowed.
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.
"""
@ -107,17 +152,69 @@ class BECDockArea(BECConnector, DockArea):
if position is None:
position = "bottom"
dock = BECDock(name=name, parent_dock_area=self, closable=True)
dock = BECDock(name=name, parent_dock_area=self, closable=closable)
dock.config.position = position
self.config.docks[name] = dock.config
self.addDock(dock=dock, position=position, relativeTo=relative_to)
if widget is not None:
dock.addWidget(widget) # , row, col, rowspan, colspan)
if len(self.docks) <= 1:
dock.hide_title_bar()
elif len(self.docks) > 1:
for dock in self.docks.values():
dock.show_title_bar()
if widget is not None and isinstance(widget, str):
dock.add_widget_bec(
widget_type=widget, row=row, col=col, rowspan=rowspan, colspan=colspan
)
elif widget is not None and isinstance(widget, QWidget):
dock.addWidget(widget, row=row, col=col, rowspan=rowspan, colspan=colspan)
if self._instructions_visible:
self._instructions_visible = False
self.update()
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.docks[dock_name]
self.floatDock(dock)
return dock
def attach_all(self):
"""
Return all floating docks to the dock area.
"""
while self.tempAreas:
for temp_area in self.tempAreas:
self.removeTempArea(temp_area)
def clear_all(self):
for dock in self.docks.values():
dock.close()
"""
Close all docks and remove all temp areas.
"""
self.attach_all()
for dock in dict(self.docks).values():
dock.remove()
def cleanup(self):
"""
Cleanup the dock area.
"""
self.clear_all()
super().cleanup()
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()

View File

@ -1,20 +1,18 @@
# pylint: disable = no-name-in-module,missing-module-docstring
from __future__ import annotations
import itertools
import os
import uuid
from collections import defaultdict
from typing import Literal, Optional, Type
from typing import Literal, Optional
import numpy as np
import pyqtgraph as pg
import qdarktheme
from pydantic import Field
from pyqtgraph.Qt import uic
from qtpy.QtCore import Signal as pyqtSignal
from qtpy.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget
from qtpy.QtWidgets import QWidget
from bec_widgets.utils import BECConnector, BECDispatcher, ConnectionConfig, WidgetContainerUtils
from bec_widgets.utils import BECConnector, ConnectionConfig, WidgetContainerUtils
from bec_widgets.widgets.plots import (
BECImageShow,
BECMotorMap,
@ -166,14 +164,29 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
@widget_list.setter
def widget_list(self, value: list[BECPlotBase]):
"""
Access all widget in BECFigure as a list
Returns:
list[BECPlotBase]: List of all widgets in the figure.
"""
self._axes = value
@property
def widgets(self) -> dict:
"""
All widgets within the figure with gui ids as keys.
Returns:
dict: All widgets within the figure.
"""
return self._widgets
@widgets.setter
def widgets(self, value: dict):
"""
All widgets within the figure with gui ids as keys.
Returns:
dict: All widgets within the figure.
"""
self._widgets = value
def add_plot(
@ -204,7 +217,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
config(dict): Additional configuration for the widget.
**axis_kwargs(dict): Additional axis properties to set on the widget after creation.
"""
widget_id = WidgetContainerUtils.generate_unique_widget_id(self._widgets)
widget_id = str(uuid.uuid4())
waveform = self.add_widget(
widget_type="Waveform1D",
widget_id=widget_id,
@ -430,7 +443,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
BECImageShow: The image widget.
"""
widget_id = WidgetContainerUtils.generate_unique_widget_id(self._widgets)
widget_id = str(uuid.uuid4())
if config is None:
config = ImageConfig(
widget_class="BECImageShow",
@ -513,7 +526,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
Returns:
BECMotorMap: The motor map widget.
"""
widget_id = WidgetContainerUtils.generate_unique_widget_id(self._widgets)
widget_id = str(uuid.uuid4())
if config is None:
config = MotorMapConfig(
widget_class="BECMotorMap",
@ -554,7 +567,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
**axis_kwargs(dict): Additional axis properties to set on the widget after creation.
"""
if not widget_id:
widget_id = WidgetContainerUtils.generate_unique_widget_id(self._widgets)
widget_id = str(uuid.uuid4())
if widget_id in self._widgets:
raise ValueError(f"Widget with ID '{widget_id}' already exists.")
@ -767,12 +780,16 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
def clear_all(self):
"""Clear all widgets from the figure and reset to default state"""
for widget in self._widgets.values():
widget.cleanup()
self.clear()
for widget in list(self._widgets.values()):
widget.remove()
# self.clear()
self._widgets = defaultdict(dict)
self.grid = []
theme = self.config.theme
self.config = FigureConfig(
widget_class=self.__class__.__name__, gui_id=self.gui_id, theme=theme
)
def cleanup(self):
self.clear_all()
super().cleanup()

View File

@ -288,10 +288,6 @@ class BECImageItem(BECConnector, pg.ImageItem):
else:
raise ValueError("style should be 'simple' or 'full'")
def cleanup(self):
"""Clean up widget."""
self.rpc_register.remove_rpc(self)
class BECImageShow(BECPlotBase):
USER_ACCESS = [
@ -806,7 +802,7 @@ class BECImageShow(BECPlotBase):
for image in self.images:
image.cleanup()
self.rpc_register.remove_rpc(self)
super().cleanup()
class ImageProcessor:

View File

@ -425,4 +425,4 @@ class BECMotorMap(BECPlotBase):
def cleanup(self):
"""Cleanup the widget."""
self._disconnect_current_motors()
self.rpc_register.remove_rpc(self)
super().cleanup()

View File

@ -248,3 +248,4 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout):
def cleanup(self):
"""Cleanup the plot widget."""
super().cleanup()

View File

@ -229,14 +229,10 @@ class BECCurve(BECConnector, pg.PlotDataItem):
x_data, y_data = self.getData()
return x_data, y_data
def cleanup(self):
"""Cleanup the curve."""
self.rpc_register.remove_rpc(self)
def remove(self):
"""Remove the curve from the plot."""
self.cleanup()
self.parent_item.removeItem(self)
self.cleanup()
class BECWaveform(BECPlotBase):
@ -799,4 +795,4 @@ class BECWaveform(BECPlotBase):
self.bec_dispatcher.disconnect_slot(self.on_scan_segment, MessageEndpoints.scan_segment())
for curve in self.curves:
curve.cleanup()
self.rpc_register.remove_rpc(self)
super().cleanup()

View File

@ -3,6 +3,7 @@ import pytest
from bec_widgets.cli.rpc_register import RPCRegister
from bec_widgets.cli.server import BECWidgetsCLIServer
from bec_widgets.utils import BECDispatcher
from bec_widgets.widgets import BECDockArea, BECFigure
@pytest.fixture(autouse=True)
@ -12,11 +13,25 @@ def rpc_register():
@pytest.fixture
def rpc_server(qtbot, bec_client_lib, threads_check):
def rpc_server_figure(qtbot, bec_client_lib, threads_check):
dispatcher = BECDispatcher(client=bec_client_lib) # Has to init singleton with fixture client
server = BECWidgetsCLIServer(gui_id="figure")
qtbot.addWidget(server.fig)
qtbot.waitExposed(server.fig)
server = BECWidgetsCLIServer(gui_id="figure", gui_class=BECFigure)
qtbot.addWidget(server.gui)
qtbot.waitExposed(server.gui)
qtbot.wait(1000) # 1s long to wait until gui is ready
yield server
dispatcher.disconnect_all()
server.client.shutdown()
server.shutdown()
dispatcher.reset_singleton()
@pytest.fixture
def rpc_server_dock(qtbot, bec_client_lib, threads_check):
dispatcher = BECDispatcher(client=bec_client_lib) # Has to init singleton with fixture client
server = BECWidgetsCLIServer(gui_id="figure", gui_class=BECDockArea)
qtbot.addWidget(server.gui)
qtbot.waitExposed(server.gui)
qtbot.wait(1000) # 1s long to wait until gui is ready
yield server
dispatcher.disconnect_all()

View File

@ -0,0 +1,145 @@
import numpy as np
import pytest
from bec_lib import MessageEndpoints
from bec_widgets.cli.client import BECDockArea, BECFigure, BECImageShow, BECMotorMap, BECWaveform
def test_rpc_add_dock_with_figure_e2e(rpc_server_dock, qtbot):
dock = BECDockArea(rpc_server_dock.gui_id)
dock_server = rpc_server_dock.gui
# BEC client shortcuts
client = rpc_server_dock.client
dev = client.device_manager.devices
scans = client.scans
queue = client.queue
# Create 3 docks
d0 = dock.add_dock("dock_0")
d1 = dock.add_dock("dock_1")
d2 = dock.add_dock("dock_2")
assert len(dock_server.docks) == 3
# Add 3 figures with some widgets
fig0 = d0.add_widget_bec("BECFigure")
fig1 = d1.add_widget_bec("BECFigure")
fig2 = d2.add_widget_bec("BECFigure")
assert len(dock_server.docks) == 3
assert len(dock_server.docks["dock_0"].widgets) == 1
assert len(dock_server.docks["dock_1"].widgets) == 1
assert len(dock_server.docks["dock_2"].widgets) == 1
assert fig1.__class__.__name__ == "BECFigure"
assert fig1.__class__ == BECFigure
assert fig2.__class__.__name__ == "BECFigure"
assert fig2.__class__ == BECFigure
mm = fig0.motor_map("samx", "samy")
plt = fig1.plot("samx", "bpm4i")
im = fig2.image("eiger")
assert mm.__class__.__name__ == "BECMotorMap"
assert mm.__class__ == BECMotorMap
assert plt.__class__.__name__ == "BECWaveform"
assert plt.__class__ == BECWaveform
assert im.__class__.__name__ == "BECImageShow"
assert im.__class__ == BECImageShow
assert mm.config_dict["signals"] == {
"source": "device_readback",
"x": {
"name": "samx",
"entry": "samx",
"unit": None,
"modifier": None,
"limits": [-50.0, 50.0],
},
"y": {
"name": "samy",
"entry": "samy",
"unit": None,
"modifier": None,
"limits": [-50.0, 50.0],
},
"z": None,
}
assert plt.config_dict["curves"]["bpm4i-bpm4i"]["signals"] == {
"source": "scan_segment",
"x": {"name": "samx", "entry": "samx", "unit": None, "modifier": None, "limits": None},
"y": {"name": "bpm4i", "entry": "bpm4i", "unit": None, "modifier": None, "limits": None},
"z": None,
}
assert im.config_dict["images"]["eiger"]["monitor"] == "eiger"
# check initial position of motor map
initial_pos_x = dev.samx.read()["samx"]["value"]
initial_pos_y = dev.samy.read()["samy"]["value"]
# Try to make a scan
status = scans.line_scan(dev.samx, -5, 5, steps=10, exp_time=0.05, relative=False)
# wait for scan to finish
while not status.status == "COMPLETED":
qtbot.wait(200)
# plot
plt_last_scan_data = queue.scan_storage.storage[-1].data
plt_data = plt.get_all_data()
assert plt_data["bpm4i-bpm4i"]["x"] == plt_last_scan_data["samx"]["samx"].val
assert plt_data["bpm4i-bpm4i"]["y"] == plt_last_scan_data["bpm4i"]["bpm4i"].val
# image
last_image_device = client.connector.get_last(MessageEndpoints.device_monitor("eiger"))[
"data"
].data
qtbot.wait(500)
last_image_plot = im.images[0].get_data()
np.testing.assert_equal(last_image_device, last_image_plot)
# motor map
final_pos_x = dev.samx.read()["samx"]["value"]
final_pos_y = dev.samy.read()["samy"]["value"]
# check final coordinates of motor map
motor_map_data = mm.get_data()
np.testing.assert_equal(
[motor_map_data["x"][0], motor_map_data["y"][0]], [initial_pos_x, initial_pos_y]
)
np.testing.assert_equal(
[motor_map_data["x"][-1], motor_map_data["y"][-1]], [final_pos_x, final_pos_y]
)
def test_dock_manipulations_e2e(rpc_server_dock, qtbot):
dock = BECDockArea(rpc_server_dock.gui_id)
dock_server = rpc_server_dock.gui
d0 = dock.add_dock("dock_0")
d1 = dock.add_dock("dock_1")
d2 = dock.add_dock("dock_2")
assert len(dock_server.docks) == 3
d0.detach()
dock.detach_dock("dock_2")
assert len(dock_server.docks) == 3
assert len(dock_server.tempAreas) == 2
d0.attach()
assert len(dock_server.docks) == 3
assert len(dock_server.tempAreas) == 1
d2.remove()
qtbot.wait(200)
assert len(dock_server.docks) == 2
docks_list = list(dict(dock_server.docks).keys())
assert ["dock_0", "dock_1"] == docks_list
dock.clear_all()
assert len(dock_server.docks) == 0
assert len(dock_server.tempAreas) == 0

View File

@ -3,27 +3,11 @@ import pytest
from bec_lib import MessageEndpoints
from bec_widgets.cli.client import BECFigure, BECImageShow, BECMotorMap, BECWaveform
from bec_widgets.cli.server import BECWidgetsCLIServer
from bec_widgets.utils import BECDispatcher
@pytest.fixture
def rpc_server(qtbot, bec_client_lib, threads_check):
dispatcher = BECDispatcher(client=bec_client_lib) # Has to init singleton with fixture client
server = BECWidgetsCLIServer(gui_id="id_test")
qtbot.addWidget(server.gui)
qtbot.waitExposed(server.gui)
qtbot.wait(1000) # 1s long to wait until gui is ready
yield server
dispatcher.disconnect_all()
server.client.shutdown()
server.shutdown()
dispatcher.reset_singleton()
def test_rpc_waveform1d_custom_curve(rpc_server, qtbot):
fig = BECFigure(rpc_server.gui_id)
fig_server = rpc_server.gui
def test_rpc_waveform1d_custom_curve(rpc_server_figure, qtbot):
fig = BECFigure(rpc_server_figure.gui_id)
fig_server = rpc_server_figure.gui
ax = fig.add_plot()
curve = ax.add_curve_custom([1, 2, 3], [1, 2, 3])
@ -32,12 +16,12 @@ def test_rpc_waveform1d_custom_curve(rpc_server, qtbot):
curve.set_color("blue")
assert len(fig_server.widgets) == 1
assert len(fig_server.widgets["widget_1"].curves) == 1
assert len(fig_server.widgets[ax.rpc_id].curves) == 1
def test_rpc_plotting_shortcuts_init_configs(rpc_server, qtbot):
fig = BECFigure(rpc_server.gui_id)
fig_server = rpc_server.gui
def test_rpc_plotting_shortcuts_init_configs(rpc_server_figure, qtbot):
fig = BECFigure(rpc_server_figure.gui_id)
fig_server = rpc_server_figure.gui
plt = fig.plot("samx", "bpm4i")
im = fig.image("eiger")
@ -91,15 +75,15 @@ def test_rpc_plotting_shortcuts_init_configs(rpc_server, qtbot):
}
def test_rpc_waveform_scan(rpc_server, qtbot):
fig = BECFigure(rpc_server.gui_id)
def test_rpc_waveform_scan(rpc_server_figure, qtbot):
fig = BECFigure(rpc_server_figure.gui_id)
# add 3 different curves to track
plt = fig.plot("samx", "bpm4i")
fig.plot("samx", "bpm3a")
fig.plot("samx", "bpm4d")
client = rpc_server.client
client = rpc_server_figure.client
dev = client.device_manager.devices
scans = client.scans
queue = client.queue
@ -124,12 +108,12 @@ def test_rpc_waveform_scan(rpc_server, qtbot):
assert plt_data["bpm4d-bpm4d"]["y"] == last_scan_data["bpm4d"]["bpm4d"].val
def test_rpc_image(rpc_server, qtbot):
fig = BECFigure(rpc_server.gui_id)
def test_rpc_image(rpc_server_figure, qtbot):
fig = BECFigure(rpc_server_figure.gui_id)
im = fig.image("eiger")
client = rpc_server.client
client = rpc_server_figure.client
dev = client.device_manager.devices
scans = client.scans
@ -149,13 +133,13 @@ def test_rpc_image(rpc_server, qtbot):
np.testing.assert_equal(last_image_device, last_image_plot)
def test_rpc_motor_map(rpc_server, qtbot):
fig = BECFigure(rpc_server.gui_id)
fig_server = rpc_server.gui
def test_rpc_motor_map(rpc_server_figure, qtbot):
fig = BECFigure(rpc_server_figure.gui_id)
fig_server = rpc_server_figure.gui
motor_map = fig.motor_map("samx", "samy")
client = rpc_server.client
client = rpc_server_figure.client
dev = client.device_manager.devices
scans = client.scans

View File

@ -18,9 +18,9 @@ def find_deepest_value(d: dict):
return d
def test_rpc_register_list_connections(rpc_server, rpc_register, qtbot):
fig = BECFigure(rpc_server.gui_id)
fig_server = rpc_server.fig
def test_rpc_register_list_connections(rpc_server_figure, rpc_register, qtbot):
fig = BECFigure(rpc_server_figure.gui_id)
fig_server = rpc_server_figure.gui
plt = fig.plot("samx", "bpm4i")
im = fig.image("eiger")

View File

@ -0,0 +1,114 @@
# pylint: disable=missing-function-docstring, missing-module-docstring, unused-import
import pytest
from bec_widgets.widgets import BECDock, BECDockArea
from .client_mocks import mocked_client
@pytest.fixture
def bec_dock_area(qtbot, mocked_client):
widget = BECDockArea(client=mocked_client)
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
widget.close()
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.docks)
# Adding 3 docks
d0 = bec_dock_area.add_dock()
d1 = bec_dock_area.add_dock()
d2 = bec_dock_area.add_dock()
# Check if the docks were added
assert len(bec_dock_area.docks) == initial_count + 3
assert d0.name() in dict(bec_dock_area.docks)
assert d1.name() in dict(bec_dock_area.docks)
assert d2.name() in dict(bec_dock_area.docks)
assert bec_dock_area.docks[d0.name()].config.widget_class == "BECDock"
assert bec_dock_area.docks[d1.name()].config.widget_class == "BECDock"
assert bec_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.docks)
# Remove docks
d0_name = d0.name()
bec_dock_area.remove_dock(d0_name) # TODO fix this, works in jupyter console
qtbot.wait(200)
d1.remove()
qtbot.wait(200)
assert len(bec_dock_area.docks) == initial_count + 1
assert d0.name() not in dict(bec_dock_area.docks)
assert d1.name() not in dict(bec_dock_area.docks)
assert d2.name() in dict(bec_dock_area.docks)
def test_add_remove_bec_figure_to_dock(bec_dock_area):
d0 = bec_dock_area.add_dock()
fig = d0.add_widget_bec("BECFigure")
plt = fig.plot("samx", "bpm4i")
im = fig.image("eiger")
mm = fig.motor_map("samx", "samy")
assert len(bec_dock_area.docks) == 1
assert len(d0.widgets) == 1
assert len(d0.widget_list) == 1
assert len(fig.widgets) == 3
assert fig.config.widget_class == "BECFigure"
assert plt.config.widget_class == "BECWaveform"
assert im.config.widget_class == "BECImageShow"
assert mm.config.widget_class == "BECMotorMap"
def test_dock_area_errors(bec_dock_area):
d0 = bec_dock_area.add_dock(name="dock_0")
with pytest.raises(ValueError) as excinfo:
bec_dock_area.add_dock(name="dock_0")
assert "Dock with name dock_0 already exists." in str(excinfo.value)
def test_close_docks(bec_dock_area, qtbot):
d0 = bec_dock_area.add_dock(name="dock_0")
d1 = bec_dock_area.add_dock(name="dock_1")
d2 = bec_dock_area.add_dock(name="dock_2")
bec_dock_area.clear_all()
qtbot.wait(200)
assert len(bec_dock_area.docks) == 0
def test_undock_and_dock_docks(bec_dock_area, qtbot):
d0 = bec_dock_area.add_dock(name="dock_0")
d1 = bec_dock_area.add_dock(name="dock_1")
d2 = bec_dock_area.add_dock(name="dock_4")
d3 = bec_dock_area.add_dock(name="dock_3")
d0.detach()
bec_dock_area.detach_dock("dock_1")
d2.detach()
assert len(bec_dock_area.docks) == 4
assert len(bec_dock_area.tempAreas) == 3
d0.attach()
assert len(bec_dock_area.docks) == 4
assert len(bec_dock_area.tempAreas) == 2
bec_dock_area.attach_all()
assert len(bec_dock_area.docks) == 4
assert len(bec_dock_area.tempAreas) == 0

View File

@ -1,6 +1,4 @@
# pylint: disable=missing-function-docstring, missing-module-docstring, unused-import
import os
from unittest.mock import MagicMock
import numpy as np
import pytest
@ -48,12 +46,12 @@ def test_bec_figure_add_remove_plot(bec_figure):
# Check if the widgets were added
assert len(bec_figure._widgets) == initial_count + 3
assert "widget_1" in bec_figure._widgets
assert "widget_2" in bec_figure._widgets
assert "widget_3" in bec_figure._widgets
assert bec_figure._widgets["widget_1"].config.widget_class == "BECWaveform"
assert bec_figure._widgets["widget_2"].config.widget_class == "BECWaveform"
assert bec_figure._widgets["widget_3"].config.widget_class == "BECPlotBase"
assert w0.gui_id in bec_figure._widgets
assert w1.gui_id in bec_figure._widgets
assert w2.gui_id in bec_figure._widgets
assert bec_figure._widgets[w0.gui_id].config.widget_class == "BECWaveform"
assert bec_figure._widgets[w1.gui_id].config.widget_class == "BECWaveform"
assert bec_figure._widgets[w2.gui_id].config.widget_class == "BECPlotBase"
# Check accessing positions by the grid in figure
assert bec_figure[0, 0] == w0
@ -61,11 +59,11 @@ def test_bec_figure_add_remove_plot(bec_figure):
assert bec_figure[2, 0] == w2
# Removing 1 widget
bec_figure.remove(widget_id="widget_1")
bec_figure.remove(widget_id=w0.gui_id)
assert len(bec_figure._widgets) == initial_count + 2
assert "widget_1" not in bec_figure._widgets
assert "widget_3" in bec_figure._widgets
assert bec_figure._widgets["widget_2"].config.widget_class == "BECWaveform"
assert w0.gui_id not in bec_figure._widgets
assert w2.gui_id in bec_figure._widgets
assert bec_figure._widgets[w1.gui_id].config.widget_class == "BECWaveform"
def test_add_different_types_of_widgets(bec_figure):
@ -121,20 +119,20 @@ def test_remove_plots(bec_figure):
# remove by coordinates
bec_figure[0, 0].remove()
assert "widget_1" not in bec_figure._widgets
assert w1.gui_id not in bec_figure._widgets
# remove by widget_id
bec_figure.remove(widget_id="widget_2")
assert "widget_2" not in bec_figure._widgets
bec_figure.remove(widget_id=w2.gui_id)
assert w2.gui_id not in bec_figure._widgets
# remove by widget object
w3.remove()
assert "widget_3" not in bec_figure._widgets
assert w3.gui_id not in bec_figure._widgets
# check the remaining widget 4
assert bec_figure[0, 0] == w4
assert bec_figure["widget_4"] == w4
assert "widget_4" in bec_figure._widgets
assert bec_figure[w4.gui_id] == w4
assert w4.gui_id in bec_figure._widgets
assert len(bec_figure._widgets) == 1
@ -143,8 +141,8 @@ def test_remove_plots_by_coordinates_ints(bec_figure):
w2 = bec_figure.add_plot(row=0, col=1)
bec_figure.remove(0, 0)
assert "widget_1" not in bec_figure._widgets
assert "widget_2" in bec_figure._widgets
assert w1.gui_id not in bec_figure._widgets
assert w2.gui_id in bec_figure._widgets
assert bec_figure[0, 0] == w2
assert len(bec_figure._widgets) == 1
@ -154,8 +152,8 @@ def test_remove_plots_by_coordinates_tuple(bec_figure):
w2 = bec_figure.add_plot(row=0, col=1)
bec_figure.remove(coordinates=(0, 0))
assert "widget_1" not in bec_figure._widgets
assert "widget_2" in bec_figure._widgets
assert w1.gui_id not in bec_figure._widgets
assert w2.gui_id in bec_figure._widgets
assert bec_figure[0, 0] == w2
assert len(bec_figure._widgets) == 1

View File

@ -40,7 +40,7 @@ def test_client_generator_with_black_formatting():
'''\
# This file was automatically generated by generate_cli.py
from bec_widgets.cli.client_utils import rpc_call, RPCBase, BECFigureClientMixin
from bec_widgets.cli.client_utils import rpc_call, RPCBase, BECGuiClientMixin
from typing import Literal, Optional, overload
class MockBECWaveform1D(RPCBase):

View File

@ -141,7 +141,7 @@ def test_getting_curve(bec_figure):
c1_expected_config = CurveConfig(
widget_class="BECCurve",
gui_id="test_curve",
parent_id="widget_1",
parent_id=w1.gui_id,
label="bpm4i-bpm4i",
color="#cc4778",
symbol="o",