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