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

feat(widget/dock): BECDock and BECDock area for dockable windows

This commit is contained in:
2024-05-01 17:16:19 +02:00
parent 03fa1f26d0
commit d8ff8afcd4
11 changed files with 374 additions and 14 deletions

View File

@ -255,11 +255,11 @@ class BECWaveform(RPCBase):
""" """
@rpc_call @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. Apply the configuration to the 1D waveform widget.
Args: 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. replot_last_scan(bool, optional): If True, replot the last scan. Defaults to False.
""" """
@ -1307,3 +1307,70 @@ class BECMotorMap(RPCBase):
Returns: Returns:
dict: Data of the motor map. 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
"""

View File

@ -108,6 +108,7 @@ if __name__ == "__main__": # pragma: no cover
import os import os
from bec_widgets.utils import BECConnector 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.figure import BECFigure
from bec_widgets.widgets.plots import BECImageShow, BECMotorMap, BECPlotBase, BECWaveform from bec_widgets.widgets.plots import BECImageShow, BECMotorMap, BECPlotBase, BECWaveform
from bec_widgets.widgets.plots.image import BECImageItem from bec_widgets.widgets.plots.image import BECImageItem
@ -124,6 +125,8 @@ if __name__ == "__main__": # pragma: no cover
BECConnector, BECConnector,
BECImageItem, BECImageItem,
BECMotorMap, BECMotorMap,
BECDock,
BECDockArea,
] ]
generator = ClientGenerator() generator = ClientGenerator()
generator.generate_client(clss) generator.generate_client(clss)

View File

@ -1,6 +1,7 @@
import inspect import inspect
import threading import threading
import time import time
from typing import Literal
from bec_lib import MessageEndpoints, messages from bec_lib import MessageEndpoints, messages
from qtpy.QtCore import QTimer 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.cli.rpc_register import RPCRegister
from bec_widgets.utils import BECDispatcher from bec_widgets.utils import BECDispatcher
from bec_widgets.utils.bec_connector import BECConnector 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.figure import BECFigure
from bec_widgets.widgets.plots import BECCurve, BECImageShow, BECWaveform from bec_widgets.widgets.plots import BECCurve, BECImageShow, BECWaveform
@ -16,15 +18,20 @@ class BECWidgetsCLIServer:
WIDGETS = [BECWaveform, BECFigure, BECCurve, BECImageShow] WIDGETS = [BECWaveform, BECFigure, BECCurve, BECImageShow]
def __init__( 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: ) -> None:
self.dispatcher = BECDispatcher(config=config) if dispatcher is None else dispatcher self.dispatcher = BECDispatcher(config=config) if dispatcher is None else dispatcher
self.client = self.dispatcher.client if client is None else client self.client = self.dispatcher.client if client is None else client
self.client.start() self.client.start()
self.gui_id = gui_id 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 = RPCRegister()
self.rpc_register.add_rpc(self.fig) self.rpc_register.add_rpc(self.gui)
self.dispatcher.connect_slot( self.dispatcher.connect_slot(
self.on_rpc_update, MessageEndpoints.gui_instructions(self.gui_id) 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 = argparse.ArgumentParser(description="BEC Widgets CLI Server")
parser.add_argument("--id", type=str, help="The id of the 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.") parser.add_argument("--config", type=str, help="Config to connect to redis.")
args = parser.parse_args() args = parser.parse_args()
server = BECWidgetsCLIServer(gui_id=args.id, config=args.config) if args.gui_class == "BECFigure":
# server = BECWidgetsCLIServer(gui_id="test",config="awi-bec-dev-01:6379") 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.setCentralWidget(fig)
win.show() win.show()

View File

@ -2,14 +2,17 @@ import os
import numpy as np import numpy as np
import pyqtgraph as pg import pyqtgraph as pg
from pyqtgraph.Qt import uic from pyqtgraph.Qt import QtWidgets, uic
from qtconsole.inprocess import QtInProcessKernelManager from qtconsole.inprocess import QtInProcessKernelManager
from qtconsole.rich_jupyter_widget import RichJupyterWidget 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 qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
from bec_widgets.cli.rpc_register import RPCRegister from bec_widgets.cli.rpc_register import RPCRegister
from bec_widgets.utils import BECDispatcher from bec_widgets.utils import BECDispatcher
from bec_widgets.widgets import BECFigure from bec_widgets.widgets import BECFigure
from bec_widgets.widgets.dock.dock_area import BECDockArea
class JupyterConsoleWidget(RichJupyterWidget): # pragma: no cover: class JupyterConsoleWidget(RichJupyterWidget): # pragma: no cover:
@ -52,9 +55,13 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
{ {
"fig": self.figure, "fig": self.figure,
"register": self.register, "register": self.register,
"dock": self.dock,
"w1": self.w1, "w1": self.w1,
"w2": self.w2, "w2": self.w2,
"w3": self.w3, "w3": self.w3,
"d1": self.d1,
"d2": self.d2,
"d3": self.d3,
"bec": self.figure.client, "bec": self.figure.client,
"scans": self.figure.client.scans, "scans": self.figure.client.scans,
"dev": self.figure.client.device_manager.devices, "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.figure = BECFigure(parent=self, gui_id="remote") # Create a new BECDeviceMonitor
self.glw_1_layout.addWidget(self.figure) # Add BECDeviceMonitor to the layout 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 # add stuff to figure
self._init_figure() self._init_figure()
# init dock for testing
self._init_dock()
self.console_layout = QVBoxLayout(self.widget_console) self.console_layout = QVBoxLayout(self.widget_console)
self.console = JupyterConsoleWidget() self.console = JupyterConsoleWidget()
self.console_layout.addWidget(self.console) 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.w1.add_curve_scan("samx", "samy", "bpm3a", pen_style="dash")
self.c1 = self.w1.get_config() 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 if __name__ == "__main__": # pragma: no cover
import sys import sys
@ -101,6 +133,10 @@ if __name__ == "__main__": # pragma: no cover
app = QApplication(sys.argv) app = QApplication(sys.argv)
app.setApplicationName("Jupyter Console") 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 = JupyterConsoleWindow()
win.show() win.show()

View File

@ -13,13 +13,37 @@
<property name="windowTitle"> <property name="windowTitle">
<string>Plotting Console</string> <string>Plotting Console</string>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout"> <layout class="QVBoxLayout" name="verticalLayout_4">
<item> <item>
<widget class="QSplitter" name="splitter"> <widget class="QSplitter" name="splitter">
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Horizontal</enum>
</property> </property>
<widget class="QWidget" name="glw" native="true"/> <widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tab_1">
<attribute name="title">
<string>BECDock</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QWidget" name="dock_placeholder" native="true"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_2">
<attribute name="title">
<string>BECFigure</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QWidget" name="glw" native="true"/>
</item>
</layout>
</widget>
</widget>
<widget class="QWidget" name="widget_console" native="true"/> <widget class="QWidget" name="widget_console" native="true"/>
</widget> </widget>
</item> </item>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -1,3 +1,4 @@
from .dock import BECDock, BECDockArea
from .figure import BECFigure, FigureConfig from .figure import BECFigure, FigureConfig
from .monitor import BECMonitor from .monitor import BECMonitor
from .motor_control import ( from .motor_control import (

View File

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

View File

@ -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())

View File

@ -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()

View File

@ -3,11 +3,27 @@ import pytest
from bec_lib import MessageEndpoints from bec_lib import MessageEndpoints
from bec_widgets.cli.client import BECFigure, BECImageShow, BECMotorMap, BECWaveform 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): def test_rpc_waveform1d_custom_curve(rpc_server, qtbot):
fig = BECFigure(rpc_server.gui_id) fig = BECFigure(rpc_server.gui_id)
fig_server = rpc_server.fig fig_server = rpc_server.gui
ax = fig.add_plot() ax = fig.add_plot()
curve = ax.add_curve_custom([1, 2, 3], [1, 2, 3]) 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): def test_rpc_plotting_shortcuts_init_configs(rpc_server, qtbot):
fig = BECFigure(rpc_server.gui_id) fig = BECFigure(rpc_server.gui_id)
fig_server = rpc_server.fig fig_server = rpc_server.gui
plt = fig.plot("samx", "bpm4i") plt = fig.plot("samx", "bpm4i")
im = fig.image("eiger") im = fig.image("eiger")
@ -135,7 +151,7 @@ def test_rpc_image(rpc_server, qtbot):
def test_rpc_motor_map(rpc_server, qtbot): def test_rpc_motor_map(rpc_server, qtbot):
fig = BECFigure(rpc_server.gui_id) fig = BECFigure(rpc_server.gui_id)
fig_server = rpc_server.fig fig_server = rpc_server.gui
motor_map = fig.motor_map("samx", "samy") motor_map = fig.motor_map("samx", "samy")