From d8ff8afcd474660a6069bbdab05f10a65f221727 Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Wed, 1 May 2024 17:16:19 +0200 Subject: [PATCH] feat(widget/dock): BECDock and BECDock area for dockable windows --- bec_widgets/cli/client.py | 71 +++++++++- bec_widgets/cli/generate_cli.py | 3 + bec_widgets/cli/server.py | 35 ++++- .../jupyter_console/jupyter_console_window.py | 38 +++++- .../jupyter_console/jupyter_console_window.ui | 28 +++- .../jupyter_console/terminal_icon.png | Bin 0 -> 3889 bytes bec_widgets/widgets/__init__.py | 1 + bec_widgets/widgets/dock/__init__.py | 2 + bec_widgets/widgets/dock/dock.py | 65 +++++++++ bec_widgets/widgets/dock/dock_area.py | 123 ++++++++++++++++++ tests/end-2-end/test_bec_figure_rpc_e2e.py | 22 +++- 11 files changed, 374 insertions(+), 14 deletions(-) create mode 100644 bec_widgets/examples/jupyter_console/terminal_icon.png create mode 100644 bec_widgets/widgets/dock/__init__.py create mode 100644 bec_widgets/widgets/dock/dock.py create mode 100644 bec_widgets/widgets/dock/dock_area.py 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 0000000000000000000000000000000000000000..79e48272a3c5b88759009c26b6bd635ec8dd7722 GIT binary patch literal 3889 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 literal 0 HcmV?d00001 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")