diff --git a/bec_widgets/cli/client.py b/bec_widgets/cli/client.py
index 09d19543..c0ed1159 100644
--- a/bec_widgets/cli/client.py
+++ b/bec_widgets/cli/client.py
@@ -255,11 +255,11 @@ class BECWaveform(RPCBase):
"""
@rpc_call
- def apply_config(self, config: "dict | WidgetConfig", replot_last_scan: "bool" = False):
+ def apply_config(self, config: "dict | SubplotConfig", replot_last_scan: "bool" = False):
"""
Apply the configuration to the 1D waveform widget.
Args:
- config(dict|WidgetConfig): Configuration settings.
+ config(dict|SubplotConfig): Configuration settings.
replot_last_scan(bool, optional): If True, replot the last scan. Defaults to False.
"""
@@ -1307,3 +1307,70 @@ class BECMotorMap(RPCBase):
Returns:
dict: Data of the motor map.
"""
+
+
+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":
+ """
+ None
+ """
+
+
+class BECDockArea(RPCBase):
+ @rpc_call
+ def add_dock(
+ self,
+ name: "str" = None,
+ position: "Literal['bottom', 'top', 'left', 'right', 'above', 'below']" = None,
+ relative_to: "Optional[BECDock]" = None,
+ prefix: "str" = "dock",
+ widget: "QWidget" = None,
+ row: "int" = None,
+ col: "int" = None,
+ 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.
+ 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.
+ 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
+ """
+
+ @property
+ @rpc_call
+ def dock_dict(self) -> "dict":
+ """
+ None
+ """
diff --git a/bec_widgets/cli/generate_cli.py b/bec_widgets/cli/generate_cli.py
index 9e3cc876..7ba9f86d 100644
--- a/bec_widgets/cli/generate_cli.py
+++ b/bec_widgets/cli/generate_cli.py
@@ -108,6 +108,7 @@ if __name__ == "__main__": # pragma: no cover
import os
from bec_widgets.utils import BECConnector
+ from bec_widgets.widgets.dock import BECDock, BECDockArea
from bec_widgets.widgets.figure import BECFigure
from bec_widgets.widgets.plots import BECImageShow, BECMotorMap, BECPlotBase, BECWaveform
from bec_widgets.widgets.plots.image import BECImageItem
@@ -124,6 +125,8 @@ if __name__ == "__main__": # pragma: no cover
BECConnector,
BECImageItem,
BECMotorMap,
+ BECDock,
+ BECDockArea,
]
generator = ClientGenerator()
generator.generate_client(clss)
diff --git a/bec_widgets/cli/server.py b/bec_widgets/cli/server.py
index 095c3e67..a4c743fc 100644
--- a/bec_widgets/cli/server.py
+++ b/bec_widgets/cli/server.py
@@ -1,6 +1,7 @@
import inspect
import threading
import time
+from typing import Literal
from bec_lib import MessageEndpoints, messages
from qtpy.QtCore import QTimer
@@ -8,6 +9,7 @@ from qtpy.QtCore import QTimer
from bec_widgets.cli.rpc_register import RPCRegister
from bec_widgets.utils import BECDispatcher
from bec_widgets.utils.bec_connector import BECConnector
+from bec_widgets.widgets.dock.dock_area import BECDockArea
from bec_widgets.widgets.figure import BECFigure
from bec_widgets.widgets.plots import BECCurve, BECImageShow, BECWaveform
@@ -16,15 +18,20 @@ class BECWidgetsCLIServer:
WIDGETS = [BECWaveform, BECFigure, BECCurve, BECImageShow]
def __init__(
- self, gui_id: str = None, dispatcher: BECDispatcher = None, client=None, config=None
+ self,
+ gui_id: str = None,
+ dispatcher: BECDispatcher = None,
+ client=None,
+ config=None,
+ gui_class: 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
self.client.start()
self.gui_id = gui_id
- self.fig = BECFigure(gui_id=self.gui_id)
+ self.gui = gui_class(gui_id=self.gui_id)
self.rpc_register = RPCRegister()
- self.rpc_register.add_rpc(self.fig)
+ self.rpc_register.add_rpc(self.gui)
self.dispatcher.connect_slot(
self.on_rpc_update, MessageEndpoints.gui_instructions(self.gui_id)
@@ -127,14 +134,30 @@ if __name__ == "__main__": # pragma: no cover
parser = argparse.ArgumentParser(description="BEC Widgets CLI Server")
parser.add_argument("--id", type=str, help="The id of the server")
+ parser.add_argument(
+ "--gui_class",
+ type=str,
+ help="Name of the gui class to be rendered. Possible values: \n- BECFigure\n- BECDockArea",
+ )
parser.add_argument("--config", type=str, help="Config to connect to redis.")
args = parser.parse_args()
- server = BECWidgetsCLIServer(gui_id=args.id, config=args.config)
- # server = BECWidgetsCLIServer(gui_id="test",config="awi-bec-dev-01:6379")
+ if args.gui_class == "BECFigure":
+ gui_class = BECFigure
+ elif args.gui_class == "BECDockArea":
+ gui_class = BECDockArea
+ else:
+ print(
+ "Please specify a valid gui_class to run. Use -h for help."
+ "\n Starting with default gui_class BECFigure."
+ )
+ gui_class = BECFigure
- fig = server.fig
+ 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)
win.show()
diff --git a/bec_widgets/examples/jupyter_console/jupyter_console_window.py b/bec_widgets/examples/jupyter_console/jupyter_console_window.py
index d27a9d7f..5969b22a 100644
--- a/bec_widgets/examples/jupyter_console/jupyter_console_window.py
+++ b/bec_widgets/examples/jupyter_console/jupyter_console_window.py
@@ -2,14 +2,17 @@ import os
import numpy as np
import pyqtgraph as pg
-from pyqtgraph.Qt import uic
+from pyqtgraph.Qt import QtWidgets, uic
from qtconsole.inprocess import QtInProcessKernelManager
from qtconsole.rich_jupyter_widget import RichJupyterWidget
+from qtpy.QtCore import QSize
+from qtpy.QtGui import QIcon
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
from bec_widgets.cli.rpc_register import RPCRegister
from bec_widgets.utils import BECDispatcher
from bec_widgets.widgets import BECFigure
+from bec_widgets.widgets.dock.dock_area import BECDockArea
class JupyterConsoleWidget(RichJupyterWidget): # pragma: no cover:
@@ -52,9 +55,13 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
{
"fig": self.figure,
"register": self.register,
+ "dock": self.dock,
"w1": self.w1,
"w2": self.w2,
"w3": self.w3,
+ "d1": self.d1,
+ "d2": self.d2,
+ "d3": self.d3,
"bec": self.figure.client,
"scans": self.figure.client.scans,
"dev": self.figure.client.device_manager.devices,
@@ -67,9 +74,16 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
self.figure = BECFigure(parent=self, gui_id="remote") # Create a new BECDeviceMonitor
self.glw_1_layout.addWidget(self.figure) # Add BECDeviceMonitor to the layout
+ self.dock_layout = QVBoxLayout(self.dock_placeholder)
+ self.dock = BECDockArea(gui_id="remote")
+ self.dock_layout.addWidget(self.dock)
+
# add stuff to figure
self._init_figure()
+ # init dock for testing
+ self._init_dock()
+
self.console_layout = QVBoxLayout(self.widget_console)
self.console = JupyterConsoleWidget()
self.console_layout.addWidget(self.console)
@@ -91,6 +105,24 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
self.w1.add_curve_scan("samx", "samy", "bpm3a", pen_style="dash")
self.c1 = self.w1.get_config()
+ def _init_dock(self):
+ self.button_1 = QtWidgets.QPushButton("Button 1 ")
+ self.button_3 = QtWidgets.QPushButton("Button above Figure ")
+ self.label_1 = QtWidgets.QLabel("some scan info label with useful information")
+
+ self.label_2 = QtWidgets.QLabel("label which is added separately")
+ self.label_3 = QtWidgets.QLabel("Label above figure")
+
+ self.d1 = self.dock.add_dock(widget=self.button_1, position="left")
+ self.d1.addWidget(self.label_2)
+ self.d2 = self.dock.add_dock(widget=self.label_1, position="right")
+ self.d3 = self.dock.add_dock(name="figure")
+ self.fig_dock3 = BECFigure()
+ self.fig_dock3.plot("samx", "bpm4d")
+ self.d3.add_widget(self.label_3)
+ self.d3.add_widget(self.button_3)
+ self.d3.add_widget(self.fig_dock3)
+
if __name__ == "__main__": # pragma: no cover
import sys
@@ -101,6 +133,10 @@ if __name__ == "__main__": # pragma: no cover
app = QApplication(sys.argv)
app.setApplicationName("Jupyter Console")
+ app.setApplicationDisplayName("Jupyter Console")
+ icon = QIcon()
+ icon.addFile("terminal_icon.png", size=QSize(48, 48))
+ app.setWindowIcon(icon)
win = JupyterConsoleWindow()
win.show()
diff --git a/bec_widgets/examples/jupyter_console/jupyter_console_window.ui b/bec_widgets/examples/jupyter_console/jupyter_console_window.ui
index f0098b09..6abdf3b1 100644
--- a/bec_widgets/examples/jupyter_console/jupyter_console_window.ui
+++ b/bec_widgets/examples/jupyter_console/jupyter_console_window.ui
@@ -13,13 +13,37 @@
Plotting Console
-
+
-
Qt::Horizontal
-
+
+
+ 0
+
+
+
+ BECDock
+
+
+
-
+
+
+
+
+
+
+ BECFigure
+
+
+ -
+
+
+
+
+
diff --git a/bec_widgets/examples/jupyter_console/terminal_icon.png b/bec_widgets/examples/jupyter_console/terminal_icon.png
new file mode 100644
index 00000000..79e48272
Binary files /dev/null and b/bec_widgets/examples/jupyter_console/terminal_icon.png differ
diff --git a/bec_widgets/widgets/__init__.py b/bec_widgets/widgets/__init__.py
index 3d0e94eb..0fdb16ab 100644
--- a/bec_widgets/widgets/__init__.py
+++ b/bec_widgets/widgets/__init__.py
@@ -1,3 +1,4 @@
+from .dock import BECDock, BECDockArea
from .figure import BECFigure, FigureConfig
from .monitor import BECMonitor
from .motor_control import (
diff --git a/bec_widgets/widgets/dock/__init__.py b/bec_widgets/widgets/dock/__init__.py
new file mode 100644
index 00000000..d83dbe5f
--- /dev/null
+++ b/bec_widgets/widgets/dock/__init__.py
@@ -0,0 +1,2 @@
+from .dock import BECDock
+from .dock_area import BECDockArea
diff --git a/bec_widgets/widgets/dock/dock.py b/bec_widgets/widgets/dock/dock.py
new file mode 100644
index 00000000..8b3c11b1
--- /dev/null
+++ b/bec_widgets/widgets/dock/dock.py
@@ -0,0 +1,65 @@
+from __future__ import annotations
+
+from typing import Literal, Optional
+
+from pydantic import Field
+from pyqtgraph.dockarea import Dock
+from qtpy.QtWidgets import QWidget
+
+from bec_widgets.utils import BECConnector, ConnectionConfig
+
+
+class DockConfig(ConnectionConfig):
+ widgets: dict[str, ConnectionConfig] = 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] = Field(
+ None, description="The GUI ID of parent dock area of the dock."
+ )
+
+
+class BECDock(BECConnector, Dock):
+ USER_ACCESS = ["add_widget", "widget_list"]
+
+ 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,
+ client=None,
+ gui_id: Optional[str] = None,
+ **kwargs,
+ ) -> None:
+ if config is None:
+ config = DockConfig(
+ widget_class=self.__class__.__name__, parent_dock_area=parent_dock_area.gui_id
+ )
+ else:
+ if isinstance(config, dict):
+ config = DockConfig(**config)
+ self.config = config
+ super().__init__(client=client, config=config, gui_id=gui_id)
+ Dock.__init__(self, name=name, **kwargs)
+
+ self.parent_dock_area = parent_dock_area
+
+ self.sigClosed.connect(self._remove_from_dock_area)
+
+ @property
+ def widget_list(self) -> list:
+ return self.widgets
+
+ @widget_list.setter
+ def widget_list(self, value: list):
+ self.widgets = value
+
+ def add_widget(self, widget: QWidget, row=None, col=0, rowspan=1, colspan=1):
+ 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())
diff --git a/bec_widgets/widgets/dock/dock_area.py b/bec_widgets/widgets/dock/dock_area.py
new file mode 100644
index 00000000..e6c40fe2
--- /dev/null
+++ b/bec_widgets/widgets/dock/dock_area.py
@@ -0,0 +1,123 @@
+from __future__ import annotations
+
+from typing import Literal, Optional
+
+from pydantic import Field
+from pyqtgraph.dockarea.DockArea import DockArea
+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.")
+
+
+class BECDockArea(BECConnector, DockArea):
+ USER_ACCESS = [
+ "add_dock",
+ "remove_dock_by_id",
+ "clear_all",
+ "dock_dict",
+ ]
+
+ def __init__(
+ self,
+ parent: Optional[QWidget] = None,
+ config: Optional[DockAreaConfig] = None,
+ client=None,
+ gui_id: Optional[str] = None,
+ ) -> 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__(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
+
+ @property
+ def dock_dict(self) -> dict:
+ return dict(self.docks)
+
+ @dock_dict.setter
+ def dock_dict(self, value: dict):
+ from weakref import WeakValueDictionary
+
+ 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 remove_dock(self, name: str):
+ for id, dock in self.docks.items():
+ dock_name = dock.name()
+ if dock_name == name:
+ dock.close()
+ break
+
+ def add_dock(
+ self,
+ name: str = None,
+ position: Literal["bottom", "top", "left", "right", "above", "below"] = None,
+ relative_to: Optional[BECDock] = None,
+ prefix: str = "dock",
+ widget: QWidget = None,
+ row: int = None,
+ col: int = None,
+ 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.
+ 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.
+ 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.
+ """
+ if name is None:
+ name = WidgetContainerUtils.generate_unique_widget_id(
+ container=self.docks, prefix=prefix
+ )
+
+ if name in set(self.docks.keys()):
+ raise ValueError(f"Dock with name {name} already exists.")
+
+ if position is None:
+ position = "bottom"
+
+ dock = BECDock(name=name, parent_dock_area=self, closable=True)
+ 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)
+
+ return dock
+
+ def clear_all(self):
+ for dock in self.docks.values():
+ dock.close()
diff --git a/tests/end-2-end/test_bec_figure_rpc_e2e.py b/tests/end-2-end/test_bec_figure_rpc_e2e.py
index e64d47f6..e490ef24 100644
--- a/tests/end-2-end/test_bec_figure_rpc_e2e.py
+++ b/tests/end-2-end/test_bec_figure_rpc_e2e.py
@@ -3,11 +3,27 @@ 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.fig
+ fig_server = rpc_server.gui
ax = fig.add_plot()
curve = ax.add_curve_custom([1, 2, 3], [1, 2, 3])
@@ -21,7 +37,7 @@ def test_rpc_waveform1d_custom_curve(rpc_server, qtbot):
def test_rpc_plotting_shortcuts_init_configs(rpc_server, qtbot):
fig = BECFigure(rpc_server.gui_id)
- fig_server = rpc_server.fig
+ fig_server = rpc_server.gui
plt = fig.plot("samx", "bpm4i")
im = fig.image("eiger")
@@ -135,7 +151,7 @@ def test_rpc_image(rpc_server, qtbot):
def test_rpc_motor_map(rpc_server, qtbot):
fig = BECFigure(rpc_server.gui_id)
- fig_server = rpc_server.fig
+ fig_server = rpc_server.gui
motor_map = fig.motor_map("samx", "samy")