From b51e1dc98ab570edae3a8db303cd4871f85b1ddd Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Tue, 16 Apr 2024 15:27:06 +0200 Subject: [PATCH] feat(widgets/dock): BECDockArea as a container for BECDocks --- .../jupyter_console/jupyter_console_window.py | 36 +++- .../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 | 1 + bec_widgets/widgets/dock/dock_area.py | 173 ++++++++++++++++++ bec_widgets/widgets/plots/image.py | 4 +- 7 files changed, 236 insertions(+), 7 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_area.py diff --git a/bec_widgets/examples/jupyter_console/jupyter_console_window.py b/bec_widgets/examples/jupyter_console/jupyter_console_window.py index 0d080f61..c64c1e0c 100644 --- a/bec_widgets/examples/jupyter_console/jupyter_console_window.py +++ b/bec_widgets/examples/jupyter_console/jupyter_console_window.py @@ -2,13 +2,16 @@ import os import numpy as np import pyqtgraph as pg -from pyqtgraph.Qt import uic +from PyQt6.QtCore import QSize +from PyQt6.QtGui import QIcon +from PyQt6.QtWidgets import QMainWindow +from pyqtgraph.Qt import uic, QtWidgets from qtconsole.inprocess import QtInProcessKernelManager from qtconsole.rich_jupyter_widget import RichJupyterWidget from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget from bec_widgets.utils import BECDispatcher -from bec_widgets.widgets import BECFigure +from bec_widgets.widgets import BECFigure, BECDockArea class JupyterConsoleWidget(RichJupyterWidget): # pragma: no cover: @@ -47,9 +50,14 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover: self.console.kernel_manager.kernel.shell.push( { "fig": self.figure, + "dock": self.dock, "w1": self.w1, "w2": self.w2, "w3": self.w3, + "d1": self.d1, + "d2": self.d2, + "d3": self.d3, + "label_2": self.label_2, "bec": self.figure.client, "scans": self.figure.client.scans, "dev": self.figure.client.device_manager.devices, @@ -62,9 +70,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) @@ -86,6 +101,19 @@ 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.label_1 = QtWidgets.QLabel("some scan info label with useful information") + + self.label_2 = QtWidgets.QLabel("label which is added separately") + + self.d1 = self.dock.add_dock(widget=self.button_1, position="left") + self.d2 = self.dock.add_dock(widget=self.label_1, position="right") + self.d3 = self.dock.plot(x_name="samx", y_name="bpm4d") + self.d4 = self.dock.image(monitor="eiger") + + self.d4.set_vrange(0, 100) + if __name__ == "__main__": # pragma: no cover import sys @@ -96,6 +124,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 e66c1f8c..ff683d61 100644 --- a/bec_widgets/widgets/__init__.py +++ b/bec_widgets/widgets/__init__.py @@ -11,3 +11,4 @@ from .motor_control import ( from .motor_map import MotorMap from .plots import BECCurve, BECMotorMap, BECWaveform from .scan_control import ScanControl +from .dock import BECDockArea, BECDock diff --git a/bec_widgets/widgets/dock/__init__.py b/bec_widgets/widgets/dock/__init__.py new file mode 100644 index 00000000..0579eee6 --- /dev/null +++ b/bec_widgets/widgets/dock/__init__.py @@ -0,0 +1 @@ +from .dock_area import BECDockArea, BECDock diff --git a/bec_widgets/widgets/dock/dock_area.py b/bec_widgets/widgets/dock/dock_area.py new file mode 100644 index 00000000..6b25edb0 --- /dev/null +++ b/bec_widgets/widgets/dock/dock_area.py @@ -0,0 +1,173 @@ +import itertools +import warnings +from collections import defaultdict +from typing import Optional, Literal + +import pyqtgraph as pg +from PyQt6.QtWidgets import QWidget +from pydantic import Field +from pyqtgraph import QtWidgets +from pyqtgraph.dockarea.DockArea import DockArea, Dock +from bec_widgets.utils import BECConnector, ConnectionConfig +from bec_widgets.widgets import BECWaveform, BECFigure, BECMotorMap +from bec_widgets.widgets.plots import BECImageShow + + +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 DockAreaConfig(ConnectionConfig): + docks: dict[str, DockConfig] = Field({}, description="The docks in the dock area.") + + +class BECDock(BECConnector, Dock): + 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) # TODO test if it works + + def _remove_from_dock_area(self): + """Remove this dock from the DockArea it lives inside.""" + self.parent_dock_area.docks.pop(self.name()) # TODO test if works + + # def close(self): + # """Remove this dock from the DockArea it lives inside.""" + # if self._container is None: + # warnings.warn( + # f"Cannot close dock {self} because it is not open.", RuntimeWarning, stacklevel=2 + # ) + # return + # + # self.setParent(None) + # QtWidgets.QLabel.close(self.label) + # self.label.setParent(None) + # self._container.apoptose() + # self._container = None + # self.sigClosed.emit(self) + # # TODO add remove from dict from DockArea + + +class BECDockArea(BECConnector, DockArea): + USER_ACCESS = [] + + 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._docks = defaultdict(dict) # TODO check how is the pyqtgraph .docks implemented + self._last_state = None # TOOD not sure if this will ever work + + def figure(self, name: str = None) -> BECFigure: + figure = BECFigure(gui_id="remote") + self.add_dock(name=name, widget=figure, prefix="figure") + return figure + + def plot( + self, + x_name: str = None, + y_name: str = None, + name: str = None, + ) -> BECWaveform: + figure = BECFigure(gui_id="remote") + self.add_dock(name=name, widget=figure, prefix="plot") + + plot = figure.plot(x_name, y_name) + return plot + + def image(self, monitor: str = "eiger", name: str = None) -> BECImageShow: + figure = BECFigure(gui_id="remote") + self.add_dock(name=name, widget=figure, prefix="image") + + image = figure.image(monitor) + return image + + def motor_map(self, x_name: str = None, y_name: str = None, name: str = None) -> BECMotorMap: + figure = BECFigure(gui_id="remote") + self.add_dock(name=name, widget=figure, prefix="motor_map") + + motor_map = figure.motor_map(x_name, y_name) + return motor_map + + def add_dock( + self, + name: str = None, + widget: QWidget = None, + position: Literal["bottom", "top", "left", "right", "above", "below"] = None, + relative_to: Optional[BECDock] = None, + prefix: str = "dock", + ) -> BECDock: + if name is None: + name = self._generate_unique_dock_id(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, position) + + if widget is not None: + dock.addWidget(widget) + + return dock + + def _generate_unique_dock_id( + self, prefix: str = "widget" + ): # TODO can be taken directly from BECFigure or made some mixin from it + """Generate a unique dock id.""" + existing_ids = set(self.docks.keys()) + for i in itertools.count(1): + dock_id = f"{prefix}_{i}" + if dock_id not in existing_ids: + return dock_id + + def _remove_dock_by_id(self, dock_id: str): + ... + # TODO implement + # self.removeDock(self.docks[dock_id]) diff --git a/bec_widgets/widgets/plots/image.py b/bec_widgets/widgets/plots/image.py index 6b39797e..c6b2a40c 100644 --- a/bec_widgets/widgets/plots/image.py +++ b/bec_widgets/widgets/plots/image.py @@ -358,7 +358,7 @@ class BECImageShow(BECPlotBase): thread.start() - def find_widget_by_id(self, item_id: str) -> BECImageItem: + def find_widget_by_id(self, item_id: str) -> BECImageItem: # todo can be done in mixin """ Find the widget by its gui_id. Args: @@ -719,10 +719,8 @@ class BECImageShow(BECPlotBase): processing_config = image_to_update.config.processing self.processor.set_config(processing_config) if self.use_threading: - print("using threaded version") self._create_thread_worker(device, data) else: - print("using NON-threaded version") data = self.processor.process_image(data) self.update_image(device, data)