mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-04-08 09:47:52 +02:00
Compare commits
69 Commits
v2.8.1
...
fix/auto_u
| Author | SHA1 | Date | |
|---|---|---|---|
| ba42c65374 | |||
| 3d5dcced9c | |||
| d7f9936f4c | |||
| 34121bfe61 | |||
| 6fd1a2e7e1 | |||
| 67ccc7a2df | |||
| da47f467b0 | |||
| 64fc85f512 | |||
| 6d6bbe6cf5 | |||
| 5421b39fd9 | |||
| a81f3a0d3f | |||
| da65d145d5 | |||
| f24bc43837 | |||
| 404bc13179 | |||
| b3d4da7813 | |||
| fc44850c73 | |||
| 53bdf143ae | |||
| 7ce8be5aae | |||
| d3ee1e1b5b | |||
| 127e197b7b | |||
| b956bd6e67 | |||
| ad0d789685 | |||
| ac9ee09a38 | |||
| 98215f1266 | |||
| 571382ca97 | |||
| 421a2f76f4 | |||
| 22be20a799 | |||
| c2a83c0830 | |||
| 80c34688fb | |||
| c59871de52 | |||
| 0538f45d9c | |||
| b483e27533 | |||
| 6a63e3747a | |||
| a77367f2bd | |||
| b3beb1375e | |||
| 4c857d9f54 | |||
| ad30b5c970 | |||
| 98a46403f0 | |||
| 0da1ec8b4c | |||
| 45c3deee58 | |||
| f3edbc99f7 | |||
| 58a6ddd442 | |||
| 87bcc67307 | |||
| 45de9bf131 | |||
| 9af9ef3830 | |||
| 8debea4706 | |||
| 843143508b | |||
| 1701bc3f80 | |||
| 97109f71c4 | |||
| ae50ca282a | |||
| 8e6a22f917 | |||
| fc001934e3 | |||
| 78365a5233 | |||
| d7b4545795 | |||
| 2168a2acf0 | |||
| 80d4d0def6 | |||
| e1cc87d421 | |||
| 7719ac86b8 | |||
| 705e819352 | |||
| 1d98aed46e | |||
| 90c4460996 | |||
| f423e8463d | |||
| accaeed832 | |||
| 20028fc057 | |||
| 188fe4840f | |||
| 157eced745 | |||
| 8e8f7f4264 | |||
| ae3e2d7946 | |||
| 867ab574cb |
23
bec_widgets/applications/bw_launch.py
Normal file
23
bec_widgets/applications/bw_launch.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from bec_widgets.cli.auto_updates import AutoUpdates
|
||||
from bec_widgets.widgets.containers.dock.dock_area import BECDockArea
|
||||
|
||||
|
||||
def dock_area(object_name: str | None = None):
|
||||
_dock_area = BECDockArea(object_name=object_name)
|
||||
return _dock_area
|
||||
|
||||
|
||||
def auto_update_dock_area(object_name: str | None = None) -> BECDockArea:
|
||||
"""
|
||||
Create a dock area with auto update enabled.
|
||||
|
||||
Args:
|
||||
object_name(str): The name of the dock area.
|
||||
|
||||
Returns:
|
||||
BECDockArea: The created dock area.
|
||||
"""
|
||||
_dock_area = BECDockArea(object_name=object_name)
|
||||
_dock_area.set_auto_update(AutoUpdates)
|
||||
_dock_area.auto_update.enabled = True # type:ignore
|
||||
return _dock_area
|
||||
35
bec_widgets/applications/launch_dialog.ui
Normal file
35
bec_widgets/applications/launch_dialog.ui
Normal file
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>400</width>
|
||||
<height>300</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QPushButton" name="open_dock_area">
|
||||
<property name="text">
|
||||
<string>PushButton</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QPushButton" name="open_auto_update_dock_area">
|
||||
<property name="text">
|
||||
<string>PushButton</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
105
bec_widgets/applications/launch_window.py
Normal file
105
bec_widgets/applications/launch_window.py
Normal file
@@ -0,0 +1,105 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
from qtpy.QtWidgets import QApplication, QSizePolicy
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.cli.rpc.rpc_register import RPCRegister
|
||||
from bec_widgets.utils.container_utils import WidgetContainerUtils
|
||||
from bec_widgets.widgets.containers.dock.dock_area import BECDockArea
|
||||
from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow
|
||||
|
||||
logger = bec_logger.logger
|
||||
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
|
||||
class LaunchWindow(BECMainWindow):
|
||||
RPC = True
|
||||
|
||||
def __init__(
|
||||
self, parent=None, gui_id: str = None, window_title="BEC Launcher", *args, **kwargs
|
||||
):
|
||||
super().__init__(parent=parent, gui_id=gui_id, window_title=window_title, **kwargs)
|
||||
|
||||
self.app = QApplication.instance()
|
||||
|
||||
self.resize(500, 300)
|
||||
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
||||
|
||||
ui_file_path = os.path.join(MODULE_PATH, "applications/launch_dialog.ui")
|
||||
self.load_ui(ui_file_path)
|
||||
self.ui.open_dock_area.setText("Open Dock Area")
|
||||
self.ui.open_dock_area.clicked.connect(lambda: self.launch("dock_area"))
|
||||
self.ui.open_auto_update_dock_area.setText("Open Dock Area with Auto Update")
|
||||
self.ui.open_auto_update_dock_area.clicked.connect(
|
||||
lambda: self.launch("auto_update_dock_area", "auto_updates")
|
||||
)
|
||||
|
||||
def launch(
|
||||
self,
|
||||
launch_script: str,
|
||||
name: str | None = None,
|
||||
geometry: tuple[int, int, int, int] | None = None,
|
||||
) -> QWidget:
|
||||
"""Launch the specified script. If the launch script creates a QWidget, it will be
|
||||
embedded in a BECMainWindow. If the launch script creates a BECMainWindow, it will be shown
|
||||
as a separate window.
|
||||
|
||||
Args:
|
||||
launch_script(str): The name of the script to be launched.
|
||||
name(str): The name of the dock area.
|
||||
geometry(tuple): The geometry parameters to be passed to the dock area.
|
||||
Returns:
|
||||
QWidget: The created dock area.
|
||||
"""
|
||||
from bec_widgets.applications import bw_launch
|
||||
|
||||
with RPCRegister.delayed_broadcast() as rpc_register:
|
||||
existing_dock_areas = rpc_register.get_names_of_rpc_by_class_type(BECDockArea)
|
||||
if name is not None:
|
||||
if name in existing_dock_areas:
|
||||
raise ValueError(
|
||||
f"Name {name} must be unique for dock areas, but already exists: {existing_dock_areas}."
|
||||
)
|
||||
else:
|
||||
name = "dock_area"
|
||||
name = WidgetContainerUtils.generate_unique_name(name, existing_dock_areas)
|
||||
|
||||
if launch_script is None:
|
||||
launch_script = "dock_area"
|
||||
if not isinstance(launch_script, str):
|
||||
raise ValueError(f"Launch script must be a string, but got {type(launch_script)}.")
|
||||
launch = getattr(bw_launch, launch_script, None)
|
||||
if launch is None:
|
||||
raise ValueError(f"Launch script {launch_script} not found.")
|
||||
|
||||
result_widget = launch(name)
|
||||
result_widget.resize(result_widget.minimumSizeHint())
|
||||
# TODO Should we simply use the specified name as title here?
|
||||
result_widget.window().setWindowTitle(f"BEC - {name}")
|
||||
logger.info(f"Created new dock area: {name}")
|
||||
logger.info(f"Existing dock areas: {geometry}")
|
||||
if geometry is not None:
|
||||
result_widget.setGeometry(*geometry)
|
||||
if isinstance(result_widget, BECMainWindow):
|
||||
result_widget.show()
|
||||
else:
|
||||
window = BECMainWindow()
|
||||
window.setCentralWidget(result_widget)
|
||||
window.show()
|
||||
return result_widget
|
||||
|
||||
def show_launcher(self):
|
||||
self.show()
|
||||
|
||||
def hide_launcher(self):
|
||||
self.hide()
|
||||
|
||||
def cleanup(self):
|
||||
super().close()
|
||||
@@ -1,169 +1,295 @@
|
||||
# TODO autoupdate disabled
|
||||
# from __future__ import annotations
|
||||
#
|
||||
# import threading
|
||||
# from queue import Queue
|
||||
# from typing import TYPE_CHECKING
|
||||
#
|
||||
# from pydantic import BaseModel
|
||||
#
|
||||
# if TYPE_CHECKING:
|
||||
# from .client import BECDockArea, BECFigure
|
||||
#
|
||||
#
|
||||
# class ScanInfo(BaseModel):
|
||||
# scan_id: str
|
||||
# scan_number: int
|
||||
# scan_name: str
|
||||
# scan_report_devices: list
|
||||
# monitored_devices: list
|
||||
# status: str
|
||||
# model_config: dict = {"validate_assignment": True}
|
||||
#
|
||||
#
|
||||
# class AutoUpdates:
|
||||
# create_default_dock: bool = False
|
||||
# enabled: bool = False
|
||||
# dock_name: str = None
|
||||
#
|
||||
# def __init__(self, gui: BECDockArea):
|
||||
# self.gui = gui
|
||||
# self._default_dock = None
|
||||
# self._default_fig = None
|
||||
#
|
||||
# def start_default_dock(self):
|
||||
# """
|
||||
# Create a default dock for the auto updates.
|
||||
# """
|
||||
# self.dock_name = "default_figure"
|
||||
# self._default_dock = self.gui.new(self.dock_name)
|
||||
# self._default_dock.new("BECFigure")
|
||||
# self._default_fig = self._default_dock.elements_list[0]
|
||||
#
|
||||
# @staticmethod
|
||||
# def get_scan_info(msg) -> ScanInfo:
|
||||
# """
|
||||
# Update the script with the given data.
|
||||
# """
|
||||
# info = msg.info
|
||||
# status = msg.status
|
||||
# scan_id = msg.scan_id
|
||||
# scan_number = info.get("scan_number", 0)
|
||||
# scan_name = info.get("scan_name", "Unknown")
|
||||
# scan_report_devices = info.get("scan_report_devices", [])
|
||||
# monitored_devices = info.get("readout_priority", {}).get("monitored", [])
|
||||
# monitored_devices = [dev for dev in monitored_devices if dev not in scan_report_devices]
|
||||
# return ScanInfo(
|
||||
# scan_id=scan_id,
|
||||
# scan_number=scan_number,
|
||||
# scan_name=scan_name,
|
||||
# scan_report_devices=scan_report_devices,
|
||||
# monitored_devices=monitored_devices,
|
||||
# status=status,
|
||||
# )
|
||||
#
|
||||
# def get_default_figure(self) -> BECFigure | None:
|
||||
# """
|
||||
# Get the default figure from the GUI.
|
||||
# """
|
||||
# return self._default_fig
|
||||
#
|
||||
# def do_update(self, msg):
|
||||
# """
|
||||
# Run the update function if enabled.
|
||||
# """
|
||||
# if not self.enabled:
|
||||
# return
|
||||
# if msg.status != "open":
|
||||
# return
|
||||
# info = self.get_scan_info(msg)
|
||||
# return self.handler(info)
|
||||
#
|
||||
# def get_selected_device(self, monitored_devices, selected_device):
|
||||
# """
|
||||
# Get the selected device for the plot. If no device is selected, the first
|
||||
# device in the monitored devices list is selected.
|
||||
# """
|
||||
# if selected_device:
|
||||
# return selected_device
|
||||
# if len(monitored_devices) > 0:
|
||||
# sel_device = monitored_devices[0]
|
||||
# return sel_device
|
||||
# return None
|
||||
#
|
||||
# def handler(self, info: ScanInfo) -> None:
|
||||
# """
|
||||
# Default update function.
|
||||
# """
|
||||
# if info.scan_name == "line_scan" and info.scan_report_devices:
|
||||
# return self.simple_line_scan(info)
|
||||
# if info.scan_name == "grid_scan" and info.scan_report_devices:
|
||||
# return self.simple_grid_scan(info)
|
||||
# if info.scan_report_devices:
|
||||
# return self.best_effort(info)
|
||||
#
|
||||
# def simple_line_scan(self, info: ScanInfo) -> None:
|
||||
# """
|
||||
# Simple line scan.
|
||||
# """
|
||||
# fig = self.get_default_figure()
|
||||
# if not fig:
|
||||
# return
|
||||
# dev_x = info.scan_report_devices[0]
|
||||
# selected_device = yield self.gui.selected_device
|
||||
# dev_y = self.get_selected_device(info.monitored_devices, selected_device)
|
||||
# if not dev_y:
|
||||
# return
|
||||
# yield fig.clear_all()
|
||||
# yield fig.plot(
|
||||
# x_name=dev_x,
|
||||
# y_name=dev_y,
|
||||
# label=f"Scan {info.scan_number} - {dev_y}",
|
||||
# title=f"Scan {info.scan_number}",
|
||||
# x_label=dev_x,
|
||||
# y_label=dev_y,
|
||||
# )
|
||||
#
|
||||
# def simple_grid_scan(self, info: ScanInfo) -> None:
|
||||
# """
|
||||
# Simple grid scan.
|
||||
# """
|
||||
# fig = self.get_default_figure()
|
||||
# if not fig:
|
||||
# return
|
||||
# dev_x = info.scan_report_devices[0]
|
||||
# dev_y = info.scan_report_devices[1]
|
||||
# selected_device = yield self.gui.selected_device
|
||||
# dev_z = self.get_selected_device(info.monitored_devices, selected_device)
|
||||
# yield fig.clear_all()
|
||||
# yield fig.plot(
|
||||
# x_name=dev_x,
|
||||
# y_name=dev_y,
|
||||
# z_name=dev_z,
|
||||
# label=f"Scan {info.scan_number} - {dev_z}",
|
||||
# title=f"Scan {info.scan_number}",
|
||||
# x_label=dev_x,
|
||||
# y_label=dev_y,
|
||||
# )
|
||||
#
|
||||
# def best_effort(self, info: ScanInfo) -> None:
|
||||
# """
|
||||
# Best effort scan.
|
||||
# """
|
||||
# fig = self.get_default_figure()
|
||||
# if not fig:
|
||||
# return
|
||||
# dev_x = info.scan_report_devices[0]
|
||||
# selected_device = yield self.gui.selected_device
|
||||
# dev_y = self.get_selected_device(info.monitored_devices, selected_device)
|
||||
# if not dev_y:
|
||||
# return
|
||||
# yield fig.clear_all()
|
||||
# yield fig.plot(
|
||||
# x_name=dev_x,
|
||||
# y_name=dev_y,
|
||||
# label=f"Scan {info.scan_number} - {dev_y}",
|
||||
# title=f"Scan {info.scan_number}",
|
||||
# x_label=dev_x,
|
||||
# y_label=dev_y,
|
||||
# )
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Literal, overload
|
||||
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.logger import bec_logger
|
||||
from bec_lib.messages import ScanStatusMessage
|
||||
|
||||
from bec_widgets.utils.error_popups import SafeSlot
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.widgets.containers.dock.dock import BECDock
|
||||
from bec_widgets.widgets.containers.dock.dock_area import BECDockArea
|
||||
from bec_widgets.widgets.plots.image.image import Image
|
||||
from bec_widgets.widgets.plots.motor_map.motor_map import MotorMap
|
||||
from bec_widgets.widgets.plots.multi_waveform.multi_waveform import MultiWaveform
|
||||
from bec_widgets.widgets.plots.scatter_waveform.scatter_waveform import ScatterWaveform
|
||||
from bec_widgets.widgets.plots.waveform.waveform import Waveform
|
||||
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class AutoUpdates:
|
||||
_default_dock: BECDock
|
||||
|
||||
def __init__(self, dock_area: BECDockArea):
|
||||
self.dock_area = dock_area
|
||||
self.bec_dispatcher = dock_area.bec_dispatcher
|
||||
self._default_dock = None # type:ignore
|
||||
self.current_widget: BECWidget | None = None
|
||||
self.dock_name = None
|
||||
self._enabled = False
|
||||
|
||||
def connect(self):
|
||||
"""
|
||||
Establish all connections for the auto updates.
|
||||
"""
|
||||
self.bec_dispatcher.connect_slot(self._on_scan_status, MessageEndpoints.scan_status())
|
||||
|
||||
def disconnect(self):
|
||||
"""
|
||||
Disconnect all connections for the auto updates.
|
||||
"""
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self._on_scan_status, MessageEndpoints.scan_status() # type:ignore
|
||||
)
|
||||
|
||||
@SafeSlot()
|
||||
def _on_scan_status(self, content: dict, metadata: dict) -> None:
|
||||
"""
|
||||
Callback for scan status messages.
|
||||
"""
|
||||
msg = ScanStatusMessage(**content, metadata=metadata)
|
||||
|
||||
match msg.status:
|
||||
case "open":
|
||||
self.on_scan_open(msg)
|
||||
case "closed":
|
||||
self.on_scan_closed(msg)
|
||||
case ["aborted", "halted"]:
|
||||
self.on_scan_abort(msg)
|
||||
case _:
|
||||
pass
|
||||
|
||||
def start_default_dock(self):
|
||||
"""
|
||||
Create a default dock for the auto updates.
|
||||
"""
|
||||
self.dock_name = "update_dock"
|
||||
self._default_dock = self.dock_area.new(self.dock_name)
|
||||
self.current_widget = self._default_dock.new("Waveform")
|
||||
|
||||
@overload
|
||||
def set_dock_to_widget(self, widget: Literal["Waveform"]) -> Waveform: ...
|
||||
|
||||
@overload
|
||||
def set_dock_to_widget(self, widget: Literal["Image"]) -> Image: ...
|
||||
|
||||
@overload
|
||||
def set_dock_to_widget(self, widget: Literal["ScatterWaveform"]) -> ScatterWaveform: ...
|
||||
|
||||
@overload
|
||||
def set_dock_to_widget(self, widget: Literal["MotorMap"]) -> MotorMap: ...
|
||||
|
||||
@overload
|
||||
def set_dock_to_widget(self, widget: Literal["MultiWaveform"]) -> MultiWaveform: ...
|
||||
|
||||
def set_dock_to_widget(
|
||||
self,
|
||||
widget: Literal["Waveform", "Image", "ScatterWaveform", "MotorMap", "MultiWaveForm"] | str,
|
||||
) -> BECWidget:
|
||||
"""
|
||||
Set the dock to the widget.
|
||||
|
||||
Args:
|
||||
widget (str): The widget to set the dock to. Must be the name of a valid widget class.
|
||||
|
||||
Returns:
|
||||
BECWidget: The widget that was set.
|
||||
"""
|
||||
if self._default_dock is None or self.current_widget is None:
|
||||
logger.warning(
|
||||
f"Auto Updates: No default dock found. Creating a new one with name {self.dock_name}"
|
||||
)
|
||||
self.start_default_dock()
|
||||
assert self.current_widget is not None
|
||||
|
||||
if not self.current_widget.__class__.__name__ == widget:
|
||||
self._default_dock.delete(self.current_widget.object_name)
|
||||
self.current_widget = self._default_dock.new(widget)
|
||||
return self.current_widget
|
||||
|
||||
def get_selected_device(
|
||||
self, monitored_devices, selected_device: str | None = None
|
||||
) -> str | None:
|
||||
"""
|
||||
Get the selected device for the plot. If no device is selected, the first
|
||||
device in the monitored devices list is selected.
|
||||
"""
|
||||
|
||||
if selected_device is None:
|
||||
selected_device = self.dock_area.selected_device
|
||||
if selected_device:
|
||||
return selected_device
|
||||
if len(monitored_devices) > 0:
|
||||
sel_device = monitored_devices[0]
|
||||
return sel_device
|
||||
return None
|
||||
|
||||
@property
|
||||
def enabled(self) -> bool:
|
||||
"""
|
||||
Get the enabled status of the auto updates.
|
||||
"""
|
||||
return self._enabled
|
||||
|
||||
@enabled.setter
|
||||
def enabled(self, value: bool) -> None:
|
||||
"""
|
||||
Set the enabled status of the auto updates.
|
||||
"""
|
||||
if self._enabled == value:
|
||||
return
|
||||
self._enabled = value
|
||||
|
||||
if value:
|
||||
self.connect()
|
||||
self.on_start()
|
||||
else:
|
||||
self.disconnect()
|
||||
self.on_stop()
|
||||
|
||||
########################################################################
|
||||
################# Update Functions #####################################
|
||||
########################################################################
|
||||
|
||||
def simple_line_scan(self, info: ScanStatusMessage) -> None:
|
||||
"""
|
||||
Simple line scan.
|
||||
|
||||
Args:
|
||||
info (ScanStatusMessage): The scan status message.
|
||||
"""
|
||||
|
||||
# Set the dock to the waveform widget
|
||||
wf = self.set_dock_to_widget("Waveform")
|
||||
|
||||
# Get the scan report devices reported by the scan
|
||||
dev_x = info.scan_report_devices[0] # type:ignore
|
||||
|
||||
# For the y axis, get the selected device
|
||||
dev_y = self.get_selected_device(info.readout_priority["monitored"]) # type:ignore
|
||||
if not dev_y:
|
||||
return
|
||||
|
||||
# Clear the waveform widget and plot the data
|
||||
# with the scan number and device names
|
||||
# as the label and title
|
||||
wf.clear_all()
|
||||
wf.plot(
|
||||
x_name=dev_x,
|
||||
y_name=dev_y,
|
||||
label=f"Scan {info.scan_number} - {dev_y}",
|
||||
title=f"Scan {info.scan_number}",
|
||||
x_label=dev_x,
|
||||
y_label=dev_y,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Auto Update [simple_line_scan]: Started plot with: x_name={dev_x}, y_name={dev_y}"
|
||||
)
|
||||
|
||||
def simple_grid_scan(self, info: ScanStatusMessage) -> None:
|
||||
"""
|
||||
Simple grid scan.
|
||||
|
||||
Args:
|
||||
info (ScanStatusMessage): The scan status message.
|
||||
"""
|
||||
# Set the dock to the scatter waveform widget
|
||||
scatter = self.set_dock_to_widget("ScatterWaveform")
|
||||
|
||||
# Get the scan report devices reported by the scan
|
||||
dev_x, dev_y = info.scan_report_devices[0], info.scan_report_devices[1] # type:ignore
|
||||
dev_z = self.get_selected_device(info.readout_priority["monitored"]) # type:ignore
|
||||
|
||||
# Clear the scatter waveform widget and plot the data
|
||||
scatter.clear_all()
|
||||
scatter.plot(
|
||||
x_name=dev_x, y_name=dev_y, z_name=dev_z, label=f"Scan {info.scan_number} - {dev_z}"
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Auto Update [simple_grid_scan]: Started plot with: x_name={dev_x}, y_name={dev_y}, z_name={dev_z}"
|
||||
)
|
||||
|
||||
def best_effort(self, info: ScanStatusMessage) -> None:
|
||||
"""
|
||||
Best effort scan.
|
||||
|
||||
Args:
|
||||
info (ScanStatusMessage): The scan status message.
|
||||
"""
|
||||
|
||||
# If the scan report devices are empty, there is nothing we can do
|
||||
if not info.scan_report_devices:
|
||||
return
|
||||
dev_x = info.scan_report_devices[0] # type:ignore
|
||||
dev_y = self.get_selected_device(info.readout_priority["monitored"]) # type:ignore
|
||||
if not dev_y:
|
||||
return
|
||||
|
||||
# Set the dock to the waveform widget
|
||||
wf = self.set_dock_to_widget("Waveform")
|
||||
|
||||
# Clear the waveform widget and plot the data
|
||||
wf.clear_all()
|
||||
wf.plot(
|
||||
x_name=dev_x,
|
||||
y_name=dev_y,
|
||||
label=f"Scan {info.scan_number} - {dev_y}",
|
||||
title=f"Scan {info.scan_number}",
|
||||
x_label=dev_x,
|
||||
y_label=dev_y,
|
||||
)
|
||||
|
||||
logger.info(f"Auto Update [best_effort]: Started plot with: x_name={dev_x}, y_name={dev_y}")
|
||||
|
||||
#######################################################################
|
||||
################# GUI Callbacks #######################################
|
||||
#######################################################################
|
||||
|
||||
def on_start(self) -> None:
|
||||
"""
|
||||
Procedure to run when the auto updates are enabled.
|
||||
"""
|
||||
self.start_default_dock()
|
||||
|
||||
def on_stop(self) -> None:
|
||||
"""
|
||||
Procedure to run when the auto updates are disabled.
|
||||
"""
|
||||
|
||||
def on_scan_open(self, msg: ScanStatusMessage) -> None:
|
||||
"""
|
||||
Procedure to run when a scan starts.
|
||||
|
||||
Args:
|
||||
msg (ScanStatusMessage): The scan status message.
|
||||
"""
|
||||
if msg.scan_name == "line_scan" and msg.scan_report_devices:
|
||||
return self.simple_line_scan(msg)
|
||||
if msg.scan_name == "grid_scan" and msg.scan_report_devices:
|
||||
return self.simple_grid_scan(msg)
|
||||
if msg.scan_report_devices:
|
||||
return self.best_effort(msg)
|
||||
return None
|
||||
|
||||
def on_scan_closed(self, msg: ScanStatusMessage) -> None:
|
||||
"""
|
||||
Procedure to run when a scan ends.
|
||||
|
||||
Args:
|
||||
msg (ScanStatusMessage): The scan status message.
|
||||
"""
|
||||
|
||||
def on_scan_abort(self, msg: ScanStatusMessage) -> None:
|
||||
"""
|
||||
Procedure to run when a scan is aborted.
|
||||
|
||||
Args:
|
||||
msg (ScanStatusMessage): The scan status message.
|
||||
"""
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# This file was automatically generated by generate_cli.py
|
||||
# type: ignore
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -15,60 +16,28 @@ class Widgets(str, enum.Enum):
|
||||
Enum for the available widgets.
|
||||
"""
|
||||
|
||||
AbortButton = "AbortButton"
|
||||
BECColorMapWidget = "BECColorMapWidget"
|
||||
BECDockArea = "BECDockArea"
|
||||
BECProgressBar = "BECProgressBar"
|
||||
BECQueue = "BECQueue"
|
||||
BECStatusBox = "BECStatusBox"
|
||||
DapComboBox = "DapComboBox"
|
||||
DarkModeButton = "DarkModeButton"
|
||||
DeviceBrowser = "DeviceBrowser"
|
||||
DeviceComboBox = "DeviceComboBox"
|
||||
DeviceLineEdit = "DeviceLineEdit"
|
||||
Image = "Image"
|
||||
LMFitDialog = "LMFitDialog"
|
||||
LogPanel = "LogPanel"
|
||||
Minesweeper = "Minesweeper"
|
||||
MotorMap = "MotorMap"
|
||||
MultiWaveform = "MultiWaveform"
|
||||
PositionIndicator = "PositionIndicator"
|
||||
PositionerBox = "PositionerBox"
|
||||
PositionerBox2D = "PositionerBox2D"
|
||||
PositionerControlLine = "PositionerControlLine"
|
||||
ResetButton = "ResetButton"
|
||||
ResumeButton = "ResumeButton"
|
||||
RingProgressBar = "RingProgressBar"
|
||||
ScanControl = "ScanControl"
|
||||
ScatterWaveform = "ScatterWaveform"
|
||||
SignalComboBox = "SignalComboBox"
|
||||
SignalLineEdit = "SignalLineEdit"
|
||||
StopButton = "StopButton"
|
||||
TextBox = "TextBox"
|
||||
VSCodeEditor = "VSCodeEditor"
|
||||
Waveform = "Waveform"
|
||||
WebsiteWidget = "WebsiteWidget"
|
||||
|
||||
|
||||
class AbortButton(RPCBase):
|
||||
"""A button that abort the scan."""
|
||||
|
||||
@rpc_call
|
||||
def remove(self):
|
||||
"""
|
||||
Cleanup the BECConnector
|
||||
"""
|
||||
|
||||
|
||||
class BECColorMapWidget(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
def colormap(self):
|
||||
"""
|
||||
Get the current colormap name.
|
||||
"""
|
||||
|
||||
|
||||
class BECDock(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
@@ -328,13 +297,6 @@ class BECDockArea(RPCBase):
|
||||
Return all floating docks to the dock area.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def selected_device(self) -> "str":
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def save_state(self) -> "dict":
|
||||
"""
|
||||
@@ -344,6 +306,26 @@ class BECDockArea(RPCBase):
|
||||
dict: The state of the dock area.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def selected_device(self) -> "str | None":
|
||||
"""
|
||||
Get the selected device from the auto update config.
|
||||
|
||||
Returns:
|
||||
str: The selected device. If no device is selected, None is returned.
|
||||
"""
|
||||
|
||||
@selected_device.setter
|
||||
@rpc_call
|
||||
def selected_device(self) -> "str | None":
|
||||
"""
|
||||
Get the selected device from the auto update config.
|
||||
|
||||
Returns:
|
||||
str: The selected device. If no device is selected, None is returned.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def restore_state(
|
||||
self, state: "dict" = None, missing: "Literal['ignore', 'error']" = "ignore", extra="bottom"
|
||||
@@ -358,14 +340,6 @@ class BECDockArea(RPCBase):
|
||||
"""
|
||||
|
||||
|
||||
class BECMainWindow(RPCBase):
|
||||
@rpc_call
|
||||
def remove(self):
|
||||
"""
|
||||
Cleanup the BECConnector
|
||||
"""
|
||||
|
||||
|
||||
class BECProgressBar(RPCBase):
|
||||
"""A custom progress bar with smooth transitions. The displayed text can be customized using a template."""
|
||||
|
||||
@@ -625,15 +599,6 @@ class DapComboBox(RPCBase):
|
||||
"""
|
||||
|
||||
|
||||
class DarkModeButton(RPCBase):
|
||||
@rpc_call
|
||||
def toggle_dark_mode(self) -> "None":
|
||||
"""
|
||||
Toggle the dark mode state. This will change the theme of the entire
|
||||
application to dark or light mode.
|
||||
"""
|
||||
|
||||
|
||||
class DeviceBrowser(RPCBase):
|
||||
@rpc_call
|
||||
def remove(self):
|
||||
@@ -672,16 +637,6 @@ class DeviceLineEdit(RPCBase):
|
||||
"""
|
||||
|
||||
|
||||
class DeviceSignalInputBase(RPCBase):
|
||||
"""Mixin base class for device signal input widgets."""
|
||||
|
||||
@rpc_call
|
||||
def remove(self):
|
||||
"""
|
||||
Cleanup the BECConnector
|
||||
"""
|
||||
|
||||
|
||||
class Image(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
@@ -1341,16 +1296,6 @@ class ImageItem(RPCBase):
|
||||
"""
|
||||
|
||||
|
||||
class LMFitDialog(RPCBase):
|
||||
"""Dialog for displaying the fit summary and params for LMFit DAP processes"""
|
||||
|
||||
@rpc_call
|
||||
def remove(self):
|
||||
"""
|
||||
Cleanup the BECConnector
|
||||
"""
|
||||
|
||||
|
||||
class LogPanel(RPCBase):
|
||||
"""Displays a log panel"""
|
||||
|
||||
@@ -1373,9 +1318,6 @@ class LogPanel(RPCBase):
|
||||
"""
|
||||
|
||||
|
||||
class Minesweeper(RPCBase): ...
|
||||
|
||||
|
||||
class MotorMap(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
@@ -2213,64 +2155,6 @@ class PositionIndicator(RPCBase):
|
||||
"""
|
||||
|
||||
|
||||
class PositionerBox(RPCBase):
|
||||
"""Simple Widget to control a positioner in box form"""
|
||||
|
||||
@rpc_call
|
||||
def set_positioner(self, positioner: "str | Positioner"):
|
||||
"""
|
||||
Set the device
|
||||
|
||||
Args:
|
||||
positioner (Positioner | str) : Positioner to set, accepts str or the device
|
||||
"""
|
||||
|
||||
|
||||
class PositionerBox2D(RPCBase):
|
||||
"""Simple Widget to control two positioners in box form"""
|
||||
|
||||
@rpc_call
|
||||
def set_positioner_hor(self, positioner: "str | Positioner"):
|
||||
"""
|
||||
Set the device
|
||||
|
||||
Args:
|
||||
positioner (Positioner | str) : Positioner to set, accepts str or the device
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_positioner_ver(self, positioner: "str | Positioner"):
|
||||
"""
|
||||
Set the device
|
||||
|
||||
Args:
|
||||
positioner (Positioner | str) : Positioner to set, accepts str or the device
|
||||
"""
|
||||
|
||||
|
||||
class PositionerBoxBase(RPCBase):
|
||||
"""Contains some core logic for positioner box widgets"""
|
||||
|
||||
@rpc_call
|
||||
def remove(self):
|
||||
"""
|
||||
Cleanup the BECConnector
|
||||
"""
|
||||
|
||||
|
||||
class PositionerControlLine(RPCBase):
|
||||
"""A widget that controls a single device."""
|
||||
|
||||
@rpc_call
|
||||
def set_positioner(self, positioner: "str | Positioner"):
|
||||
"""
|
||||
Set the device
|
||||
|
||||
Args:
|
||||
positioner (Positioner | str) : Positioner to set, accepts str or the device
|
||||
"""
|
||||
|
||||
|
||||
class PositionerGroup(RPCBase):
|
||||
"""Simple Widget to control a positioner in box form"""
|
||||
|
||||
@@ -2283,26 +2167,6 @@ class PositionerGroup(RPCBase):
|
||||
"""
|
||||
|
||||
|
||||
class ResetButton(RPCBase):
|
||||
"""A button that resets the scan queue."""
|
||||
|
||||
@rpc_call
|
||||
def remove(self):
|
||||
"""
|
||||
Cleanup the BECConnector
|
||||
"""
|
||||
|
||||
|
||||
class ResumeButton(RPCBase):
|
||||
"""A button that continue scan queue."""
|
||||
|
||||
@rpc_call
|
||||
def remove(self):
|
||||
"""
|
||||
Cleanup the BECConnector
|
||||
"""
|
||||
|
||||
|
||||
class Ring(RPCBase):
|
||||
@rpc_call
|
||||
def _get_all_rpc(self) -> "dict":
|
||||
@@ -2590,16 +2454,6 @@ class ScanControl(RPCBase):
|
||||
"""
|
||||
|
||||
|
||||
class ScanMetadata(RPCBase):
|
||||
"""Dynamically generates a form for inclusion of metadata for a scan. Uses the"""
|
||||
|
||||
@rpc_call
|
||||
def remove(self):
|
||||
"""
|
||||
Cleanup the BECConnector
|
||||
"""
|
||||
|
||||
|
||||
class ScatterCurve(RPCBase):
|
||||
"""Scatter curve item for the scatter waveform widget."""
|
||||
|
||||
@@ -2953,36 +2807,6 @@ class ScatterWaveform(RPCBase):
|
||||
"""
|
||||
|
||||
|
||||
class SignalComboBox(RPCBase):
|
||||
"""Line edit widget for device input with autocomplete for device names."""
|
||||
|
||||
@rpc_call
|
||||
def remove(self):
|
||||
"""
|
||||
Cleanup the BECConnector
|
||||
"""
|
||||
|
||||
|
||||
class SignalLineEdit(RPCBase):
|
||||
"""Line edit widget for device input with autocomplete for device names."""
|
||||
|
||||
@rpc_call
|
||||
def remove(self):
|
||||
"""
|
||||
Cleanup the BECConnector
|
||||
"""
|
||||
|
||||
|
||||
class StopButton(RPCBase):
|
||||
"""A button that stops the current scan."""
|
||||
|
||||
@rpc_call
|
||||
def remove(self):
|
||||
"""
|
||||
Cleanup the BECConnector
|
||||
"""
|
||||
|
||||
|
||||
class TextBox(RPCBase):
|
||||
"""A widget that displays text in plain and HTML format"""
|
||||
|
||||
|
||||
@@ -10,11 +10,11 @@ import threading
|
||||
import time
|
||||
from contextlib import contextmanager
|
||||
from threading import Lock
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import TYPE_CHECKING, Literal, TypeAlias, cast
|
||||
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.logger import bec_logger
|
||||
from bec_lib.utils.import_utils import lazy_import, lazy_import_from
|
||||
from bec_lib.utils.import_utils import lazy_import_from
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
@@ -22,13 +22,17 @@ import bec_widgets.cli.client as client
|
||||
from bec_widgets.cli.rpc.rpc_base import RPCBase, RPCReference
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from bec_lib.redis_connector import StreamMessage
|
||||
from bec_lib.messages import GUIRegistryStateMessage
|
||||
else:
|
||||
StreamMessage = lazy_import_from("bec_lib.redis_connector", ("StreamMessage",))
|
||||
GUIRegistryStateMessage = lazy_import_from("bec_lib.messages", "GUIRegistryStateMessage")
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
IGNORE_WIDGETS = ["BECDockArea", "BECDock"]
|
||||
IGNORE_WIDGETS = ["LaunchWindow"]
|
||||
|
||||
RegistryState: TypeAlias = dict[
|
||||
Literal["gui_id", "name", "widget_class", "config", "__rpc__"], str | bool | dict
|
||||
]
|
||||
|
||||
# pylint: disable=redefined-outer-scope
|
||||
|
||||
@@ -67,7 +71,11 @@ def _get_output(process, logger) -> None:
|
||||
|
||||
|
||||
def _start_plot_process(
|
||||
gui_id: str, gui_class: type, gui_class_id: str, config: dict | str, logger=None
|
||||
gui_id: str,
|
||||
gui_class_id: str,
|
||||
config: dict | str,
|
||||
gui_class: str = "dock_area",
|
||||
logger=None, # FIXME change gui_class back to "launcher" later
|
||||
) -> tuple[subprocess.Popen[str], threading.Thread | None]:
|
||||
"""
|
||||
Start the plot in a new process.
|
||||
@@ -82,7 +90,7 @@ def _start_plot_process(
|
||||
"--id",
|
||||
gui_id,
|
||||
"--gui_class",
|
||||
gui_class.__name__,
|
||||
gui_class,
|
||||
"--gui_class_id",
|
||||
gui_class_id,
|
||||
"--hide",
|
||||
@@ -195,25 +203,29 @@ class BECGuiClient(RPCBase):
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self._lock = Lock()
|
||||
self._default_dock_name = "bec"
|
||||
self._anchor_widget = "launcher"
|
||||
self._auto_updates_enabled = True
|
||||
self._auto_updates = None
|
||||
self._killed = False
|
||||
self._top_level: dict[str, client.BECDockArea] = {}
|
||||
self._top_level: dict[str, RPCReference] = {}
|
||||
self._startup_timeout = 0
|
||||
self._gui_started_timer = None
|
||||
self._gui_started_event = threading.Event()
|
||||
self._process = None
|
||||
self._process_output_processing_thread = None
|
||||
self._exposed_widgets = []
|
||||
self._server_registry = {}
|
||||
self._ipython_registry = {}
|
||||
self._server_registry: dict[str, RegistryState] = {}
|
||||
self._ipython_registry: dict[str, RPCReference] = {}
|
||||
self.available_widgets = AvailableWidgetsNamespace()
|
||||
|
||||
####################
|
||||
#### Client API ####
|
||||
####################
|
||||
|
||||
@property
|
||||
def launcher(self) -> RPCBase:
|
||||
"""The launcher object."""
|
||||
return RPCBase(gui_id=f"{self._gui_id}:launcher", parent=self, object_name="launcher")
|
||||
|
||||
def connect_to_gui_server(self, gui_id: str) -> None:
|
||||
"""Connect to a GUI server"""
|
||||
# Unregister the old callback
|
||||
@@ -221,21 +233,25 @@ class BECGuiClient(RPCBase):
|
||||
MessageEndpoints.gui_registry_state(self._gui_id), cb=self._handle_registry_update
|
||||
)
|
||||
self._gui_id = gui_id
|
||||
# Get the registry state
|
||||
msgs = self._client.connector.xread(
|
||||
MessageEndpoints.gui_registry_state(self._gui_id), count=1
|
||||
)
|
||||
if msgs:
|
||||
self._handle_registry_update(msgs[0])
|
||||
|
||||
# reset the namespace
|
||||
self._update_dynamic_namespace({})
|
||||
self._server_registry = {}
|
||||
self._top_level = {}
|
||||
self._ipython_registry = {}
|
||||
|
||||
# Register the new callback
|
||||
self._client.connector.register(
|
||||
MessageEndpoints.gui_registry_state(self._gui_id), cb=self._handle_registry_update
|
||||
MessageEndpoints.gui_registry_state(self._gui_id),
|
||||
cb=self._handle_registry_update,
|
||||
parent=self,
|
||||
from_start=True,
|
||||
)
|
||||
|
||||
@property
|
||||
def windows(self) -> dict:
|
||||
"""Dictionary with dock areas in the GUI."""
|
||||
return self._top_level
|
||||
return {widget.object_name: widget for widget in self._top_level.values()}
|
||||
|
||||
@property
|
||||
def window_list(self) -> list:
|
||||
@@ -275,12 +291,12 @@ class BECGuiClient(RPCBase):
|
||||
self.start(wait=True)
|
||||
if wait:
|
||||
with wait_for_server(self):
|
||||
rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self)
|
||||
rpc_client = RPCBase(gui_id=f"{self._gui_id}:launcher", parent=self)
|
||||
widget = rpc_client._run_rpc(
|
||||
"new_dock_area", name, geometry
|
||||
"launch", "dock_area", name, geometry
|
||||
) # pylint: disable=protected-access
|
||||
return widget
|
||||
rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self)
|
||||
rpc_client = RPCBase(gui_id=f"{self._gui_id}:launcher", parent=self)
|
||||
widget = rpc_client._run_rpc(
|
||||
"new_dock_area", name, geometry
|
||||
) # pylint: disable=protected-access
|
||||
@@ -352,11 +368,13 @@ class BECGuiClient(RPCBase):
|
||||
# Wait for 'bec' gui to be registered, this may take some time
|
||||
# After 60s timeout. Should this raise an exception on timeout?
|
||||
while time.time() < time.time() + timeout:
|
||||
if len(list(self._server_registry.keys())) == 0:
|
||||
if len(list(self._server_registry.keys())) < 2 or not hasattr(
|
||||
self, self._anchor_widget
|
||||
):
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
break
|
||||
self._do_show_all()
|
||||
|
||||
self._gui_started_event.set()
|
||||
|
||||
def _start_server(self, wait: bool = False) -> None:
|
||||
@@ -369,8 +387,7 @@ class BECGuiClient(RPCBase):
|
||||
self._gui_started_event.clear()
|
||||
self._process, self._process_output_processing_thread = _start_plot_process(
|
||||
self._gui_id,
|
||||
self.__class__,
|
||||
gui_class_id=self._default_dock_name,
|
||||
gui_class_id="bec", # FIXME me experiment
|
||||
config=self._client._service_config.config, # pylint: disable=protected-access
|
||||
logger=logger,
|
||||
)
|
||||
@@ -380,7 +397,7 @@ class BECGuiClient(RPCBase):
|
||||
if callable(callback):
|
||||
callback()
|
||||
finally:
|
||||
threading.current_thread().cancel()
|
||||
threading.current_thread().cancel() # type: ignore
|
||||
|
||||
self._gui_started_timer = RepeatTimer(
|
||||
0.5, lambda: self._gui_is_alive() and gui_started_callback(self._gui_post_startup)
|
||||
@@ -390,25 +407,27 @@ class BECGuiClient(RPCBase):
|
||||
if wait:
|
||||
self._gui_started_event.wait()
|
||||
|
||||
def _dump(self):
|
||||
rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self)
|
||||
return rpc_client._run_rpc("_dump")
|
||||
|
||||
def _start(self, wait: bool = False) -> None:
|
||||
self._killed = False
|
||||
self._client.connector.register(
|
||||
MessageEndpoints.gui_registry_state(self._gui_id), cb=self._handle_registry_update
|
||||
MessageEndpoints.gui_registry_state(self._gui_id),
|
||||
cb=self._handle_registry_update,
|
||||
parent=self,
|
||||
)
|
||||
return self._start_server(wait=wait)
|
||||
|
||||
def _handle_registry_update(self, msg: StreamMessage) -> None:
|
||||
@staticmethod
|
||||
def _handle_registry_update(
|
||||
msg: dict[str, GUIRegistryStateMessage], parent: BECGuiClient
|
||||
) -> None:
|
||||
# This was causing a deadlock during shutdown, not sure why.
|
||||
# with self._lock:
|
||||
self._server_registry = msg["data"].state
|
||||
self._update_dynamic_namespace()
|
||||
self = parent
|
||||
self._server_registry = cast(dict[str, RegistryState], msg["data"].state)
|
||||
self._update_dynamic_namespace(self._server_registry)
|
||||
|
||||
def _do_show_all(self):
|
||||
rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self)
|
||||
rpc_client = RPCBase(gui_id=f"{self._gui_id}:launcher", parent=self)
|
||||
rpc_client._run_rpc("show") # pylint: disable=protected-access
|
||||
for window in self._top_level.values():
|
||||
window.show()
|
||||
@@ -419,124 +438,72 @@ class BECGuiClient(RPCBase):
|
||||
|
||||
def _hide_all(self):
|
||||
with wait_for_server(self):
|
||||
rpc_client = RPCBase(gui_id=f"{self._gui_id}:window", parent=self)
|
||||
rpc_client = RPCBase(gui_id=f"{self._gui_id}:launcher", parent=self)
|
||||
rpc_client._run_rpc("hide") # pylint: disable=protected-access
|
||||
if not self._killed:
|
||||
for window in self._top_level.values():
|
||||
window.hide()
|
||||
|
||||
def _update_dynamic_namespace(self):
|
||||
"""Update the dynamic name space"""
|
||||
# Clear the top level
|
||||
self._top_level.clear()
|
||||
# First we update the name space based on the new registry state
|
||||
self._add_registry_to_namespace()
|
||||
# Then we clear the ipython registry from old objects
|
||||
self._cleanup_ipython_registry()
|
||||
def _update_dynamic_namespace(self, server_registry: dict):
|
||||
"""
|
||||
Update the dynamic name space with the given server registry.
|
||||
Setting the server registry to an empty dictionary will remove all widgets from the namespace.
|
||||
|
||||
def _cleanup_ipython_registry(self):
|
||||
"""Cleanup the ipython registry"""
|
||||
names_in_registry = list(self._ipython_registry.keys())
|
||||
names_in_server_state = list(self._server_registry.keys())
|
||||
remove_ids = list(set(names_in_registry) - set(names_in_server_state))
|
||||
for widget_id in remove_ids:
|
||||
self._ipython_registry.pop(widget_id)
|
||||
self._cleanup_rpc_references_on_rpc_base(remove_ids)
|
||||
# Clear the exposed widgets
|
||||
self._exposed_widgets.clear() # No longer needed I think
|
||||
Args:
|
||||
server_registry (dict): The server registry
|
||||
"""
|
||||
top_level_widgets: dict[str, RPCReference] = {}
|
||||
for gui_id, state in server_registry.items():
|
||||
widget = self._add_widget(state, self)
|
||||
if widget is None:
|
||||
# ignore widgets that are not supported
|
||||
continue
|
||||
# get all top-level widgets. These are widgets that have no parent
|
||||
if not state["config"].get("parent_id"):
|
||||
top_level_widgets[gui_id] = widget
|
||||
|
||||
def _cleanup_rpc_references_on_rpc_base(self, remove_ids: list[str]) -> None:
|
||||
"""Cleanup the rpc references on the RPCBase object"""
|
||||
if not remove_ids:
|
||||
return
|
||||
for widget in self._ipython_registry.values():
|
||||
to_delete = []
|
||||
for attr_name, gui_id in widget._rpc_references.items():
|
||||
if gui_id in remove_ids:
|
||||
to_delete.append(attr_name)
|
||||
for attr_name in to_delete:
|
||||
if hasattr(widget, attr_name):
|
||||
delattr(widget, attr_name)
|
||||
if attr_name.startswith("elements."):
|
||||
delattr(widget.elements, attr_name.split(".")[1])
|
||||
widget._rpc_references.pop(attr_name)
|
||||
remove_from_registry = []
|
||||
for gui_id, widget in self._ipython_registry.items():
|
||||
if gui_id not in server_registry:
|
||||
remove_from_registry.append(gui_id)
|
||||
for gui_id in remove_from_registry:
|
||||
self._ipython_registry.pop(gui_id)
|
||||
|
||||
def _set_dynamic_attributes(self, obj: object, name: str, value: Any) -> None:
|
||||
"""Add an object to the namespace"""
|
||||
setattr(obj, name, value)
|
||||
|
||||
def _update_rpc_references(self, widget: RPCBase, name: str, gui_id: str) -> None:
|
||||
"""Update the RPC references"""
|
||||
widget._rpc_references[name] = gui_id
|
||||
|
||||
def _add_registry_to_namespace(self) -> None:
|
||||
"""Add registry to namespace"""
|
||||
# Add dock areas
|
||||
dock_area_states = [
|
||||
state
|
||||
for state in self._server_registry.values()
|
||||
if state["widget_class"] == "BECDockArea"
|
||||
removed_widgets = [
|
||||
widget.object_name for widget in self._top_level.values() if widget._is_deleted()
|
||||
]
|
||||
for state in dock_area_states:
|
||||
dock_area_ref = self._add_widget(state, self)
|
||||
dock_area = self._ipython_registry.get(dock_area_ref._gui_id)
|
||||
if not hasattr(dock_area, "elements"):
|
||||
self._set_dynamic_attributes(dock_area, "elements", WidgetNameSpace())
|
||||
self._set_dynamic_attributes(self, dock_area.widget_name, dock_area_ref)
|
||||
# Keep track of rpc references on RPCBase object
|
||||
self._update_rpc_references(self, dock_area.widget_name, dock_area_ref._gui_id)
|
||||
# Add dock_area to the top level
|
||||
self._top_level[dock_area_ref.widget_name] = dock_area_ref
|
||||
self._exposed_widgets.append(dock_area_ref._gui_id)
|
||||
|
||||
# Add docks
|
||||
dock_states = [
|
||||
state
|
||||
for state in self._server_registry.values()
|
||||
if state["config"].get("parent_id", "") == dock_area_ref._gui_id
|
||||
]
|
||||
for state in dock_states:
|
||||
dock_ref = self._add_widget(state, dock_area)
|
||||
dock = self._ipython_registry.get(dock_ref._gui_id)
|
||||
self._set_dynamic_attributes(dock_area, dock_ref.widget_name, dock_ref)
|
||||
# Keep track of rpc references on RPCBase object
|
||||
self._update_rpc_references(dock_area, dock_ref.widget_name, dock_ref._gui_id)
|
||||
# Keep track of exposed docks
|
||||
self._exposed_widgets.append(dock_ref._gui_id)
|
||||
for widget_name in removed_widgets:
|
||||
# the check is not strictly necessary, but better safe
|
||||
# than sorry; who knows what the user has done
|
||||
if hasattr(self, widget_name):
|
||||
delattr(self, widget_name)
|
||||
|
||||
# Add widgets
|
||||
widget_states = [
|
||||
state
|
||||
for state in self._server_registry.values()
|
||||
if state["config"].get("parent_id", "") == dock_ref._gui_id
|
||||
]
|
||||
for state in widget_states:
|
||||
widget_ref = self._add_widget(state, dock)
|
||||
self._set_dynamic_attributes(dock, widget_ref.widget_name, widget_ref)
|
||||
self._set_dynamic_attributes(
|
||||
dock_area.elements, widget_ref.widget_name, widget_ref
|
||||
)
|
||||
# Keep track of rpc references on RPCBase object
|
||||
self._update_rpc_references(
|
||||
dock_area, f"elements.{widget_ref.widget_name}", widget_ref._gui_id
|
||||
)
|
||||
self._update_rpc_references(dock, widget_ref.widget_name, widget_ref._gui_id)
|
||||
# Keep track of exposed widgets
|
||||
self._exposed_widgets.append(widget_ref._gui_id)
|
||||
for gui_id, widget_ref in top_level_widgets.items():
|
||||
setattr(self, widget_ref.object_name, widget_ref)
|
||||
|
||||
def _add_widget(self, state: dict, parent: object) -> RPCReference:
|
||||
self._top_level = top_level_widgets
|
||||
|
||||
for widget in self._ipython_registry.values():
|
||||
widget._refresh_references()
|
||||
|
||||
def _add_widget(self, state: dict, parent: object) -> RPCReference | None:
|
||||
"""Add a widget to the namespace
|
||||
|
||||
Args:
|
||||
state (dict): The state of the widget from the _server_registry.
|
||||
parent (object): The parent object.
|
||||
"""
|
||||
name = state["name"]
|
||||
object_name = state["object_name"]
|
||||
gui_id = state["gui_id"]
|
||||
widget_class = getattr(client, state["widget_class"])
|
||||
if state["widget_class"] in IGNORE_WIDGETS:
|
||||
return
|
||||
widget_class = getattr(client, state["widget_class"], None)
|
||||
if widget_class is None:
|
||||
return
|
||||
obj = self._ipython_registry.get(gui_id)
|
||||
if obj is None:
|
||||
widget = widget_class(gui_id=gui_id, name=name, parent=parent)
|
||||
widget = widget_class(gui_id=gui_id, object_name=object_name, parent=parent)
|
||||
self._ipython_registry[gui_id] = widget
|
||||
else:
|
||||
widget = obj
|
||||
|
||||
@@ -30,7 +30,8 @@ else:
|
||||
|
||||
class ClientGenerator:
|
||||
def __init__(self):
|
||||
self.header = """# This file was automatically generated by generate_cli.py\n
|
||||
self.header = """# This file was automatically generated by generate_cli.py
|
||||
# type: ignore \n
|
||||
from __future__ import annotations
|
||||
import enum
|
||||
from typing import Literal, Optional, overload
|
||||
|
||||
@@ -4,7 +4,7 @@ import inspect
|
||||
import threading
|
||||
import uuid
|
||||
from functools import wraps
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from bec_lib.client import BECClient
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
@@ -15,6 +15,9 @@ import bec_widgets.cli.client as client
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from bec_lib import messages
|
||||
from bec_lib.connector import MessageObject
|
||||
|
||||
from bec_widgets.cli.client_utils import BECGuiClient
|
||||
|
||||
else:
|
||||
messages = lazy_import("bec_lib.messages")
|
||||
# from bec_lib.connector import MessageObject
|
||||
@@ -38,7 +41,7 @@ def rpc_call(func):
|
||||
def wrapper(self, *args, **kwargs):
|
||||
# we could rely on a strict type check here, but this is more flexible
|
||||
# moreover, it would anyway crash for objects...
|
||||
caller_frame = inspect.currentframe().f_back
|
||||
caller_frame = inspect.currentframe().f_back # type: ignore
|
||||
while caller_frame:
|
||||
if "jedi" in caller_frame.f_globals:
|
||||
# Jedi module is present, likely tab completion
|
||||
@@ -88,13 +91,21 @@ class RPCReference:
|
||||
def __init__(self, registry: dict, gui_id: str) -> None:
|
||||
self._registry = registry
|
||||
self._gui_id = gui_id
|
||||
self.object_name = self._registry[self._gui_id].object_name
|
||||
|
||||
@check_for_deleted_widget
|
||||
def __getattr__(self, name):
|
||||
if name in ["_registry", "_gui_id"]:
|
||||
if name in ["_registry", "_gui_id", "_is_deleted", "_name", "object_name"]:
|
||||
return super().__getattribute__(name)
|
||||
return self._registry[self._gui_id].__getattribute__(name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if name in ["_registry", "_gui_id", "_is_deleted", "_name", "object_name"]:
|
||||
return super().__setattr__(name, value)
|
||||
if self._gui_id not in self._registry:
|
||||
raise DeletedWidgetError(f"Widget with gui_id {self._gui_id} has been deleted")
|
||||
self._registry[self._gui_id].__setattr__(name, value)
|
||||
|
||||
@check_for_deleted_widget
|
||||
def __getitem__(self, key):
|
||||
return self._registry[self._gui_id].__getitem__(key)
|
||||
@@ -114,19 +125,22 @@ class RPCReference:
|
||||
return []
|
||||
return self._registry[self._gui_id].__dir__()
|
||||
|
||||
def _is_deleted(self) -> bool:
|
||||
return self._gui_id not in self._registry
|
||||
|
||||
|
||||
class RPCBase:
|
||||
def __init__(
|
||||
self,
|
||||
gui_id: str | None = None,
|
||||
config: dict | None = None,
|
||||
name: str | None = None,
|
||||
object_name: str | None = None,
|
||||
parent=None,
|
||||
) -> None:
|
||||
self._client = BECClient() # BECClient is a singleton; here, we simply get the instance
|
||||
self._config = config if config is not None else {}
|
||||
self._gui_id = gui_id if gui_id is not None else str(uuid.uuid4())[:5]
|
||||
self._name = name if name is not None else str(uuid.uuid4())[:5]
|
||||
self.object_name = object_name if object_name is not None else str(uuid.uuid4())[:5]
|
||||
self._parent = parent
|
||||
self._msg_wait_event = threading.Event()
|
||||
self._rpc_response = None
|
||||
@@ -149,10 +163,10 @@ class RPCBase:
|
||||
"""
|
||||
Get the widget name.
|
||||
"""
|
||||
return self._name
|
||||
return self.object_name
|
||||
|
||||
@property
|
||||
def _root(self):
|
||||
def _root(self) -> BECGuiClient:
|
||||
"""
|
||||
Get the root widget. This is the BECFigure widget that holds
|
||||
the anchor gui_id.
|
||||
@@ -161,9 +175,9 @@ class RPCBase:
|
||||
# pylint: disable=protected-access
|
||||
while parent._parent is not None:
|
||||
parent = parent._parent
|
||||
return parent
|
||||
return parent # type: ignore
|
||||
|
||||
def _run_rpc(self, method, *args, wait_for_rpc_response=True, timeout=3, **kwargs) -> Any:
|
||||
def _run_rpc(self, method, *args, wait_for_rpc_response=True, timeout=300, **kwargs) -> Any:
|
||||
"""
|
||||
Run the RPC call.
|
||||
|
||||
@@ -205,7 +219,11 @@ class RPCBase:
|
||||
self._client.connector.unregister(
|
||||
MessageEndpoints.gui_instruction_response(request_id), cb=self._on_rpc_response
|
||||
)
|
||||
# get class name
|
||||
|
||||
# we can assume that the response is a RequestResponseMessage, updated by
|
||||
# the _on_rpc_response method
|
||||
assert isinstance(self._rpc_response, messages.RequestResponseMessage)
|
||||
|
||||
if not self._rpc_response.accepted:
|
||||
raise ValueError(self._rpc_response.message["error"])
|
||||
msg_result = self._rpc_response.message.get("result")
|
||||
@@ -213,8 +231,8 @@ class RPCBase:
|
||||
return self._create_widget_from_msg_result(msg_result)
|
||||
|
||||
@staticmethod
|
||||
def _on_rpc_response(msg: MessageObject, parent: RPCBase) -> None:
|
||||
msg = msg.value
|
||||
def _on_rpc_response(msg_obj: MessageObject, parent: RPCBase) -> None:
|
||||
msg = cast(messages.RequestResponseMessage, msg_obj.value)
|
||||
parent._msg_wait_event.set()
|
||||
parent._rpc_response = msg
|
||||
|
||||
@@ -236,13 +254,14 @@ class RPCBase:
|
||||
|
||||
cls = getattr(client, cls)
|
||||
# The namespace of the object will be updated dynamically on the client side
|
||||
# Therefor it is important to check if the object is already in the registry
|
||||
# Therefore it is important to check if the object is already in the registry
|
||||
# If yes, we return the reference to the object, otherwise we create a new object
|
||||
# pylint: disable=protected-access
|
||||
if msg_result["gui_id"] in self._root._ipython_registry:
|
||||
return RPCReference(self._root._ipython_registry, msg_result["gui_id"])
|
||||
ret = cls(parent=self, **msg_result)
|
||||
self._root._ipython_registry[ret._gui_id] = ret
|
||||
self._refresh_references()
|
||||
obj = RPCReference(self._root._ipython_registry, ret._gui_id)
|
||||
return obj
|
||||
# return ret
|
||||
@@ -258,3 +277,27 @@ class RPCBase:
|
||||
if heart.status == messages.BECStatus.RUNNING:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _refresh_references(self):
|
||||
"""
|
||||
Refresh the references.
|
||||
"""
|
||||
with self._root._lock:
|
||||
references = {}
|
||||
for key, val in self._root._server_registry.items():
|
||||
parent_id = val["config"].get("parent_id")
|
||||
if parent_id == self._gui_id:
|
||||
references[key] = {
|
||||
"gui_id": val["config"]["gui_id"],
|
||||
"object_name": val["object_name"],
|
||||
}
|
||||
removed_references = set(self._rpc_references.keys()) - set(references.keys())
|
||||
for key in removed_references:
|
||||
delattr(self, self._rpc_references[key]["object_name"])
|
||||
self._rpc_references = references
|
||||
for key, val in references.items():
|
||||
setattr(
|
||||
self,
|
||||
val["object_name"],
|
||||
RPCReference(self._root._ipython_registry, val["gui_id"]),
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import wraps
|
||||
from threading import Lock, RLock
|
||||
from threading import RLock
|
||||
from typing import TYPE_CHECKING, Callable
|
||||
from weakref import WeakValueDictionary
|
||||
|
||||
@@ -77,7 +77,7 @@ class RPCRegister:
|
||||
self._rpc_register[rpc.gui_id] = rpc
|
||||
|
||||
@broadcast_update
|
||||
def remove_rpc(self, rpc: str):
|
||||
def remove_rpc(self, rpc: BECConnector):
|
||||
"""
|
||||
Remove an RPC object from the register.
|
||||
|
||||
@@ -113,7 +113,7 @@ class RPCRegister:
|
||||
return connections
|
||||
|
||||
def get_names_of_rpc_by_class_type(
|
||||
self, cls: BECWidget | BECConnector | BECDock | BECDockArea
|
||||
self, cls: type[BECWidget] | type[BECConnector] | type[BECDock] | type[BECDockArea]
|
||||
) -> list[str]:
|
||||
"""Get all the names of the widgets.
|
||||
|
||||
@@ -123,7 +123,7 @@ class RPCRegister:
|
||||
# This retrieves any rpc objects that are subclass of BECWidget,
|
||||
# i.e. curve and image items are excluded
|
||||
widgets = [rpc for rpc in self._rpc_register.values() if isinstance(rpc, cls)]
|
||||
return [widget._name for widget in widgets]
|
||||
return [widget.object_name for widget in widgets]
|
||||
|
||||
def broadcast(self):
|
||||
"""
|
||||
@@ -170,7 +170,8 @@ class RPCRegisterBroadcast:
|
||||
|
||||
def __exit__(self, *exc):
|
||||
"""Exit the context manager"""
|
||||
|
||||
self._call_depth -= 1 # Remove nested calls
|
||||
if self._call_depth == 0: # Last one to exit is repsonsible for broadcasting
|
||||
if self._call_depth == 0: # The Last one to exit is responsible for broadcasting
|
||||
self.rpc_register._skip_broadcast = False
|
||||
self.rpc_register.broadcast()
|
||||
|
||||
@@ -38,7 +38,7 @@ class RPCWidgetHandler:
|
||||
cls.__name__: cls for cls in clss.widgets if cls.__name__ not in IGNORE_WIDGETS
|
||||
}
|
||||
|
||||
def create_widget(self, widget_type, name: str | None = None, **kwargs) -> BECWidget:
|
||||
def create_widget(self, widget_type, **kwargs) -> BECWidget:
|
||||
"""
|
||||
Create a widget from an RPC message.
|
||||
|
||||
@@ -52,7 +52,7 @@ class RPCWidgetHandler:
|
||||
"""
|
||||
widget_class = self.widget_classes.get(widget_type) # type: ignore
|
||||
if widget_class:
|
||||
return widget_class(name=name, **kwargs)
|
||||
return widget_class(**kwargs)
|
||||
raise ValueError(f"Unknown widget type: {widget_type}")
|
||||
|
||||
|
||||
|
||||
@@ -1,188 +1,27 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import types
|
||||
from contextlib import contextmanager, redirect_stderr, redirect_stdout
|
||||
from typing import Union
|
||||
from contextlib import redirect_stderr, redirect_stdout
|
||||
from typing import cast
|
||||
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.logger import bec_logger
|
||||
from bec_lib.service_config import ServiceConfig
|
||||
from bec_lib.utils.import_utils import lazy_import
|
||||
from qtpy.QtCore import Qt, QTimer
|
||||
from redis.exceptions import RedisError
|
||||
from qtpy.QtCore import QSize, Qt
|
||||
from qtpy.QtGui import QIcon
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.applications.launch_window import LaunchWindow
|
||||
from bec_widgets.cli.rpc.rpc_register import RPCRegister
|
||||
from bec_widgets.utils import BECDispatcher
|
||||
from bec_widgets.utils.bec_connector import BECConnector
|
||||
from bec_widgets.utils.error_popups import ErrorPopupUtility
|
||||
from bec_widgets.widgets.containers.dock import BECDockArea
|
||||
from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow
|
||||
from bec_widgets.utils.bec_dispatcher import BECDispatcher
|
||||
|
||||
messages = lazy_import("bec_lib.messages")
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
@contextmanager
|
||||
def rpc_exception_hook(err_func):
|
||||
"""This context replaces the popup message box for error display with a specific hook"""
|
||||
# get error popup utility singleton
|
||||
popup = ErrorPopupUtility()
|
||||
# save current setting
|
||||
old_exception_hook = popup.custom_exception_hook
|
||||
|
||||
# install err_func, if it is a callable
|
||||
# IMPORTANT, Keep self here, because this method is overwriting the custom_exception_hook
|
||||
# of the ErrorPopupUtility (popup instance) class.
|
||||
def custom_exception_hook(self, exc_type, value, tb, **kwargs):
|
||||
err_func({"error": popup.get_error_message(exc_type, value, tb)})
|
||||
|
||||
popup.custom_exception_hook = types.MethodType(custom_exception_hook, popup)
|
||||
|
||||
try:
|
||||
yield popup
|
||||
finally:
|
||||
# restore state of error popup utility singleton
|
||||
popup.custom_exception_hook = old_exception_hook
|
||||
|
||||
|
||||
class BECWidgetsCLIServer:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
gui_id: str,
|
||||
dispatcher: BECDispatcher = None,
|
||||
client=None,
|
||||
config=None,
|
||||
gui_class: type[BECDockArea] = BECDockArea,
|
||||
gui_class_id: str = "bec",
|
||||
) -> None:
|
||||
self.status = messages.BECStatus.BUSY
|
||||
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
|
||||
# register broadcast callback
|
||||
self.rpc_register = RPCRegister()
|
||||
self.rpc_register.add_callback(self.broadcast_registry_update)
|
||||
|
||||
self.dispatcher.connect_slot(
|
||||
self.on_rpc_update, MessageEndpoints.gui_instructions(self.gui_id)
|
||||
)
|
||||
|
||||
# Setup QTimer for heartbeat
|
||||
self._heartbeat_timer = QTimer()
|
||||
self._heartbeat_timer.timeout.connect(self.emit_heartbeat)
|
||||
self._heartbeat_timer.start(200)
|
||||
|
||||
self.status = messages.BECStatus.RUNNING
|
||||
with RPCRegister.delayed_broadcast():
|
||||
self.gui = gui_class(parent=None, name=gui_class_id, gui_id=gui_class_id)
|
||||
logger.success(f"Server started with gui_id: {self.gui_id}")
|
||||
# Create initial object -> BECFigure or BECDockArea
|
||||
|
||||
def on_rpc_update(self, msg: dict, metadata: dict):
|
||||
request_id = metadata.get("request_id")
|
||||
logger.debug(f"Received RPC instruction: {msg}, metadata: {metadata}")
|
||||
with rpc_exception_hook(functools.partial(self.send_response, request_id, False)):
|
||||
try:
|
||||
obj = self.get_object_from_config(msg["parameter"])
|
||||
method = msg["action"]
|
||||
args = msg["parameter"].get("args", [])
|
||||
kwargs = msg["parameter"].get("kwargs", {})
|
||||
res = self.run_rpc(obj, method, args, kwargs)
|
||||
except Exception as e:
|
||||
logger.error(f"Error while executing RPC instruction: {e}")
|
||||
self.send_response(request_id, False, {"error": str(e)})
|
||||
else:
|
||||
logger.debug(f"RPC instruction executed successfully: {res}")
|
||||
self.send_response(request_id, True, {"result": res})
|
||||
|
||||
def send_response(self, request_id: str, accepted: bool, msg: dict):
|
||||
self.client.connector.set_and_publish(
|
||||
MessageEndpoints.gui_instruction_response(request_id),
|
||||
messages.RequestResponseMessage(accepted=accepted, message=msg),
|
||||
expire=60,
|
||||
)
|
||||
|
||||
def get_object_from_config(self, config: dict):
|
||||
gui_id = config.get("gui_id")
|
||||
obj = self.rpc_register.get_rpc_by_id(gui_id)
|
||||
if obj is None:
|
||||
raise ValueError(f"Object with gui_id {gui_id} not found")
|
||||
return obj
|
||||
|
||||
def run_rpc(self, obj, method, args, kwargs):
|
||||
# Run with rpc registry broadcast, but only once
|
||||
with RPCRegister.delayed_broadcast():
|
||||
logger.debug(f"Running RPC instruction: {method} with args: {args}, kwargs: {kwargs}")
|
||||
method_obj = getattr(obj, method)
|
||||
# check if the method accepts args and kwargs
|
||||
if not callable(method_obj):
|
||||
if not args:
|
||||
res = method_obj
|
||||
else:
|
||||
setattr(obj, method, args[0])
|
||||
res = None
|
||||
else:
|
||||
res = method_obj(*args, **kwargs)
|
||||
|
||||
if isinstance(res, list):
|
||||
res = [self.serialize_object(obj) for obj in res]
|
||||
elif isinstance(res, dict):
|
||||
res = {key: self.serialize_object(val) for key, val in res.items()}
|
||||
else:
|
||||
res = self.serialize_object(res)
|
||||
return res
|
||||
|
||||
def serialize_object(self, obj):
|
||||
if isinstance(obj, BECConnector):
|
||||
config = obj.config.model_dump()
|
||||
config["parent_id"] = obj.parent_id # add parent_id to config
|
||||
return {
|
||||
"gui_id": obj.gui_id,
|
||||
"name": (
|
||||
obj._name if hasattr(obj, "_name") else obj.__class__.__name__
|
||||
), # pylint: disable=protected-access
|
||||
"widget_class": obj.__class__.__name__,
|
||||
"config": config,
|
||||
"__rpc__": True,
|
||||
}
|
||||
return obj
|
||||
|
||||
def emit_heartbeat(self):
|
||||
logger.trace(f"Emitting heartbeat for {self.gui_id}")
|
||||
try:
|
||||
self.client.connector.set(
|
||||
MessageEndpoints.gui_heartbeat(self.gui_id),
|
||||
messages.StatusMessage(name=self.gui_id, status=self.status, info={}),
|
||||
expire=10,
|
||||
)
|
||||
except RedisError as exc:
|
||||
logger.error(f"Error while emitting heartbeat: {exc}")
|
||||
|
||||
def broadcast_registry_update(self, connections: dict):
|
||||
"""
|
||||
Broadcast the updated registry to all clients.
|
||||
"""
|
||||
|
||||
# We only need to broadcast the dock areas
|
||||
data = {key: self.serialize_object(val) for key, val in connections.items()}
|
||||
self.client.connector.xadd(
|
||||
MessageEndpoints.gui_registry_state(self.gui_id),
|
||||
msg_dict={"data": messages.GUIRegistryStateMessage(state=data)},
|
||||
max_size=1, # only single message in stream
|
||||
)
|
||||
|
||||
def shutdown(self): # TODO not sure if needed when cleanup is done at level of BECConnector
|
||||
self.status = messages.BECStatus.IDLE
|
||||
self._heartbeat_timer.stop()
|
||||
self.emit_heartbeat()
|
||||
logger.info("Succeded in shutting down gui")
|
||||
self.client.shutdown()
|
||||
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
||||
|
||||
|
||||
class SimpleFileLikeFromLogOutputFunc:
|
||||
@@ -203,40 +42,134 @@ class SimpleFileLikeFromLogOutputFunc:
|
||||
return
|
||||
|
||||
|
||||
def _start_server(
|
||||
gui_id: str, gui_class: BECDockArea, gui_class_id: str = "bec", config: str | None = None
|
||||
):
|
||||
if config:
|
||||
try:
|
||||
config = json.loads(config)
|
||||
service_config = ServiceConfig(config=config)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
service_config = ServiceConfig(config_path=config)
|
||||
else:
|
||||
# if no config is provided, use the default config
|
||||
service_config = ServiceConfig()
|
||||
class GUIServer:
|
||||
"""
|
||||
This class is used to start the BEC GUI and is the main entry point for launching BEC Widgets in a subprocess.
|
||||
"""
|
||||
|
||||
# bec_logger.configure(
|
||||
# service_config.redis,
|
||||
# QtRedisConnector,
|
||||
# service_name="BECWidgetsCLIServer",
|
||||
# service_config=service_config.service_config,
|
||||
# )
|
||||
server = BECWidgetsCLIServer(
|
||||
gui_id=gui_id, config=service_config, gui_class=gui_class, gui_class_id=gui_class_id
|
||||
)
|
||||
return server
|
||||
def __init__(self, args):
|
||||
self.config = args.config
|
||||
self.gui_id = args.id
|
||||
self.gui_class = args.gui_class
|
||||
self.gui_class_id = args.gui_class_id
|
||||
self.hide = args.hide
|
||||
self.app: QApplication | None = None
|
||||
self.launcher_window: LaunchWindow | None = None
|
||||
self.dispatcher: BECDispatcher | None = None
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
Start the GUI server.
|
||||
"""
|
||||
bec_logger.level = bec_logger.LOGLEVEL.INFO
|
||||
if self.hide:
|
||||
# pylint: disable=protected-access
|
||||
bec_logger._stderr_log_level = bec_logger.LOGLEVEL.ERROR
|
||||
bec_logger._update_sinks()
|
||||
|
||||
with redirect_stdout(SimpleFileLikeFromLogOutputFunc(logger.info)): # type: ignore
|
||||
with redirect_stderr(SimpleFileLikeFromLogOutputFunc(logger.error)): # type: ignore
|
||||
self._run()
|
||||
|
||||
def _get_service_config(self) -> ServiceConfig:
|
||||
if self.config:
|
||||
try:
|
||||
config = json.loads(self.config)
|
||||
service_config = ServiceConfig(config=config)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
service_config = ServiceConfig(config_path=config)
|
||||
else:
|
||||
# if no config is provided, use the default config
|
||||
service_config = ServiceConfig()
|
||||
return service_config
|
||||
|
||||
def _turn_off_the_lights(self, connections: dict):
|
||||
"""
|
||||
If there is only one connection remaining, it is the launcher, so we show it.
|
||||
Once the launcher is closed as the last window, we quit the application.
|
||||
"""
|
||||
self.launcher_window = cast(LaunchWindow, self.launcher_window)
|
||||
|
||||
if len(connections) <= 1:
|
||||
self.launcher_window.show()
|
||||
self.launcher_window.activateWindow()
|
||||
self.launcher_window.raise_()
|
||||
if self.app:
|
||||
self.app.setQuitOnLastWindowClosed(True)
|
||||
else:
|
||||
self.launcher_window.hide()
|
||||
if self.app:
|
||||
self.app.setQuitOnLastWindowClosed(False)
|
||||
|
||||
def _run(self):
|
||||
"""
|
||||
Run the GUI server.
|
||||
"""
|
||||
self.app = QApplication(sys.argv)
|
||||
self.app.setApplicationName("BEC")
|
||||
self.app.gui_id = self.gui_id # type: ignore
|
||||
self.setup_bec_icon()
|
||||
|
||||
service_config = self._get_service_config()
|
||||
self.dispatcher = BECDispatcher(config=service_config, gui_id=self.gui_id)
|
||||
# self.dispatcher.start_cli_server(gui_id=self.gui_id)
|
||||
|
||||
self.launcher_window = LaunchWindow(gui_id=f"{self.gui_id}:launcher")
|
||||
self.launcher_window.setAttribute(Qt.WA_ShowWithoutActivating) # type: ignore
|
||||
|
||||
self.app.aboutToQuit.connect(self.shutdown)
|
||||
self.app.setQuitOnLastWindowClosed(False)
|
||||
|
||||
register = RPCRegister()
|
||||
register.callbacks.append(self._turn_off_the_lights)
|
||||
register.broadcast()
|
||||
|
||||
if self.gui_class:
|
||||
# If the server is started with a specific gui class, we launch it.
|
||||
# This will automatically hide the launcher.
|
||||
self.launcher_window.launch(self.gui_class, name=self.gui_class_id)
|
||||
|
||||
def sigint_handler(*args):
|
||||
# display message, for people to let it terminate gracefully
|
||||
print("Caught SIGINT, exiting")
|
||||
# Widgets should be all closed.
|
||||
with RPCRegister.delayed_broadcast():
|
||||
for widget in QApplication.instance().topLevelWidgets(): # type: ignore
|
||||
widget.close()
|
||||
if self.app:
|
||||
self.app.quit()
|
||||
|
||||
signal.signal(signal.SIGINT, sigint_handler)
|
||||
signal.signal(signal.SIGTERM, sigint_handler)
|
||||
|
||||
sys.exit(self.app.exec())
|
||||
|
||||
def setup_bec_icon(self):
|
||||
"""
|
||||
Set the BEC icon for the application
|
||||
"""
|
||||
if self.app is None:
|
||||
return
|
||||
icon = QIcon()
|
||||
icon.addFile(
|
||||
os.path.join(MODULE_PATH, "assets", "app_icons", "bec_widgets_icon.png"),
|
||||
size=QSize(48, 48),
|
||||
)
|
||||
self.app.setWindowIcon(icon)
|
||||
|
||||
def shutdown(self):
|
||||
"""
|
||||
Shutdown the GUI server.
|
||||
"""
|
||||
if self.dispatcher:
|
||||
self.dispatcher.stop_cli_server()
|
||||
self.dispatcher.disconnect_all()
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
import os
|
||||
|
||||
from qtpy.QtCore import QSize
|
||||
from qtpy.QtGui import QIcon
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
import bec_widgets
|
||||
"""
|
||||
Main entry point for subprocesses that start a GUI server.
|
||||
"""
|
||||
|
||||
parser = argparse.ArgumentParser(description="BEC Widgets CLI Server")
|
||||
parser.add_argument("--id", type=str, default="test", help="The id of the server")
|
||||
@@ -256,69 +189,12 @@ def main():
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
bec_logger.level = bec_logger.LOGLEVEL.INFO
|
||||
if args.hide:
|
||||
# pylint: disable=protected-access
|
||||
bec_logger._stderr_log_level = bec_logger.LOGLEVEL.ERROR
|
||||
bec_logger._update_sinks()
|
||||
|
||||
if 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 = BECDockArea
|
||||
|
||||
with redirect_stdout(SimpleFileLikeFromLogOutputFunc(logger.info)):
|
||||
with redirect_stderr(SimpleFileLikeFromLogOutputFunc(logger.error)):
|
||||
app = QApplication(sys.argv)
|
||||
# set close on last window, only if not under control of client ;
|
||||
# indeed, Qt considers a hidden window a closed window, so if all windows
|
||||
# are hidden by default it exits
|
||||
app.setQuitOnLastWindowClosed(not args.hide)
|
||||
module_path = os.path.dirname(bec_widgets.__file__)
|
||||
icon = QIcon()
|
||||
icon.addFile(
|
||||
os.path.join(module_path, "assets", "app_icons", "bec_widgets_icon.png"),
|
||||
size=QSize(48, 48),
|
||||
)
|
||||
app.setWindowIcon(icon)
|
||||
# store gui id within QApplication object, to make it available to all widgets
|
||||
app.gui_id = args.id
|
||||
|
||||
# args.id = "abff6"
|
||||
server = _start_server(args.id, gui_class, args.gui_class_id, args.config)
|
||||
|
||||
win = BECMainWindow(gui_id=f"{server.gui_id}:window")
|
||||
win.setAttribute(Qt.WA_ShowWithoutActivating)
|
||||
win.setWindowTitle("BEC")
|
||||
|
||||
RPCRegister().add_rpc(win)
|
||||
gui = server.gui
|
||||
win.setCentralWidget(gui)
|
||||
if not args.hide:
|
||||
win.show()
|
||||
|
||||
app.aboutToQuit.connect(server.shutdown)
|
||||
|
||||
def sigint_handler(*args):
|
||||
# display message, for people to let it terminate gracefully
|
||||
print("Caught SIGINT, exiting")
|
||||
# Widgets should be all closed.
|
||||
with RPCRegister.delayed_broadcast():
|
||||
for widget in QApplication.instance().topLevelWidgets():
|
||||
widget.close()
|
||||
app.quit()
|
||||
|
||||
# gui.bec.close()
|
||||
# win.shutdown()
|
||||
signal.signal(signal.SIGINT, sigint_handler)
|
||||
signal.signal(signal.SIGTERM, sigint_handler)
|
||||
|
||||
sys.exit(app.exec())
|
||||
server = GUIServer(args)
|
||||
server.start()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# import sys
|
||||
|
||||
# sys.argv = ["bec_widgets", "--gui_class", "MainWindow"]
|
||||
main()
|
||||
|
||||
@@ -34,31 +34,34 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||
super().__init__(parent)
|
||||
|
||||
self._init_ui()
|
||||
self.app = QApplication.instance()
|
||||
|
||||
# console push
|
||||
if self.console.inprocess is True:
|
||||
self.console.kernel_manager.kernel.shell.push(
|
||||
{
|
||||
"np": np,
|
||||
"app": self.app,
|
||||
"pg": pg,
|
||||
"wh": wh,
|
||||
"dock": self.dock,
|
||||
"im": self.im,
|
||||
"mi": self.mi,
|
||||
"mm": self.mm,
|
||||
"lm": self.lm,
|
||||
"btn1": self.btn1,
|
||||
"btn2": self.btn2,
|
||||
"btn3": self.btn3,
|
||||
"btn4": self.btn4,
|
||||
"btn5": self.btn5,
|
||||
"btn6": self.btn6,
|
||||
"pb": self.pb,
|
||||
"pi": self.pi,
|
||||
"dock_area": self.dock_area,
|
||||
"dock_1": self.dock_1,
|
||||
"wf": self.wf,
|
||||
"scatter": self.scatter,
|
||||
"scatter_mi": self.scatter,
|
||||
"mwf": self.mwf,
|
||||
# "dock_2": self.dock_2,
|
||||
# "mm": self.mm,
|
||||
# "lm": self.lm,
|
||||
# "btn1": self.btn1,
|
||||
# "btn2": self.btn2,
|
||||
# "btn3": self.btn3,
|
||||
# "btn4": self.btn4,
|
||||
# "btn5": self.btn5,
|
||||
# "btn6": self.btn6,
|
||||
# "pb": self.pb,
|
||||
# "pi": self.pi,
|
||||
# "wf": self.wf,
|
||||
# "scatter": self.scatter,
|
||||
# "scatter_mi": self.scatter,
|
||||
# "mwf": self.mwf,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -71,80 +74,75 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||
|
||||
tab_widget = QTabWidget(splitter)
|
||||
|
||||
first_tab = QWidget()
|
||||
first_tab_layout = QVBoxLayout(first_tab)
|
||||
self.dock = BECDockArea(gui_id="dock")
|
||||
first_tab_layout.addWidget(self.dock)
|
||||
tab_widget.addTab(first_tab, "Dock Area")
|
||||
|
||||
third_tab = QWidget()
|
||||
third_tab_layout = QVBoxLayout(third_tab)
|
||||
self.lm = LayoutManagerWidget()
|
||||
third_tab_layout.addWidget(self.lm)
|
||||
tab_widget.addTab(third_tab, "Layout Manager Widget")
|
||||
|
||||
fourth_tab = QWidget()
|
||||
fourth_tab_layout = QVBoxLayout(fourth_tab)
|
||||
self.pb = PlotBase()
|
||||
self.pi = self.pb.plot_item
|
||||
fourth_tab_layout.addWidget(self.pb)
|
||||
tab_widget.addTab(fourth_tab, "PlotBase")
|
||||
|
||||
tab_widget.setCurrentIndex(3)
|
||||
|
||||
group_box = QGroupBox("Jupyter Console", splitter)
|
||||
group_box_layout = QVBoxLayout(group_box)
|
||||
self.console = BECJupyterConsole(inprocess=True)
|
||||
group_box_layout.addWidget(self.console)
|
||||
|
||||
# Some buttons for layout testing
|
||||
self.btn1 = QPushButton("Button 1")
|
||||
self.btn2 = QPushButton("Button 2")
|
||||
self.btn3 = QPushButton("Button 3")
|
||||
self.btn4 = QPushButton("Button 4")
|
||||
self.btn5 = QPushButton("Button 5")
|
||||
self.btn6 = QPushButton("Button 6")
|
||||
first_tab = QWidget()
|
||||
first_tab_layout = QVBoxLayout(first_tab)
|
||||
self.dock_area = BECDockArea(gui_id="dock")
|
||||
first_tab_layout.addWidget(self.dock_area)
|
||||
tab_widget.addTab(first_tab, "Dock Area")
|
||||
self._init_dock()
|
||||
|
||||
fifth_tab = QWidget()
|
||||
fifth_tab_layout = QVBoxLayout(fifth_tab)
|
||||
self.wf = Waveform()
|
||||
fifth_tab_layout.addWidget(self.wf)
|
||||
tab_widget.addTab(fifth_tab, "Waveform Next Gen")
|
||||
tab_widget.setCurrentIndex(4)
|
||||
|
||||
sixth_tab = QWidget()
|
||||
sixth_tab_layout = QVBoxLayout(sixth_tab)
|
||||
self.im = Image()
|
||||
self.mi = self.im.main_image
|
||||
sixth_tab_layout.addWidget(self.im)
|
||||
tab_widget.addTab(sixth_tab, "Image Next Gen")
|
||||
tab_widget.setCurrentIndex(5)
|
||||
|
||||
seventh_tab = QWidget()
|
||||
seventh_tab_layout = QVBoxLayout(seventh_tab)
|
||||
self.scatter = ScatterWaveform()
|
||||
self.scatter_mi = self.scatter.main_curve
|
||||
self.scatter.plot("samx", "samy", "bpm4i")
|
||||
seventh_tab_layout.addWidget(self.scatter)
|
||||
tab_widget.addTab(seventh_tab, "Scatter Waveform")
|
||||
tab_widget.setCurrentIndex(6)
|
||||
|
||||
eighth_tab = QWidget()
|
||||
eighth_tab_layout = QVBoxLayout(eighth_tab)
|
||||
self.mm = MotorMap()
|
||||
eighth_tab_layout.addWidget(self.mm)
|
||||
tab_widget.addTab(eighth_tab, "Motor Map")
|
||||
tab_widget.setCurrentIndex(7)
|
||||
|
||||
ninth_tab = QWidget()
|
||||
ninth_tab_layout = QVBoxLayout(ninth_tab)
|
||||
self.mwf = MultiWaveform()
|
||||
ninth_tab_layout.addWidget(self.mwf)
|
||||
tab_widget.addTab(ninth_tab, "MultiWaveform")
|
||||
tab_widget.setCurrentIndex(8)
|
||||
|
||||
# add stuff to the new Waveform widget
|
||||
self._init_waveform()
|
||||
# third_tab = QWidget()
|
||||
# third_tab_layout = QVBoxLayout(third_tab)
|
||||
# self.lm = LayoutManagerWidget()
|
||||
# third_tab_layout.addWidget(self.lm)
|
||||
# tab_widget.addTab(third_tab, "Layout Manager Widget")
|
||||
#
|
||||
# fourth_tab = QWidget()
|
||||
# fourth_tab_layout = QVBoxLayout(fourth_tab)
|
||||
# self.pb = PlotBase()
|
||||
# self.pi = self.pb.plot_item
|
||||
# fourth_tab_layout.addWidget(self.pb)
|
||||
# tab_widget.addTab(fourth_tab, "PlotBase")
|
||||
#
|
||||
# tab_widget.setCurrentIndex(3)
|
||||
#
|
||||
#
|
||||
#
|
||||
# # Some buttons for layout testing
|
||||
# self.btn1 = QPushButton("Button 1")
|
||||
# self.btn2 = QPushButton("Button 2")
|
||||
# self.btn3 = QPushButton("Button 3")
|
||||
# self.btn4 = QPushButton("Button 4")
|
||||
# self.btn5 = QPushButton("Button 5")
|
||||
# self.btn6 = QPushButton("Button 6")
|
||||
#
|
||||
# fifth_tab = QWidget()
|
||||
# fifth_tab_layout = QVBoxLayout(fifth_tab)
|
||||
# self.wf = Waveform()
|
||||
# fifth_tab_layout.addWidget(self.wf)
|
||||
# tab_widget.addTab(fifth_tab, "Waveform Next Gen")
|
||||
# tab_widget.setCurrentIndex(4)
|
||||
#
|
||||
# seventh_tab = QWidget()
|
||||
# seventh_tab_layout = QVBoxLayout(seventh_tab)
|
||||
# self.scatter = ScatterWaveform()
|
||||
# self.scatter_mi = self.scatter.main_curve
|
||||
# self.scatter.plot("samx", "samy", "bpm4i")
|
||||
# seventh_tab_layout.addWidget(self.scatter)
|
||||
# tab_widget.addTab(seventh_tab, "Scatter Waveform")
|
||||
# tab_widget.setCurrentIndex(6)
|
||||
#
|
||||
# eighth_tab = QWidget()
|
||||
# eighth_tab_layout = QVBoxLayout(eighth_tab)
|
||||
# self.mm = MotorMap()
|
||||
# eighth_tab_layout.addWidget(self.mm)
|
||||
# tab_widget.addTab(eighth_tab, "Motor Map")
|
||||
# tab_widget.setCurrentIndex(7)
|
||||
#
|
||||
# ninth_tab = QWidget()
|
||||
# ninth_tab_layout = QVBoxLayout(ninth_tab)
|
||||
# self.mwf = MultiWaveform()
|
||||
# ninth_tab_layout.addWidget(self.mwf)
|
||||
# tab_widget.addTab(ninth_tab, "MultiWaveform")
|
||||
# tab_widget.setCurrentIndex(0)
|
||||
#
|
||||
# # add stuff to the new Waveform widget
|
||||
# self._init_waveform()
|
||||
|
||||
self.setWindowTitle("Jupyter Console Window")
|
||||
|
||||
@@ -152,10 +150,15 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||
self.wf.plot(y_name="bpm4i", y_entry="bpm4i", dap="GaussianModel")
|
||||
self.wf.plot(y_name="bpm3a", y_entry="bpm3a", dap="GaussianModel")
|
||||
|
||||
def _init_dock(self):
|
||||
self.dock_1 = self.dock_area.new(name="dock_0")
|
||||
self.wf = self.dock_1.new(widget="Waveform")
|
||||
# self.dock_2 = self.dock_area.new(widget="DarkModeButton")
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""Override to handle things when main window is closed."""
|
||||
self.dock.cleanup()
|
||||
self.dock.close()
|
||||
self.dock_area.cleanup()
|
||||
self.dock_area.close()
|
||||
self.console.close()
|
||||
|
||||
super().closeEvent(event)
|
||||
|
||||
@@ -10,21 +10,23 @@ from typing import TYPE_CHECKING, Optional
|
||||
from bec_lib.logger import bec_logger
|
||||
from bec_lib.utils.import_utils import lazy_import_from
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from qtpy.QtCore import QObject, QRunnable, QThreadPool, Signal
|
||||
from qtpy.QtCore import QObject, QRunnable, Qt, QThreadPool, Signal
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from bec_widgets.cli.rpc.rpc_register import RPCRegister
|
||||
from bec_widgets.utils.container_utils import WidgetContainerUtils
|
||||
from bec_widgets.utils.error_popups import ErrorPopupUtility
|
||||
from bec_widgets.utils.error_popups import SafeSlot as pyqtSlot
|
||||
from bec_widgets.utils.widget_io import WidgetHierarchy
|
||||
from bec_widgets.utils.yaml_dialog import load_yaml, load_yaml_gui, save_yaml, save_yaml_gui
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from bec_widgets.utils.bec_dispatcher import BECDispatcher
|
||||
from bec_widgets.widgets.containers.dock import BECDock
|
||||
else:
|
||||
BECDispatcher = lazy_import_from("bec_widgets.utils.bec_dispatcher", ("BECDispatcher",))
|
||||
|
||||
logger = bec_logger.logger
|
||||
BECDispatcher = lazy_import_from("bec_widgets.utils.bec_dispatcher", ("BECDispatcher",))
|
||||
|
||||
|
||||
class ConnectionConfig(BaseModel):
|
||||
@@ -82,14 +84,21 @@ class BECConnector:
|
||||
client=None,
|
||||
config: ConnectionConfig | None = None,
|
||||
gui_id: str | None = None,
|
||||
name: str | None = None,
|
||||
parent_dock: BECDock | None = None,
|
||||
object_name: str | None = None,
|
||||
parent_dock: BECDock | None = None, # TODO should go away
|
||||
parent_id: str | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
# Extract object_name from kwargs to not pass it to Qt class
|
||||
object_name = object_name or kwargs.pop("objectName", None)
|
||||
# Ensure the parent is always the first argument for QObject
|
||||
parent = kwargs.pop("parent", None)
|
||||
# This initializes the QObject or any qt related class
|
||||
super().__init__(parent=parent, **kwargs)
|
||||
# BEC related connections
|
||||
self.bec_dispatcher = BECDispatcher(client=client)
|
||||
self.client = self.bec_dispatcher.client if client is None else client
|
||||
self._parent_dock = parent_dock
|
||||
self._parent_dock = parent_dock # TODO also remove at some point
|
||||
|
||||
if not self.client in BECConnector.EXIT_HANDLERS:
|
||||
# register function to clean connections at exit;
|
||||
@@ -122,12 +131,24 @@ class BECConnector:
|
||||
self.gui_id: str = gui_id # Keep namespace in sync
|
||||
else:
|
||||
self.gui_id: str = self.config.gui_id # type: ignore
|
||||
if name is None:
|
||||
name = self.__class__.__name__
|
||||
else:
|
||||
if not WidgetContainerUtils.has_name_valid_chars(name):
|
||||
raise ValueError(f"Name {name} contains invalid characters.")
|
||||
self._name = name if name else self.__class__.__name__
|
||||
|
||||
# TODO Hierarchy can be refreshed upon creation -> also registry should be notified if objectName changes -> issue #472
|
||||
if object_name is not None:
|
||||
self.setObjectName(object_name)
|
||||
|
||||
# 1) If no objectName is set, set the initial name
|
||||
if not self.objectName():
|
||||
self.setObjectName(self.__class__.__name__)
|
||||
self.object_name = self.objectName()
|
||||
|
||||
# 2) Enforce unique objectName among siblings with the same BECConnector parent
|
||||
self.setParent(parent)
|
||||
if parent_id is None:
|
||||
connector_parent = WidgetHierarchy._get_becwidget_ancestor(self)
|
||||
if connector_parent is not None:
|
||||
self.parent_id = connector_parent.gui_id
|
||||
|
||||
self._enforce_unique_sibling_name()
|
||||
self.rpc_register = RPCRegister()
|
||||
self.rpc_register.add_rpc(self)
|
||||
|
||||
@@ -138,6 +159,49 @@ class BECConnector:
|
||||
# Store references to running workers so they're not garbage collected prematurely.
|
||||
self._workers = []
|
||||
|
||||
def _enforce_unique_sibling_name(self):
|
||||
"""
|
||||
Enforce that this BECConnector has a unique objectName among its siblings.
|
||||
|
||||
Sibling logic:
|
||||
- If there's a nearest BECConnector parent, only compare with children of that parent.
|
||||
- If parent is None (i.e., top-level object), compare with all other top-level BECConnectors.
|
||||
"""
|
||||
parent_bec = WidgetHierarchy._get_becwidget_ancestor(self)
|
||||
|
||||
if parent_bec:
|
||||
# We have a parent => only compare with siblings under that parent
|
||||
siblings = parent_bec.findChildren(BECConnector)
|
||||
else:
|
||||
# No parent => treat all top-level BECConnectors as siblings
|
||||
# 1) Gather all BECConnectors from QApplication
|
||||
all_widgets = QApplication.allWidgets()
|
||||
all_bec = [w for w in all_widgets if isinstance(w, BECConnector)]
|
||||
# 2) "Top-level" means closest BECConnector parent is None
|
||||
top_level_bec = [
|
||||
w for w in all_bec if WidgetHierarchy._get_becwidget_ancestor(w) is None
|
||||
]
|
||||
# 3) We are among these top-level siblings
|
||||
siblings = top_level_bec
|
||||
|
||||
# Collect used names among siblings
|
||||
used_names = {sib.objectName() for sib in siblings if sib is not self}
|
||||
|
||||
base_name = self.objectName()
|
||||
if base_name not in used_names:
|
||||
# Name is already unique among siblings
|
||||
return
|
||||
|
||||
# Need a suffix to avoid collision
|
||||
counter = 0
|
||||
while True:
|
||||
trial_name = f"{base_name}_{counter}"
|
||||
if trial_name not in used_names:
|
||||
self.setObjectName(trial_name)
|
||||
self.object_name = trial_name
|
||||
break
|
||||
counter += 1
|
||||
|
||||
def submit_task(self, fn, *args, on_complete: pyqtSlot = None, **kwargs) -> Worker:
|
||||
"""
|
||||
Submit a task to run in a separate thread. The task will run the specified
|
||||
@@ -316,8 +380,9 @@ class BECConnector:
|
||||
def remove(self):
|
||||
"""Cleanup the BECConnector"""
|
||||
# If the widget is attached to a dock, remove it from the dock.
|
||||
# TODO this should be handled by dock and dock are not by BECConnector
|
||||
if self._parent_dock is not None:
|
||||
self._parent_dock.delete(self._name)
|
||||
self._parent_dock.delete(self.object_name)
|
||||
# If the widget is from Qt, trigger its close method.
|
||||
elif hasattr(self, "close"):
|
||||
self.close()
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import collections
|
||||
import random
|
||||
import string
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING, Union
|
||||
|
||||
@@ -17,6 +19,8 @@ logger = bec_logger.logger
|
||||
if TYPE_CHECKING:
|
||||
from bec_lib.endpoints import EndpointInfo
|
||||
|
||||
from bec_widgets.utils.cli_server import CLIServer
|
||||
|
||||
|
||||
class QtThreadSafeCallback(QObject):
|
||||
cb_signal = pyqtSignal(dict, dict)
|
||||
@@ -73,14 +77,24 @@ class BECDispatcher:
|
||||
|
||||
_instance = None
|
||||
_initialized = False
|
||||
client: BECClient
|
||||
cli_server: CLIServer | None = None
|
||||
|
||||
def __new__(cls, client=None, config: str = None, *args, **kwargs):
|
||||
# TODO add custom gui id for server
|
||||
def __new__(
|
||||
cls,
|
||||
client=None,
|
||||
config: str | ServiceConfig | None = None,
|
||||
gui_id: str = None,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
if cls._instance is None:
|
||||
cls._instance = super(BECDispatcher, cls).__new__(cls)
|
||||
cls._initialized = False
|
||||
return cls._instance
|
||||
|
||||
def __init__(self, client=None, config: str | ServiceConfig = None):
|
||||
def __init__(self, client=None, config: str | ServiceConfig | None = None, gui_id: str = None):
|
||||
if self._initialized:
|
||||
return
|
||||
|
||||
@@ -108,10 +122,15 @@ class BECDispatcher:
|
||||
logger.warning("Could not connect to Redis, skipping start of BECClient.")
|
||||
|
||||
logger.success("Initialized BECDispatcher")
|
||||
|
||||
self.start_cli_server(gui_id=gui_id)
|
||||
self._initialized = True
|
||||
|
||||
@classmethod
|
||||
def reset_singleton(cls):
|
||||
"""
|
||||
Reset the singleton instance of the BECDispatcher.
|
||||
"""
|
||||
cls._instance = None
|
||||
cls._initialized = False
|
||||
|
||||
@@ -178,4 +197,49 @@ class BECDispatcher:
|
||||
*args: Arbitrary positional arguments
|
||||
**kwargs: Arbitrary keyword arguments
|
||||
"""
|
||||
# pylint: disable=protected-access
|
||||
self.disconnect_topics(self.client.connector._topics_cb)
|
||||
|
||||
def start_cli_server(self, gui_id: str | None = None):
|
||||
"""
|
||||
Start the CLI server.
|
||||
|
||||
Args:
|
||||
gui_id(str, optional): The GUI ID. Defaults to None. If None, a unique identifier will be generated.
|
||||
"""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from bec_widgets.utils.cli_server import CLIServer
|
||||
|
||||
if gui_id is None:
|
||||
gui_id = self.generate_unique_identifier()
|
||||
|
||||
if not self.client.started:
|
||||
logger.error("Cannot start CLI server without a running client")
|
||||
return
|
||||
self.cli_server = CLIServer(gui_id, dispatcher=self, client=self.client)
|
||||
logger.success(f"Started CLI server with gui_id: {gui_id}")
|
||||
|
||||
def stop_cli_server(self):
|
||||
"""
|
||||
Stop the CLI server.
|
||||
"""
|
||||
if self.cli_server is None:
|
||||
logger.error("Cannot stop CLI server without starting it first")
|
||||
return
|
||||
self.cli_server.shutdown()
|
||||
self.cli_server = None
|
||||
logger.success("Stopped CLI server")
|
||||
|
||||
@staticmethod
|
||||
def generate_unique_identifier(length: int = 4) -> str:
|
||||
"""
|
||||
Generate a unique identifier for the application.
|
||||
|
||||
Args:
|
||||
length: The length of the identifier. Defaults to 4.
|
||||
|
||||
Returns:
|
||||
str: The unique identifier.
|
||||
"""
|
||||
allowed_chars = string.ascii_lowercase + string.digits
|
||||
return "".join(random.choices(allowed_chars, k=length))
|
||||
|
||||
225
bec_widgets/utils/bec_qapp.py
Normal file
225
bec_widgets/utils/bec_qapp.py
Normal file
@@ -0,0 +1,225 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
from bec_lib.service_config import ServiceConfig
|
||||
from qtpy.QtCore import QSize
|
||||
from qtpy.QtGui import QIcon
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.cli.rpc.rpc_register import RPCRegister
|
||||
from bec_widgets.utils.bec_dispatcher import BECDispatcher
|
||||
from bec_widgets.utils.cli_server import CLIServer
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
||||
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from bec_lib.client import BECClient
|
||||
|
||||
|
||||
class BECApplication:
|
||||
"""
|
||||
Custom QApplication class for BEC applications.
|
||||
"""
|
||||
|
||||
gui_id: str
|
||||
dispatcher: BECDispatcher
|
||||
rpc_register: RPCRegister
|
||||
client: BECClient
|
||||
is_bec_app: bool
|
||||
cli_server: CLIServer
|
||||
|
||||
_instance: BECApplication
|
||||
_initialized: bool
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*args,
|
||||
client=None,
|
||||
config: str | ServiceConfig | None = None,
|
||||
gui_id: str | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
if self._initialized:
|
||||
return
|
||||
self.app = QApplication.instance()
|
||||
if self.app is None:
|
||||
self.app = QApplication([])
|
||||
self._initialize_bec_app(client, config, gui_id)
|
||||
self._initialized = True
|
||||
|
||||
def _initialize_bec_app(
|
||||
self, client=None, config: str | ServiceConfig | None = None, gui_id: str | None = None
|
||||
):
|
||||
"""
|
||||
Initialize the BECApplication instance with the given client and configuration.
|
||||
|
||||
Args:
|
||||
app: The QApplication instance to initialize.
|
||||
client: The BECClient instance to use for communication.
|
||||
config: The ServiceConfig instance to use for configuration.
|
||||
gui_id: The unique identifier for this application.
|
||||
"""
|
||||
self.app.gui_id = gui_id or BECApplication.generate_unique_identifier()
|
||||
self.app.dispatcher = BECDispatcher(client=client, config=config)
|
||||
self.app.rpc_register = RPCRegister()
|
||||
self.app.client = self.app.dispatcher.client # type: ignore
|
||||
self.app.is_bec_app = True
|
||||
self.app.aboutToQuit.connect(self.shutdown)
|
||||
|
||||
self.setup_bec_icon()
|
||||
|
||||
def __instancecheck__(self, instance: Any) -> bool:
|
||||
return isinstance(instance, (QApplication, BECApplication))
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
if hasattr(self.app, name):
|
||||
return getattr(self.app, name)
|
||||
return super().__getattribute__(name)
|
||||
|
||||
def __new__(cls, *args, **kwargs) -> BECApplication:
|
||||
if not hasattr(cls, "_instance"):
|
||||
cls._instance = super().__new__(cls)
|
||||
cls._initialized = False
|
||||
return cls._instance
|
||||
|
||||
@classmethod
|
||||
def from_qapplication(
|
||||
cls, client=None, config: str | ServiceConfig | None = None, gui_id: str | None = None
|
||||
) -> BECApplication:
|
||||
"""
|
||||
Create a BECApplication instance from an existing QApplication instance.
|
||||
"""
|
||||
print("from_qapplication")
|
||||
app = QApplication.instance()
|
||||
if isinstance(app, BECApplication):
|
||||
return app
|
||||
|
||||
return cls(client=client, config=config, gui_id=gui_id)
|
||||
|
||||
def setup_bec_icon(self):
|
||||
"""
|
||||
Set the BEC icon for the application
|
||||
"""
|
||||
icon = QIcon()
|
||||
icon.addFile(
|
||||
os.path.join(MODULE_PATH, "assets", "app_icons", "bec_widgets_icon.png"),
|
||||
size=QSize(48, 48),
|
||||
)
|
||||
self.setWindowIcon(icon)
|
||||
|
||||
@staticmethod
|
||||
def generate_unique_identifier(length: int = 4) -> str:
|
||||
"""
|
||||
Generate a unique identifier for the application.
|
||||
|
||||
Args:
|
||||
length: The length of the identifier. Defaults to 4.
|
||||
|
||||
Returns:
|
||||
str: The unique identifier.
|
||||
"""
|
||||
allowed_chars = string.ascii_lowercase + string.digits
|
||||
return "".join(random.choices(allowed_chars, k=length))
|
||||
|
||||
# # TODO not sure if needed
|
||||
# def register_all(self):
|
||||
# widgets = self.allWidgets()
|
||||
# all_connections = self.rpc_register.list_all_connections()
|
||||
# for widget in widgets:
|
||||
# if not isinstance(widget, BECWidget):
|
||||
# continue
|
||||
# gui_id = getattr(widget, "gui_id", None)
|
||||
# if gui_id and widget not in all_connections:
|
||||
# self.rpc_register.add_rpc(widget)
|
||||
# print(
|
||||
# f"[BECQApplication]: Registered widget {widget.__class__} with GUI ID: {gui_id}"
|
||||
# )
|
||||
|
||||
# # TODO not sure if needed
|
||||
# def list_all_bec_widgets(self):
|
||||
# widgets = self.allWidgets()
|
||||
# bec_widgets = []
|
||||
# for widget in widgets:
|
||||
# if isinstance(widget, BECWidget):
|
||||
# bec_widgets.append(widget)
|
||||
# return bec_widgets
|
||||
|
||||
# def list_hierarchy(self, only_bec_widgets: bool = True, show_parent: bool = True):
|
||||
# """
|
||||
# List the hierarchy of all BECWidgets in this application.
|
||||
|
||||
# Args:
|
||||
# only_bec_widgets (bool): If True, prints only BECWidgets. Non-BECWidgets are skipped but their children are still traversed.
|
||||
# show_parent (bool): If True, displays the immediate BECWidget ancestor for each item.
|
||||
# """
|
||||
# bec_widgets = self.list_all_bec_widgets()
|
||||
# # Identify top-level BECWidgets (whose parent is not another BECWidget)
|
||||
# top_level = [
|
||||
# w for w in bec_widgets if not isinstance(self._get_becwidget_ancestor(w), BECWidget)
|
||||
# ]
|
||||
|
||||
# print("[BECQApplication]: Listing BECWidget hierarchy:")
|
||||
# for widget in top_level:
|
||||
# self._print_becwidget_hierarchy(
|
||||
# widget, indent=0, only_bec_widgets=only_bec_widgets, show_parent=show_parent
|
||||
# )
|
||||
|
||||
# def _print_becwidget_hierarchy(self, widget, indent=0, only_bec_widgets=True, show_parent=True):
|
||||
# # Decide if this widget should be printed
|
||||
# is_bec = isinstance(widget, BECWidget)
|
||||
# print_this = (not only_bec_widgets) or is_bec
|
||||
|
||||
# parent_info = ""
|
||||
# if show_parent and is_bec:
|
||||
# ancestor = self._get_becwidget_ancestor(widget)
|
||||
# if ancestor is not None:
|
||||
# parent_info = f" parent={ancestor.__class__.__name__}"
|
||||
# else:
|
||||
# parent_info = " parent=None"
|
||||
|
||||
# if print_this:
|
||||
# prefix = " " * indent
|
||||
# print(
|
||||
# f"{prefix}- {widget.__class__.__name__} (objectName={widget.objectName()}){parent_info}"
|
||||
# )
|
||||
|
||||
# # Always recurse so deeper BECWidgets aren't missed
|
||||
# for child in widget.children():
|
||||
# # Skip known non-BECWidgets if only_bec_widgets is True, but keep recursion
|
||||
# # We'll still call _print_becwidget_hierarchy to discover any BECWidget descendants.
|
||||
# self._print_becwidget_hierarchy(
|
||||
# child, indent + 2, only_bec_widgets=only_bec_widgets, show_parent=show_parent
|
||||
# )
|
||||
|
||||
# def _get_becwidget_ancestor(self, widget):
|
||||
# """
|
||||
# Climb the .parent() chain until finding another BECWidget, or None.
|
||||
# """
|
||||
# p = widget.parent()
|
||||
# while p is not None:
|
||||
# if isinstance(p, BECWidget):
|
||||
# return p
|
||||
# p = p.parent()
|
||||
# return None
|
||||
|
||||
def shutdown(self):
|
||||
self.dispatcher.disconnect_all()
|
||||
self.cli_server.shutdown()
|
||||
self.rpc_register.reset_singleton()
|
||||
delattr(self.app, "gui_id")
|
||||
delattr(self.app, "dispatcher")
|
||||
delattr(self.app, "rpc_register")
|
||||
delattr(self.app, "client")
|
||||
delattr(self.app, "is_bec_app")
|
||||
delattr(self.app, "cli_server")
|
||||
self._initialized = False
|
||||
self._instance = None
|
||||
@@ -4,8 +4,9 @@ from typing import TYPE_CHECKING
|
||||
|
||||
import darkdetect
|
||||
from bec_lib.logger import bec_logger
|
||||
from PySide6.QtCore import QObject
|
||||
from qtpy.QtCore import Slot
|
||||
from qtpy.QtWidgets import QApplication, QWidget
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from bec_widgets.cli.rpc.rpc_register import RPCRegister
|
||||
from bec_widgets.utils.bec_connector import BECConnector, ConnectionConfig
|
||||
@@ -32,8 +33,7 @@ class BECWidget(BECConnector):
|
||||
config: ConnectionConfig = None,
|
||||
gui_id: str | None = None,
|
||||
theme_update: bool = False,
|
||||
name: str | None = None,
|
||||
parent_dock: BECDock | None = None,
|
||||
parent_dock: BECDock | None = None, # TODO should not be there
|
||||
parent_id: str | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
@@ -54,17 +54,17 @@ class BECWidget(BECConnector):
|
||||
theme_update(bool, optional): Whether to subscribe to theme updates. Defaults to False. When set to True, the
|
||||
widget's apply_theme method will be called when the theme changes.
|
||||
"""
|
||||
if not isinstance(self, QWidget):
|
||||
raise RuntimeError(f"{repr(self)} is not a subclass of QWidget")
|
||||
|
||||
super().__init__(
|
||||
client=client,
|
||||
config=config,
|
||||
gui_id=gui_id,
|
||||
name=name,
|
||||
parent_dock=parent_dock,
|
||||
parent_id=parent_id,
|
||||
**kwargs,
|
||||
)
|
||||
if not isinstance(self, QObject):
|
||||
raise RuntimeError(f"{repr(self)} is not a subclass of QWidget")
|
||||
app = QApplication.instance()
|
||||
if not hasattr(app, "theme"):
|
||||
# DO NOT SET THE THEME TO AUTO! Otherwise, the qwebengineview will segfault
|
||||
@@ -78,6 +78,13 @@ class BECWidget(BECConnector):
|
||||
logger.debug(f"Subscribing to theme updates for {self.__class__.__name__}")
|
||||
self._connect_to_theme_change()
|
||||
|
||||
def _ensure_bec_app(self):
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from bec_widgets.utils.bec_qapp import BECApplication
|
||||
|
||||
app = BECApplication.from_qapplication()
|
||||
return app
|
||||
|
||||
def _connect_to_theme_change(self):
|
||||
"""Connect to the theme change signal."""
|
||||
qapp = QApplication.instance()
|
||||
@@ -107,6 +114,7 @@ class BECWidget(BECConnector):
|
||||
"""Cleanup the widget."""
|
||||
with RPCRegister.delayed_broadcast():
|
||||
# All widgets need to call super().cleanup() in their cleanup method
|
||||
logger.info(f"Registry cleanup for widget {self.__class__.__name__}")
|
||||
self.rpc_register.remove_rpc(self)
|
||||
|
||||
def closeEvent(self, event):
|
||||
|
||||
222
bec_widgets/utils/cli_server.py
Normal file
222
bec_widgets/utils/cli_server.py
Normal file
@@ -0,0 +1,222 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import traceback
|
||||
import types
|
||||
from contextlib import contextmanager
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bec_lib.client import BECClient
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.logger import bec_logger
|
||||
from bec_lib.utils.import_utils import lazy_import
|
||||
from qtpy.QtCore import QTimer
|
||||
from qtpy.QtWidgets import QApplication
|
||||
from redis.exceptions import RedisError
|
||||
|
||||
from bec_widgets.cli.rpc.rpc_register import RPCRegister
|
||||
from bec_widgets.utils import BECDispatcher
|
||||
from bec_widgets.utils.bec_connector import BECConnector
|
||||
from bec_widgets.utils.error_popups import ErrorPopupUtility
|
||||
from bec_widgets.utils.widget_io import WidgetHierarchy
|
||||
from bec_widgets.widgets.plots.plot_base import PlotBase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bec_lib import messages
|
||||
else:
|
||||
messages = lazy_import("bec_lib.messages")
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
@contextmanager
|
||||
def rpc_exception_hook(err_func):
|
||||
"""This context replaces the popup message box for error display with a specific hook"""
|
||||
# get error popup utility singleton
|
||||
popup = ErrorPopupUtility()
|
||||
# save current setting
|
||||
old_exception_hook = popup.custom_exception_hook
|
||||
|
||||
# install err_func, if it is a callable
|
||||
# IMPORTANT, Keep self here, because this method is overwriting the custom_exception_hook
|
||||
# of the ErrorPopupUtility (popup instance) class.
|
||||
def custom_exception_hook(self, exc_type, value, tb, **kwargs):
|
||||
err_func({"error": popup.get_error_message(exc_type, value, tb)})
|
||||
|
||||
popup.custom_exception_hook = types.MethodType(custom_exception_hook, popup)
|
||||
|
||||
try:
|
||||
yield popup
|
||||
finally:
|
||||
# restore state of error popup utility singleton
|
||||
popup.custom_exception_hook = old_exception_hook
|
||||
|
||||
|
||||
class CLIServer:
|
||||
|
||||
client: BECClient
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
gui_id: str,
|
||||
dispatcher: BECDispatcher | None = None,
|
||||
client: BECClient | None = None,
|
||||
config=None,
|
||||
gui_class_id: str = "bec",
|
||||
) -> None:
|
||||
self.status = messages.BECStatus.BUSY
|
||||
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
|
||||
# register broadcast callback
|
||||
self.rpc_register = RPCRegister()
|
||||
self.rpc_register.add_callback(self.broadcast_registry_update)
|
||||
|
||||
self.dispatcher.connect_slot(
|
||||
self.on_rpc_update, MessageEndpoints.gui_instructions(self.gui_id)
|
||||
)
|
||||
|
||||
# Setup QTimer for heartbeat
|
||||
self._heartbeat_timer = QTimer()
|
||||
self._heartbeat_timer.timeout.connect(self.emit_heartbeat)
|
||||
self._heartbeat_timer.start(200)
|
||||
self._registry_update_callbacks = []
|
||||
|
||||
self.status = messages.BECStatus.RUNNING
|
||||
logger.success(f"Server started with gui_id: {self.gui_id}")
|
||||
|
||||
def on_rpc_update(self, msg: dict, metadata: dict):
|
||||
request_id = metadata.get("request_id")
|
||||
if request_id is None:
|
||||
logger.error("Received RPC instruction without request_id")
|
||||
return
|
||||
logger.debug(f"Received RPC instruction: {msg}, metadata: {metadata}")
|
||||
with rpc_exception_hook(functools.partial(self.send_response, request_id, False)):
|
||||
try:
|
||||
obj = self.get_object_from_config(msg["parameter"])
|
||||
method = msg["action"]
|
||||
args = msg["parameter"].get("args", [])
|
||||
kwargs = msg["parameter"].get("kwargs", {})
|
||||
res = self.run_rpc(obj, method, args, kwargs)
|
||||
except Exception as e:
|
||||
content = traceback.format_exc()
|
||||
logger.error(f"Error while executing RPC instruction: {content}")
|
||||
self.send_response(request_id, False, {"error": content})
|
||||
else:
|
||||
logger.debug(f"RPC instruction executed successfully: {res}")
|
||||
self.send_response(request_id, True, {"result": res})
|
||||
|
||||
def send_response(self, request_id: str, accepted: bool, msg: dict):
|
||||
self.client.connector.set_and_publish(
|
||||
MessageEndpoints.gui_instruction_response(request_id),
|
||||
messages.RequestResponseMessage(accepted=accepted, message=msg),
|
||||
expire=60,
|
||||
)
|
||||
|
||||
def get_object_from_config(self, config: dict):
|
||||
gui_id = config.get("gui_id")
|
||||
obj = self.rpc_register.get_rpc_by_id(gui_id)
|
||||
if obj is None:
|
||||
raise ValueError(f"Object with gui_id {gui_id} not found")
|
||||
return obj
|
||||
|
||||
def run_rpc(self, obj, method, args, kwargs):
|
||||
# Run with rpc registry broadcast, but only once
|
||||
with RPCRegister.delayed_broadcast():
|
||||
logger.debug(f"Running RPC instruction: {method} with args: {args}, kwargs: {kwargs}")
|
||||
method_obj = getattr(obj, method)
|
||||
# check if the method accepts args and kwargs
|
||||
if not callable(method_obj):
|
||||
if not args:
|
||||
res = method_obj
|
||||
else:
|
||||
setattr(obj, method, args[0])
|
||||
res = None
|
||||
else:
|
||||
res = method_obj(*args, **kwargs)
|
||||
|
||||
if isinstance(res, list):
|
||||
res = [self.serialize_object(obj) for obj in res]
|
||||
elif isinstance(res, dict):
|
||||
res = {key: self.serialize_object(val) for key, val in res.items()}
|
||||
else:
|
||||
res = self.serialize_object(res)
|
||||
return res
|
||||
|
||||
def serialize_object(self, obj):
|
||||
if isinstance(obj, BECConnector):
|
||||
# Respect RPC = False
|
||||
if hasattr(obj, "RPC") and obj.RPC is False:
|
||||
return None
|
||||
return self._serialize_bec_connector(obj)
|
||||
return obj
|
||||
|
||||
def emit_heartbeat(self):
|
||||
logger.trace(f"Emitting heartbeat for {self.gui_id}")
|
||||
try:
|
||||
self.client.connector.set(
|
||||
MessageEndpoints.gui_heartbeat(self.gui_id),
|
||||
messages.StatusMessage(name=self.gui_id, status=self.status, info={}),
|
||||
expire=10,
|
||||
)
|
||||
except RedisError as exc:
|
||||
logger.error(f"Error while emitting heartbeat: {exc}")
|
||||
|
||||
def broadcast_registry_update(self, connections: dict):
|
||||
data = {}
|
||||
for key, val in connections.items():
|
||||
if not isinstance(val, BECConnector):
|
||||
continue
|
||||
if not getattr(val, "RPC", True):
|
||||
continue
|
||||
data[key] = self._serialize_bec_connector(val)
|
||||
|
||||
logger.info(f"Broadcasting registry update: {data} for {self.gui_id}")
|
||||
self.client.connector.xadd(
|
||||
MessageEndpoints.gui_registry_state(self.gui_id),
|
||||
msg_dict={"data": messages.GUIRegistryStateMessage(state=data)},
|
||||
max_size=1,
|
||||
)
|
||||
|
||||
def _serialize_bec_connector(self, connector: BECConnector) -> dict:
|
||||
"""
|
||||
Create the serialization dict for a single BECConnector,
|
||||
setting 'parent_id' via the real nearest BECConnector parent.
|
||||
"""
|
||||
|
||||
config_dict = connector.config.model_dump()
|
||||
config_dict["parent_id"] = getattr(connector, "parent_id", None)
|
||||
|
||||
return {
|
||||
"gui_id": connector.gui_id,
|
||||
"object_name": connector.object_name or connector.__class__.__name__,
|
||||
"widget_class": connector.__class__.__name__,
|
||||
"config": config_dict,
|
||||
"__rpc__": True,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _get_becwidget_ancestor(widget):
|
||||
"""
|
||||
Traverse up the parent chain to find the nearest BECConnector.
|
||||
Returns None if none is found.
|
||||
"""
|
||||
from bec_widgets.utils import BECConnector
|
||||
|
||||
parent = widget.parent()
|
||||
while parent is not None:
|
||||
if isinstance(parent, BECConnector):
|
||||
return parent
|
||||
parent = parent.parent()
|
||||
return None
|
||||
|
||||
# Suppose clients register callbacks to receive updates
|
||||
def add_registry_update_callback(self, cb):
|
||||
self._registry_update_callbacks.append(cb)
|
||||
|
||||
def shutdown(self): # TODO not sure if needed when cleanup is done at level of BECConnector
|
||||
self.status = messages.BECStatus.IDLE
|
||||
self._heartbeat_timer.stop()
|
||||
self.emit_heartbeat()
|
||||
logger.info("Succeded in shutting down CLI server")
|
||||
self.client.shutdown()
|
||||
@@ -22,10 +22,10 @@ class PaletteViewer(BECWidget, QWidget):
|
||||
"""
|
||||
|
||||
ICON_NAME = "palette"
|
||||
RPC = False
|
||||
|
||||
def __init__(self, *args, parent=None, **kwargs):
|
||||
super().__init__(*args, theme_update=True, **kwargs)
|
||||
QWidget.__init__(self, parent=parent)
|
||||
super().__init__(parent=parent, **kwargs)
|
||||
self.setFixedSize(400, 600)
|
||||
layout = QVBoxLayout(self)
|
||||
dark_mode_button = DarkModeButton(self)
|
||||
|
||||
@@ -25,7 +25,6 @@ class RoundedFrame(QFrame):
|
||||
|
||||
# Apply rounded frame styling
|
||||
self.setProperty("skip_settings", True)
|
||||
self.setObjectName("roundedFrame")
|
||||
|
||||
# Create a layout for the frame
|
||||
self.layout = QHBoxLayout(self)
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
from bec_lib.logger import bec_logger
|
||||
from PySide6.QtGui import QCloseEvent
|
||||
from qtpy.QtWidgets import QDialog, QDialogButtonBox, QHBoxLayout, QPushButton, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils.error_popups import SafeSlot
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class SettingWidget(QWidget):
|
||||
"""
|
||||
@@ -37,6 +41,15 @@ class SettingWidget(QWidget):
|
||||
"""
|
||||
pass
|
||||
|
||||
def cleanup(self):
|
||||
"""
|
||||
Cleanup the settings widget.
|
||||
"""
|
||||
|
||||
def closeEvent(self, event: QCloseEvent) -> None:
|
||||
self.cleanup()
|
||||
return super().closeEvent(event)
|
||||
|
||||
|
||||
class SettingsDialog(QDialog):
|
||||
"""
|
||||
@@ -99,8 +112,17 @@ class SettingsDialog(QDialog):
|
||||
Accept the changes made in the settings widget and close the dialog.
|
||||
"""
|
||||
self.widget.accept_changes()
|
||||
self.cleanup()
|
||||
super().accept()
|
||||
|
||||
@SafeSlot()
|
||||
def reject(self):
|
||||
"""
|
||||
Reject the changes made in the settings widget and close the dialog.
|
||||
"""
|
||||
self.cleanup()
|
||||
super().reject()
|
||||
|
||||
@SafeSlot()
|
||||
def apply_changes(self):
|
||||
"""
|
||||
@@ -114,7 +136,10 @@ class SettingsDialog(QDialog):
|
||||
"""
|
||||
self.button_box.close()
|
||||
self.button_box.deleteLater()
|
||||
self.widget.close()
|
||||
self.widget.deleteLater()
|
||||
|
||||
def closeEvent(self, event):
|
||||
logger.info("Closing settings dialog")
|
||||
self.cleanup()
|
||||
super().closeEvent(event)
|
||||
|
||||
@@ -35,7 +35,6 @@ class SidePanel(QWidget):
|
||||
super().__init__(parent=parent)
|
||||
|
||||
self.setProperty("skip_settings", True)
|
||||
self.setObjectName("SidePanel")
|
||||
|
||||
self._orientation = orientation
|
||||
self._panel_max_width = panel_max_width
|
||||
|
||||
@@ -858,7 +858,7 @@ class MainWindow(QMainWindow): # pragma: no cover
|
||||
|
||||
# For theme testing
|
||||
|
||||
self.dark_button = DarkModeButton(toolbar=True)
|
||||
self.dark_button = DarkModeButton(parent=self, toolbar=True)
|
||||
dark_mode_action = WidgetAction(label=None, widget=self.dark_button)
|
||||
self.toolbar.add_action("dark_mode", dark_mode_action, self)
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import os
|
||||
import inspect
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
from qtpy import PYQT6, PYSIDE6, QT_VERSION
|
||||
from qtpy.QtCore import QFile, QIODevice
|
||||
|
||||
from bec_widgets.utils.generate_designer_plugin import DesignerPluginInfo
|
||||
from bec_widgets.utils.plugin_utils import get_custom_classes
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
if PYSIDE6:
|
||||
from PySide6.QtUiTools import QUiLoader
|
||||
|
||||
@@ -18,10 +21,18 @@ if PYSIDE6:
|
||||
|
||||
def createWidget(self, class_name, parent=None, name=""):
|
||||
if class_name in self.custom_widgets:
|
||||
widget = self.custom_widgets[class_name](parent)
|
||||
widget.setObjectName(name)
|
||||
|
||||
# check if the custom widget has a parent_id argument
|
||||
if "parent_id" in inspect.signature(self.custom_widgets[class_name]).parameters:
|
||||
gui_id = getattr(self.baseinstance, "gui_id", None)
|
||||
widget = self.custom_widgets[class_name](self.baseinstance, parent_id=gui_id)
|
||||
else:
|
||||
logger.warning(
|
||||
f"Custom widget {class_name} does not have a parent_id argument. "
|
||||
)
|
||||
widget = self.custom_widgets[class_name](self.baseinstance)
|
||||
return widget
|
||||
return super().createWidget(class_name, parent, name)
|
||||
return super().createWidget(class_name, self.baseinstance, name)
|
||||
|
||||
|
||||
class UILoader:
|
||||
@@ -51,7 +62,7 @@ class UILoader:
|
||||
Returns:
|
||||
QWidget: The loaded widget.
|
||||
"""
|
||||
|
||||
parent = parent or self.parent
|
||||
loader = CustomUiLoader(parent, self.custom_widgets)
|
||||
file = QFile(ui_file)
|
||||
if not file.open(QIODevice.ReadOnly):
|
||||
|
||||
@@ -275,39 +275,160 @@ class WidgetHierarchy:
|
||||
grab_values: bool = False,
|
||||
prefix: str = "",
|
||||
exclude_internal_widgets: bool = True,
|
||||
only_bec_widgets: bool = False,
|
||||
show_parent: bool = True,
|
||||
) -> None:
|
||||
"""
|
||||
Print the widget hierarchy to the console.
|
||||
|
||||
Args:
|
||||
widget: Widget to print the hierarchy of
|
||||
widget: Widget to print the hierarchy of.
|
||||
indent(int, optional): Level of indentation.
|
||||
grab_values(bool,optional): Whether to grab the values of the widgets.
|
||||
prefix(stc,optional): Custom string prefix for indentation.
|
||||
prefix(str,optional): Custom string prefix for indentation.
|
||||
exclude_internal_widgets(bool,optional): Whether to exclude internal widgets (e.g. QComboBox in PyQt6).
|
||||
only_bec_widgets(bool, optional): Whether to print only widgets that are instances of BECWidget.
|
||||
show_parent(bool, optional): Whether to display which BECWidget is the parent of each discovered BECWidget.
|
||||
"""
|
||||
widget_info = f"{widget.__class__.__name__} ({widget.objectName()})"
|
||||
if grab_values:
|
||||
value = WidgetIO.get_value(widget, ignore_errors=True)
|
||||
value_str = f" [value: {value}]" if value is not None else ""
|
||||
widget_info += value_str
|
||||
from bec_widgets.utils import BECConnector
|
||||
from bec_widgets.widgets.plots.waveform.waveform import Waveform
|
||||
|
||||
# 1) Filter out widgets that are not BECConnectors (if 'only_bec_widgets' is True)
|
||||
is_bec = isinstance(widget, BECConnector)
|
||||
if only_bec_widgets and not is_bec:
|
||||
return
|
||||
|
||||
# 2) Determine and print the parent's info (closest BECConnector)
|
||||
parent_info = ""
|
||||
if show_parent and is_bec:
|
||||
ancestor = WidgetHierarchy._get_becwidget_ancestor(widget)
|
||||
if ancestor:
|
||||
parent_label = ancestor.objectName() or ancestor.__class__.__name__
|
||||
parent_info = f" parent={parent_label}"
|
||||
else:
|
||||
parent_info = " parent=None"
|
||||
|
||||
widget_info = f"{widget.__class__.__name__} ({widget.objectName()}){parent_info}"
|
||||
print(prefix + widget_info)
|
||||
|
||||
children = widget.children()
|
||||
for child in children:
|
||||
if (
|
||||
exclude_internal_widgets
|
||||
and isinstance(widget, QComboBox)
|
||||
and child.__class__.__name__ in ["QFrame", "QBoxLayout", "QListView"]
|
||||
):
|
||||
# 3) If it's a Waveform, explicitly print the curves
|
||||
if isinstance(widget, Waveform):
|
||||
for curve in widget.curves:
|
||||
curve_prefix = prefix + " └─ "
|
||||
print(
|
||||
f"{curve_prefix}{curve.__class__.__name__} ({curve.objectName()}) "
|
||||
f"parent={widget.objectName()}"
|
||||
)
|
||||
|
||||
# 4) Recursively handle each child if:
|
||||
# - It's a QWidget
|
||||
# - It is a BECConnector (or we don't care about filtering)
|
||||
# - Its closest BECConnector parent is the current widget
|
||||
for child in widget.findChildren(QWidget):
|
||||
if only_bec_widgets and not isinstance(child, BECConnector):
|
||||
continue
|
||||
child_prefix = prefix + " "
|
||||
arrow = "├─ " if child != children[-1] else "└─ "
|
||||
|
||||
# if WidgetHierarchy._get_becwidget_ancestor(child) == widget:
|
||||
child_prefix = prefix + " └─ "
|
||||
WidgetHierarchy.print_widget_hierarchy(
|
||||
child, indent + 1, grab_values, prefix=child_prefix + arrow
|
||||
child,
|
||||
indent=indent + 1,
|
||||
grab_values=grab_values,
|
||||
prefix=child_prefix,
|
||||
exclude_internal_widgets=exclude_internal_widgets,
|
||||
only_bec_widgets=only_bec_widgets,
|
||||
show_parent=show_parent,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def print_becconnector_hierarchy_from_app():
|
||||
"""
|
||||
Enumerate ALL BECConnector objects in the QApplication.
|
||||
Also detect if a widget is a PlotBase, and add any data items
|
||||
(PlotDataItem-like) that are also BECConnector objects.
|
||||
|
||||
Build a parent->children graph where each child's 'parent'
|
||||
is its closest BECConnector ancestor. Print the entire hierarchy
|
||||
from the root(s).
|
||||
|
||||
The result is a single, consolidated tree for your entire
|
||||
running GUI, including PlotBase data items that are BECConnector.
|
||||
"""
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from bec_widgets.utils import BECConnector
|
||||
from bec_widgets.widgets.plots.plot_base import PlotBase
|
||||
|
||||
# 1) Gather ALL QWidget-based BECConnector objects
|
||||
all_qwidgets = QApplication.allWidgets()
|
||||
bec_widgets = set(w for w in all_qwidgets if isinstance(w, BECConnector))
|
||||
|
||||
# 2) Also gather any BECConnector-based data items from PlotBase widgets
|
||||
for w in all_qwidgets:
|
||||
if isinstance(w, PlotBase) and hasattr(w, "plot_item"):
|
||||
plot_item = w.plot_item
|
||||
if hasattr(plot_item, "listDataItems"):
|
||||
for data_item in plot_item.listDataItems():
|
||||
if isinstance(data_item, BECConnector):
|
||||
bec_widgets.add(data_item)
|
||||
|
||||
# 3) Build a map of (closest BECConnector parent) -> list of children
|
||||
parent_map = defaultdict(list)
|
||||
for w in bec_widgets:
|
||||
parent_bec = WidgetHierarchy._get_becwidget_ancestor(w)
|
||||
parent_map[parent_bec].append(w)
|
||||
|
||||
# 4) Define a recursive printer to show each object's children
|
||||
def print_tree(parent, prefix=""):
|
||||
children = parent_map[parent]
|
||||
for i, child in enumerate(children):
|
||||
connector_class = child.__class__.__name__
|
||||
connector_name = child.objectName() or connector_class
|
||||
|
||||
if parent is None:
|
||||
parent_label = "None"
|
||||
else:
|
||||
parent_label = parent.objectName() or parent.__class__.__name__
|
||||
|
||||
line = f"{connector_class} ({connector_name}) parent={parent_label}"
|
||||
# Determine tree-branch symbols
|
||||
is_last = i == len(children) - 1
|
||||
branch_str = "└─ " if is_last else "├─ "
|
||||
print(prefix + branch_str + line)
|
||||
|
||||
# Recurse deeper
|
||||
next_prefix = prefix + (" " if is_last else "│ ")
|
||||
print_tree(child, prefix=next_prefix)
|
||||
|
||||
# 5) Print top-level items (roots) whose BECConnector parent is None
|
||||
roots = parent_map[None]
|
||||
for r_i, root in enumerate(roots):
|
||||
root_class = root.__class__.__name__
|
||||
root_name = root.objectName() or root_class
|
||||
line = f"{root_class} ({root_name}) parent=None"
|
||||
is_last_root = r_i == len(roots) - 1
|
||||
print(line)
|
||||
# Recurse into its children
|
||||
print_tree(root, prefix=" ")
|
||||
|
||||
@staticmethod
|
||||
def _get_becwidget_ancestor(widget):
|
||||
"""
|
||||
Traverse up the parent chain to find the nearest BECConnector.
|
||||
Returns None if none is found.
|
||||
"""
|
||||
from bec_widgets.utils import BECConnector
|
||||
|
||||
parent = widget.parent()
|
||||
while parent is not None:
|
||||
if isinstance(parent, BECConnector):
|
||||
return parent
|
||||
parent = parent.parent()
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def export_config_to_dict(
|
||||
widget: QWidget,
|
||||
|
||||
@@ -133,6 +133,7 @@ class BECDock(BECWidget, Dock):
|
||||
parent_id: str | None = None,
|
||||
config: DockConfig | None = None,
|
||||
name: str | None = None,
|
||||
object_name: str | None = None,
|
||||
client=None,
|
||||
gui_id: str | None = None,
|
||||
closable: bool = True,
|
||||
@@ -148,12 +149,17 @@ class BECDock(BECWidget, Dock):
|
||||
if isinstance(config, dict):
|
||||
config = DockConfig(**config)
|
||||
self.config = config
|
||||
super().__init__(
|
||||
client=client, config=config, gui_id=gui_id, name=name, parent_id=parent_id
|
||||
) # Name was checked and created in BEC Widget
|
||||
label = CustomDockLabel(text=name, closable=closable)
|
||||
Dock.__init__(self, name=name, label=label, parent=self, **kwargs)
|
||||
# Dock.__init__(self, name=name, **kwargs)
|
||||
super().__init__(
|
||||
parent=parent_dock_area,
|
||||
name=name,
|
||||
object_name=object_name,
|
||||
client=client,
|
||||
gui_id=gui_id,
|
||||
config=config,
|
||||
label=label,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
self.parent_dock_area = parent_dock_area
|
||||
# Layout Manager
|
||||
@@ -193,7 +199,7 @@ class BECDock(BECWidget, Dock):
|
||||
widgets(dict): The widgets in the dock.
|
||||
"""
|
||||
# pylint: disable=protected-access
|
||||
return dict((widget._name, widget) for widget in self.element_list)
|
||||
return dict((widget.object_name, widget) for widget in self.element_list)
|
||||
|
||||
@property
|
||||
def element_list(self) -> list[BECWidget]:
|
||||
@@ -295,27 +301,11 @@ class BECDock(BECWidget, Dock):
|
||||
shift(Literal["down", "up", "left", "right"]): The direction to shift the widgets if the position is occupied.
|
||||
"""
|
||||
if row is None:
|
||||
# row = cast(int, self.layout.rowCount()) # type:ignore
|
||||
row = self.layout.rowCount()
|
||||
# row = cast(int, row)
|
||||
|
||||
if self.layout_manager.is_position_occupied(row, col):
|
||||
self.layout_manager.shift_widgets(shift, start_row=row)
|
||||
|
||||
existing_widgets_parent_dock = self._get_list_of_widget_name_of_parent_dock_area()
|
||||
|
||||
if name is not None: # Name is provided
|
||||
if name in existing_widgets_parent_dock:
|
||||
# pylint: disable=protected-access
|
||||
raise ValueError(
|
||||
f"Name {name} must be unique for widgets, but already exists in DockArea "
|
||||
f"with name: {self.parent_dock_area._name} and id {self.parent_dock_area.gui_id}."
|
||||
)
|
||||
else: # Name is not provided
|
||||
widget_class_name = widget if isinstance(widget, str) else widget.__class__.__name__
|
||||
name = WidgetContainerUtils.generate_unique_name(
|
||||
name=widget_class_name, list_of_names=existing_widgets_parent_dock
|
||||
)
|
||||
# Check that Widget is not BECDock or BECDockArea
|
||||
widget_class_name = widget if isinstance(widget, str) else widget.__class__.__name__
|
||||
if widget_class_name in IGNORE_WIDGETS:
|
||||
@@ -325,16 +315,20 @@ class BECDock(BECWidget, Dock):
|
||||
widget = cast(
|
||||
BECWidget,
|
||||
widget_handler.create_widget(
|
||||
widget_type=widget, name=name, parent_dock=self, parent_id=self.gui_id
|
||||
widget_type=widget,
|
||||
object_name=name,
|
||||
parent_dock=self,
|
||||
parent_id=self.gui_id,
|
||||
parent=self,
|
||||
),
|
||||
)
|
||||
else:
|
||||
widget._name = name # pylint: disable=protected-access
|
||||
widget.object_name = name
|
||||
|
||||
self.addWidget(widget, row=row, col=col, rowspan=rowspan, colspan=colspan)
|
||||
if hasattr(widget, "config"):
|
||||
widget.config.gui_id = widget.gui_id
|
||||
self.config.widgets[widget._name] = widget.config # pylint: disable=protected-access
|
||||
self.config.widgets[widget.object_name] = widget.config
|
||||
return widget
|
||||
|
||||
def move_widget(self, widget: QWidget, new_row: int, new_col: int):
|
||||
@@ -364,7 +358,7 @@ class BECDock(BECWidget, Dock):
|
||||
"""
|
||||
Remove the dock from the parent dock area.
|
||||
"""
|
||||
self.parent_dock_area.delete(self._name)
|
||||
self.parent_dock_area.delete(self.object_name)
|
||||
|
||||
def delete(self, widget_name: str) -> None:
|
||||
"""
|
||||
@@ -374,7 +368,7 @@ class BECDock(BECWidget, Dock):
|
||||
widget_name(str): Delete the widget with the given name.
|
||||
"""
|
||||
# pylint: disable=protected-access
|
||||
widgets = [widget for widget in self.widgets if widget._name == widget_name]
|
||||
widgets = [widget for widget in self.widgets if widget.object_name == widget_name]
|
||||
if len(widgets) == 0:
|
||||
logger.warning(
|
||||
f"Widget with name {widget_name} not found in dock {self.name()}. "
|
||||
@@ -390,7 +384,7 @@ class BECDock(BECWidget, Dock):
|
||||
else:
|
||||
widget = widgets[0]
|
||||
self.layout.removeWidget(widget)
|
||||
self.config.widgets.pop(widget._name, None)
|
||||
self.config.widgets.pop(widget.object_name, None)
|
||||
if widget in self.widgets:
|
||||
self.widgets.remove(widget)
|
||||
widget.close()
|
||||
@@ -400,7 +394,7 @@ class BECDock(BECWidget, Dock):
|
||||
Remove all widgets from the dock.
|
||||
"""
|
||||
for widget in self.widgets:
|
||||
self.delete(widget._name) # pylint: disable=protected-access
|
||||
self.delete(widget.object_name)
|
||||
|
||||
def cleanup(self):
|
||||
"""
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal, Optional
|
||||
from typing import TYPE_CHECKING, Literal, Optional
|
||||
from weakref import WeakValueDictionary
|
||||
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
@@ -21,6 +21,7 @@ from bec_widgets.utils.toolbar import (
|
||||
ModularToolBar,
|
||||
SeparatorAction,
|
||||
)
|
||||
from bec_widgets.utils.widget_io import WidgetHierarchy
|
||||
from bec_widgets.widgets.containers.dock.dock import BECDock, DockConfig
|
||||
from bec_widgets.widgets.control.device_control.positioner_box import PositionerBox
|
||||
from bec_widgets.widgets.control.scan_control.scan_control import ScanControl
|
||||
@@ -36,6 +37,9 @@ from bec_widgets.widgets.services.bec_status_box.bec_status_box import BECStatus
|
||||
from bec_widgets.widgets.utility.logpanel.logpanel import LogPanel
|
||||
from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from bec_widgets.cli.auto_updates import AutoUpdates
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
@@ -62,8 +66,9 @@ class BECDockArea(BECWidget, QWidget):
|
||||
"remove",
|
||||
"detach_dock",
|
||||
"attach_all",
|
||||
"selected_device",
|
||||
"save_state",
|
||||
"selected_device",
|
||||
"selected_device.setter",
|
||||
"restore_state",
|
||||
]
|
||||
|
||||
@@ -73,7 +78,7 @@ class BECDockArea(BECWidget, QWidget):
|
||||
config: DockAreaConfig | None = None,
|
||||
client=None,
|
||||
gui_id: str = None,
|
||||
name: str | None = None,
|
||||
object_name: str = None,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
if config is None:
|
||||
@@ -82,17 +87,27 @@ class BECDockArea(BECWidget, QWidget):
|
||||
if isinstance(config, dict):
|
||||
config = DockAreaConfig(**config)
|
||||
self.config = config
|
||||
super().__init__(client=client, config=config, gui_id=gui_id, name=name, **kwargs)
|
||||
QWidget.__init__(self, parent=parent)
|
||||
self._parent = parent
|
||||
super().__init__(
|
||||
parent=parent,
|
||||
object_name=object_name,
|
||||
client=client,
|
||||
gui_id=gui_id,
|
||||
config=config,
|
||||
**kwargs,
|
||||
)
|
||||
self._parent = parent # TODO probably not needed
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.layout.setSpacing(5)
|
||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.auto_update: AutoUpdates | None = None
|
||||
self._auto_update_selected_device: str | None = None
|
||||
self._instructions_visible = True
|
||||
|
||||
self.dark_mode_button = DarkModeButton(parent=self, parent_id=self.gui_id, toolbar=True)
|
||||
self.dock_area = DockArea()
|
||||
self.toolbar = ModularToolBar(
|
||||
parent=self,
|
||||
actions={
|
||||
"menu_plots": ExpandableMenuAction(
|
||||
label="Add Plot ",
|
||||
@@ -172,7 +187,7 @@ class BECDockArea(BECWidget, QWidget):
|
||||
self.spacer = QWidget()
|
||||
self.spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
self.toolbar.addWidget(self.spacer)
|
||||
self.toolbar.addWidget(DarkModeButton(toolbar=True))
|
||||
self.toolbar.addWidget(self.dark_mode_button)
|
||||
self._hook_toolbar()
|
||||
|
||||
def minimumSizeHint(self):
|
||||
@@ -244,15 +259,33 @@ class BECDockArea(BECWidget, QWidget):
|
||||
)
|
||||
|
||||
@property
|
||||
def selected_device(self) -> str:
|
||||
gui_id = QApplication.instance().gui_id
|
||||
auto_update_config = self.client.connector.get(
|
||||
MessageEndpoints.gui_auto_update_config(gui_id)
|
||||
)
|
||||
try:
|
||||
return auto_update_config.selected_device
|
||||
except AttributeError:
|
||||
return None
|
||||
def selected_device(self) -> str | None:
|
||||
"""
|
||||
Get the selected device from the auto update config.
|
||||
|
||||
Returns:
|
||||
str: The selected device. If no device is selected, None is returned.
|
||||
"""
|
||||
return self._auto_update_selected_device
|
||||
|
||||
@selected_device.setter
|
||||
def selected_device(self, value: str | None) -> None:
|
||||
"""
|
||||
Set the selected device in the auto update config.
|
||||
|
||||
Args:
|
||||
value(str): The selected device.
|
||||
"""
|
||||
self._auto_update_selected_device = value
|
||||
|
||||
def set_auto_update(self, auto_update_cls: type[AutoUpdates]) -> None:
|
||||
"""
|
||||
Set the auto update object for the dock area.
|
||||
|
||||
Args:
|
||||
auto_update(AutoUpdates): The auto update object.
|
||||
"""
|
||||
self.auto_update = auto_update_cls(self)
|
||||
|
||||
@property
|
||||
def panels(self) -> dict[str, BECDock]:
|
||||
@@ -352,17 +385,26 @@ class BECDockArea(BECWidget, QWidget):
|
||||
Returns:
|
||||
BECDock: The created dock.
|
||||
"""
|
||||
dock_names = [dock._name for dock in self.panel_list] # pylint: disable=protected-access
|
||||
dock_names = [
|
||||
dock.object_name for dock in self.panel_list
|
||||
] # pylint: disable=protected-access
|
||||
if name is not None: # Name is provided
|
||||
if name in dock_names:
|
||||
raise ValueError(
|
||||
f"Name {name} must be unique for docks, but already exists in DockArea "
|
||||
f"with name: {self._name} and id {self.gui_id}."
|
||||
f"with name: {self.object_name} and id {self.gui_id}."
|
||||
)
|
||||
else: # Name is not provided
|
||||
name = WidgetContainerUtils.generate_unique_name(name="dock", list_of_names=dock_names)
|
||||
|
||||
dock = BECDock(name=name, parent_dock_area=self, parent_id=self.gui_id, closable=closable)
|
||||
dock = BECDock(
|
||||
parent=self,
|
||||
name=name, # this is dock name pyqtgraph property, this is displayed on label
|
||||
object_name=name, # this is a real qt object name passed to BECConnector
|
||||
parent_dock_area=self,
|
||||
parent_id=self.gui_id,
|
||||
closable=closable,
|
||||
)
|
||||
dock.config.position = position
|
||||
self.config.docks[dock.name()] = dock.config
|
||||
# The dock.name is equal to the name passed to BECDock
|
||||
@@ -429,9 +471,13 @@ class BECDockArea(BECWidget, QWidget):
|
||||
"""
|
||||
Cleanup the dock area.
|
||||
"""
|
||||
if self.auto_update:
|
||||
self.auto_update.enabled = False
|
||||
self.delete_all()
|
||||
self.toolbar.close()
|
||||
self.toolbar.deleteLater()
|
||||
self.dark_mode_button.close()
|
||||
self.dark_mode_button.deleteLater()
|
||||
self.dock_area.close()
|
||||
self.dock_area.deleteLater()
|
||||
super().cleanup()
|
||||
@@ -495,11 +541,13 @@ if __name__ == "__main__": # pragma: no cover
|
||||
app = QApplication([])
|
||||
set_theme("auto")
|
||||
dock_area = BECDockArea()
|
||||
dock_1 = dock_area.new(name="dock_0", widget="Waveform")
|
||||
dock_1 = dock_area.new(name="dock_0", widget="DarkModeButton")
|
||||
dock_1.new(widget="DarkModeButton")
|
||||
# dock_1 = dock_area.new(name="dock_0", widget="Waveform")
|
||||
dock_area.new(widget="Waveform")
|
||||
dock_area.new(widget="DarkModeButton")
|
||||
dock_area.show()
|
||||
dock_area.setGeometry(100, 100, 800, 600)
|
||||
app.topLevelWidgets()
|
||||
WidgetHierarchy.print_becconnector_hierarchy_from_app()
|
||||
app.exec_()
|
||||
sys.exit(app.exec_())
|
||||
|
||||
@@ -34,7 +34,6 @@ class LayoutManagerWidget(QWidget):
|
||||
|
||||
def __init__(self, parent=None, auto_reindex=True):
|
||||
super().__init__(parent)
|
||||
self.setObjectName("LayoutManagerWidget")
|
||||
self.layout = QGridLayout(self)
|
||||
self.auto_reindex = auto_reindex
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import webbrowser
|
||||
|
||||
|
||||
class BECWebLinksMixin:
|
||||
@staticmethod
|
||||
def open_bec_docs():
|
||||
webbrowser.open("https://beamline-experiment-control.readthedocs.io/en/latest/")
|
||||
|
||||
@staticmethod
|
||||
def open_bec_widgets_docs():
|
||||
webbrowser.open("https://bec.readthedocs.io/projects/bec-widgets/en/latest/")
|
||||
|
||||
@staticmethod
|
||||
def open_bec_bug_report():
|
||||
webbrowser.open("https://gitlab.psi.ch/groups/bec/-/issues/")
|
||||
42
bec_widgets/widgets/containers/main_window/example_app.ui
Normal file
42
bec_widgets/widgets/containers/main_window/example_app.ui
Normal file
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>824</width>
|
||||
<height>1234</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="Waveform" name="waveform"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="BECDockArea" name="dock_area_2"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="BECDockArea" name="dock_area"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>BECDockArea</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>dock_area</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>Waveform</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>waveform</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
262
bec_widgets/widgets/containers/main_window/general_app.ui
Normal file
262
bec_widgets/widgets/containers/main_window/general_app.ui
Normal file
@@ -0,0 +1,262 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MainWindow</class>
|
||||
<widget class="QMainWindow" name="MainWindow">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1718</width>
|
||||
<height>1139</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>MainWindow</string>
|
||||
</property>
|
||||
<property name="tabShape">
|
||||
<enum>QTabWidget::TabShape::Rounded</enum>
|
||||
</property>
|
||||
<widget class="QWidget" name="centralwidget">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<widget class="QTabWidget" name="central_tab">
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="dock_area_tab">
|
||||
<attribute name="title">
|
||||
<string>Dock Area</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="leftMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="BECDockArea" name="dock_area"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="vscode_tab">
|
||||
<attribute name="icon">
|
||||
<iconset theme="QIcon::ThemeIcon::Computer"/>
|
||||
</attribute>
|
||||
<attribute name="title">
|
||||
<string>Visual Studio Code</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<property name="leftMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="VSCodeEditor" name="vscode"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QMenuBar" name="menubar">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1718</width>
|
||||
<height>31</height>
|
||||
</rect>
|
||||
</property>
|
||||
<widget class="QMenu" name="menuHelp">
|
||||
<property name="title">
|
||||
<string>Help</string>
|
||||
</property>
|
||||
<addaction name="action_BEC_docs"/>
|
||||
<addaction name="action_BEC_widgets_docs"/>
|
||||
<addaction name="action_bug_report"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuTheme">
|
||||
<property name="title">
|
||||
<string>Theme</string>
|
||||
</property>
|
||||
<addaction name="action_light"/>
|
||||
<addaction name="action_dark"/>
|
||||
</widget>
|
||||
<addaction name="menuTheme"/>
|
||||
<addaction name="menuHelp"/>
|
||||
</widget>
|
||||
<widget class="QStatusBar" name="statusbar"/>
|
||||
<widget class="QDockWidget" name="dock_scan_control">
|
||||
<property name="windowTitle">
|
||||
<string>Scan Control</string>
|
||||
</property>
|
||||
<attribute name="dockWidgetArea">
|
||||
<number>2</number>
|
||||
</attribute>
|
||||
<widget class="QWidget" name="dockWidgetContents_2">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<item>
|
||||
<widget class="ScanControl" name="scan_control"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<widget class="QDockWidget" name="dock_status_2">
|
||||
<property name="windowTitle">
|
||||
<string>BEC Service Status</string>
|
||||
</property>
|
||||
<attribute name="dockWidgetArea">
|
||||
<number>2</number>
|
||||
</attribute>
|
||||
<widget class="QWidget" name="dockWidgetContents_3">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="BECStatusBox" name="bec_status_box_2"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<widget class="QDockWidget" name="dock_queue">
|
||||
<property name="windowTitle">
|
||||
<string>Scan Queue</string>
|
||||
</property>
|
||||
<attribute name="dockWidgetArea">
|
||||
<number>2</number>
|
||||
</attribute>
|
||||
<widget class="QWidget" name="dockWidgetContents_4">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_6">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="BECQueue" name="bec_queue">
|
||||
<row/>
|
||||
<column/>
|
||||
<column/>
|
||||
<column/>
|
||||
<item row="0" column="0"/>
|
||||
<item row="0" column="1"/>
|
||||
<item row="0" column="2"/>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<action name="action_BEC_docs">
|
||||
<property name="icon">
|
||||
<iconset theme="QIcon::ThemeIcon::DialogQuestion"/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>BEC Docs</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_BEC_widgets_docs">
|
||||
<property name="icon">
|
||||
<iconset theme="QIcon::ThemeIcon::DialogQuestion"/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>BEC Widgets Docs</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_bug_report">
|
||||
<property name="icon">
|
||||
<iconset theme="QIcon::ThemeIcon::DialogError"/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Bug Report</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_light">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Light</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_dark">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Dark</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>WebsiteWidget</class>
|
||||
<extends>QWebEngineView</extends>
|
||||
<header>website_widget</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>BECQueue</class>
|
||||
<extends>QTableWidget</extends>
|
||||
<header>bec_queue</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>ScanControl</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>scan_control</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>VSCodeEditor</class>
|
||||
<extends>WebsiteWidget</extends>
|
||||
<header>vs_code_editor</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>BECStatusBox</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>bec_status_box</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>BECDockArea</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>dock_area</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>QWebEngineView</class>
|
||||
<extends></extends>
|
||||
<header location="global">QtWebEngineWidgets/QWebEngineView</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -1,74 +1,180 @@
|
||||
from bec_lib.logger import bec_logger
|
||||
from qtpy.QtWidgets import QApplication, QMainWindow
|
||||
import os
|
||||
|
||||
from bec_widgets.cli.rpc.rpc_register import RPCRegister
|
||||
from qtpy.QtCore import QSize
|
||||
from qtpy.QtGui import QAction, QActionGroup, QIcon
|
||||
from qtpy.QtWidgets import QApplication, QMainWindow, QStyle
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.utils import UILoader
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.colors import apply_theme
|
||||
from bec_widgets.utils.container_utils import WidgetContainerUtils
|
||||
from bec_widgets.widgets.containers.dock.dock_area import BECDockArea
|
||||
from bec_widgets.utils.error_popups import SafeSlot
|
||||
from bec_widgets.utils.widget_io import WidgetHierarchy
|
||||
from bec_widgets.widgets.containers.main_window.addons.web_links import BECWebLinksMixin
|
||||
|
||||
logger = bec_logger.logger
|
||||
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
||||
|
||||
|
||||
class BECMainWindow(BECWidget, QMainWindow):
|
||||
def __init__(self, gui_id: str = None, *args, **kwargs):
|
||||
BECWidget.__init__(self, gui_id=gui_id, **kwargs)
|
||||
QMainWindow.__init__(self, *args, **kwargs)
|
||||
RPC = False
|
||||
|
||||
def _dump(self):
|
||||
"""Return a dictionary with informations about the application state, for use in tests"""
|
||||
# TODO: ModularToolBar and something else leak top-level widgets (3 or 4 QMenu + 2 QWidget);
|
||||
# so, a filtering based on title is applied here, but the solution is to not have those widgets
|
||||
# as top-level (so for now, a window with no title does not appear in _dump() result)
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
gui_id: str = None,
|
||||
client=None,
|
||||
window_title: str = "BEC",
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(parent=parent, gui_id=gui_id, **kwargs)
|
||||
|
||||
# NOTE: the main window itself is excluded, since we want to dump dock areas
|
||||
info = {
|
||||
tlw.gui_id: {
|
||||
"title": tlw.windowTitle(),
|
||||
"visible": tlw.isVisible(),
|
||||
"class": str(type(tlw)),
|
||||
}
|
||||
for tlw in QApplication.instance().topLevelWidgets()
|
||||
if tlw is not self and tlw.windowTitle()
|
||||
}
|
||||
# Add the main window dock area
|
||||
info[self.centralWidget().gui_id] = {
|
||||
"title": self.windowTitle(),
|
||||
"visible": self.isVisible(),
|
||||
"class": str(type(self.centralWidget())),
|
||||
}
|
||||
return info
|
||||
self.app = QApplication.instance()
|
||||
self.setWindowTitle(window_title)
|
||||
self._init_ui()
|
||||
|
||||
def new_dock_area(
|
||||
self, name: str | None = None, geometry: tuple[int, int, int, int] | None = None
|
||||
) -> BECDockArea:
|
||||
"""Create a new dock area.
|
||||
def _init_ui(self):
|
||||
|
||||
Args:
|
||||
name(str): The name of the dock area.
|
||||
geometry(tuple): The geometry parameters to be passed to the dock area.
|
||||
Returns:
|
||||
BECDockArea: The newly created dock area.
|
||||
# Set the icon
|
||||
self._init_bec_icon()
|
||||
|
||||
# Set Menu and Status bar
|
||||
self._setup_menu_bar()
|
||||
|
||||
# BEC Specific UI
|
||||
self.display_app_id()
|
||||
|
||||
def _init_bec_icon(self):
|
||||
icon = self.app.windowIcon()
|
||||
if icon.isNull():
|
||||
print("No icon is set, setting default icon")
|
||||
icon = QIcon()
|
||||
icon.addFile(
|
||||
os.path.join(MODULE_PATH, "assets", "app_icons", "bec_widgets_icon.png"),
|
||||
size=QSize(48, 48),
|
||||
)
|
||||
self.app.setWindowIcon(icon)
|
||||
else:
|
||||
print("An icon is set")
|
||||
|
||||
def load_ui(self, ui_file):
|
||||
loader = UILoader(self)
|
||||
self.ui = loader.loader(ui_file)
|
||||
self.setCentralWidget(self.ui)
|
||||
|
||||
def display_app_id(self):
|
||||
server_id = self.bec_dispatcher.cli_server.gui_id
|
||||
self.statusBar().showMessage(f"App ID: {server_id}")
|
||||
|
||||
def _fetch_theme(self) -> str:
|
||||
return self.app.theme.theme
|
||||
|
||||
def _setup_menu_bar(self):
|
||||
"""
|
||||
with RPCRegister.delayed_broadcast() as rpc_register:
|
||||
existing_dock_areas = rpc_register.get_names_of_rpc_by_class_type(BECDockArea)
|
||||
if name is not None:
|
||||
if name in existing_dock_areas:
|
||||
raise ValueError(
|
||||
f"Name {name} must be unique for dock areas, but already exists: {existing_dock_areas}."
|
||||
)
|
||||
else:
|
||||
name = "dock_area"
|
||||
name = WidgetContainerUtils.generate_unique_name(name, existing_dock_areas)
|
||||
dock_area = BECDockArea(name=name)
|
||||
dock_area.resize(dock_area.minimumSizeHint())
|
||||
# TODO Should we simply use the specified name as title here?
|
||||
dock_area.window().setWindowTitle(f"BEC - {name}")
|
||||
logger.info(f"Created new dock area: {name}")
|
||||
logger.info(f"Existing dock areas: {geometry}")
|
||||
if geometry is not None:
|
||||
dock_area.setGeometry(*geometry)
|
||||
dock_area.show()
|
||||
return dock_area
|
||||
Setup the menu bar for the main window.
|
||||
"""
|
||||
menu_bar = self.menuBar()
|
||||
|
||||
########################################
|
||||
# Theme menu
|
||||
theme_menu = menu_bar.addMenu("Theme")
|
||||
|
||||
theme_group = QActionGroup(self)
|
||||
light_theme_action = QAction("Light Theme", self, checkable=True)
|
||||
dark_theme_action = QAction("Dark Theme", self, checkable=True)
|
||||
theme_group.addAction(light_theme_action)
|
||||
theme_group.addAction(dark_theme_action)
|
||||
theme_group.setExclusive(True)
|
||||
|
||||
theme_menu.addAction(light_theme_action)
|
||||
theme_menu.addAction(dark_theme_action)
|
||||
|
||||
# Connect theme actions
|
||||
light_theme_action.triggered.connect(lambda: self.change_theme("light"))
|
||||
dark_theme_action.triggered.connect(lambda: self.change_theme("dark"))
|
||||
|
||||
# Set the default theme
|
||||
theme = self.app.theme.theme
|
||||
if theme == "light":
|
||||
light_theme_action.setChecked(True)
|
||||
elif theme == "dark":
|
||||
dark_theme_action.setChecked(True)
|
||||
|
||||
########################################
|
||||
# Help menu
|
||||
help_menu = menu_bar.addMenu("Help")
|
||||
|
||||
help_icon = QApplication.style().standardIcon(QStyle.SP_MessageBoxQuestion)
|
||||
bug_icon = QApplication.style().standardIcon(QStyle.SP_MessageBoxInformation)
|
||||
|
||||
bec_docs = QAction("BEC Docs", self)
|
||||
bec_docs.setIcon(help_icon)
|
||||
widgets_docs = QAction("BEC Widgets Docs", self)
|
||||
widgets_docs.setIcon(help_icon)
|
||||
bug_report = QAction("Bug Report", self)
|
||||
bug_report.setIcon(bug_icon)
|
||||
|
||||
bec_docs.triggered.connect(BECWebLinksMixin.open_bec_docs)
|
||||
widgets_docs.triggered.connect(BECWebLinksMixin.open_bec_widgets_docs)
|
||||
bug_report.triggered.connect(BECWebLinksMixin.open_bec_bug_report)
|
||||
|
||||
help_menu.addAction(bec_docs)
|
||||
help_menu.addAction(widgets_docs)
|
||||
help_menu.addAction(bug_report)
|
||||
|
||||
@SafeSlot(str)
|
||||
def change_theme(self, theme: str):
|
||||
apply_theme(theme)
|
||||
|
||||
def cleanup(self):
|
||||
super().close()
|
||||
central_widget = self.centralWidget()
|
||||
central_widget.close()
|
||||
central_widget.deleteLater()
|
||||
super().cleanup()
|
||||
|
||||
|
||||
class WindowWithUi(BECMainWindow):
|
||||
"""
|
||||
This is just testing app wiht UI file which could be connected to RPC.
|
||||
|
||||
"""
|
||||
|
||||
USER_ACCESS = ["new_dock_area", "all_connections", "change_theme", "hierarchy"]
|
||||
|
||||
def __init__(self, *args, name: str = None, **kwargs):
|
||||
super().__init__(gui_id="test", *args, **kwargs)
|
||||
if name is None:
|
||||
name = self.__class__.__name__
|
||||
else:
|
||||
if not WidgetContainerUtils.has_name_valid_chars(name):
|
||||
raise ValueError(f"Name {name} contains invalid characters.")
|
||||
self._name = name if name else self.__class__.__name__
|
||||
ui_file_path = os.path.join(os.path.dirname(__file__), "example_app.ui")
|
||||
self.load_ui(ui_file_path)
|
||||
|
||||
def load_ui(self, ui_file):
|
||||
loader = UILoader(self)
|
||||
self.ui = loader.loader(ui_file)
|
||||
self.setCentralWidget(self.ui)
|
||||
|
||||
@property
|
||||
def all_connections(self) -> list:
|
||||
all_connections = self.rpc_register.list_all_connections()
|
||||
all_connections_keys = list(all_connections.keys())
|
||||
return all_connections_keys
|
||||
|
||||
def hierarchy(self):
|
||||
WidgetHierarchy.print_widget_hierarchy(self, only_bec_widgets=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
print(id(app))
|
||||
# app = BECApplication(sys.argv)
|
||||
# print(id(app))
|
||||
main_window = WindowWithUi()
|
||||
main_window.show()
|
||||
sys.exit(app.exec())
|
||||
|
||||
@@ -11,6 +11,7 @@ class AbortButton(BECWidget, QWidget):
|
||||
|
||||
PLUGIN = True
|
||||
ICON_NAME = "cancel"
|
||||
RPC = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -22,9 +23,7 @@ class AbortButton(BECWidget, QWidget):
|
||||
scan_id=None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(client=client, config=config, gui_id=gui_id, **kwargs)
|
||||
QWidget.__init__(self, parent=parent)
|
||||
|
||||
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
|
||||
self.get_bec_shortcuts()
|
||||
|
||||
self.layout = QHBoxLayout(self)
|
||||
|
||||
@@ -11,11 +11,10 @@ class ResetButton(BECWidget, QWidget):
|
||||
|
||||
PLUGIN = True
|
||||
ICON_NAME = "restart_alt"
|
||||
RPC = False
|
||||
|
||||
def __init__(self, parent=None, client=None, config=None, gui_id=None, toolbar=False, **kwargs):
|
||||
super().__init__(client=client, config=config, gui_id=gui_id, **kwargs)
|
||||
QWidget.__init__(self, parent=parent)
|
||||
|
||||
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
|
||||
self.get_bec_shortcuts()
|
||||
|
||||
self.layout = QHBoxLayout(self)
|
||||
|
||||
@@ -11,10 +11,10 @@ class ResumeButton(BECWidget, QWidget):
|
||||
|
||||
PLUGIN = True
|
||||
ICON_NAME = "resume"
|
||||
RPC = False
|
||||
|
||||
def __init__(self, parent=None, client=None, config=None, gui_id=None, toolbar=False, **kwargs):
|
||||
super().__init__(client=client, config=config, gui_id=gui_id, **kwargs)
|
||||
QWidget.__init__(self, parent=parent)
|
||||
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
|
||||
|
||||
self.get_bec_shortcuts()
|
||||
|
||||
|
||||
@@ -11,10 +11,10 @@ class StopButton(BECWidget, QWidget):
|
||||
|
||||
PLUGIN = True
|
||||
ICON_NAME = "dangerous"
|
||||
RPC = False
|
||||
|
||||
def __init__(self, parent=None, client=None, config=None, gui_id=None, toolbar=False, **kwargs):
|
||||
super().__init__(client=client, config=config, gui_id=gui_id, **kwargs)
|
||||
QWidget.__init__(self, parent=parent)
|
||||
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
|
||||
|
||||
self.get_bec_shortcuts()
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ class PositionIndicator(BECWidget, QWidget):
|
||||
ICON_NAME = "horizontal_distribute"
|
||||
|
||||
def __init__(self, parent=None, client=None, config=None, gui_id=None, **kwargs):
|
||||
super().__init__(client=client, config=config, gui_id=gui_id, **kwargs)
|
||||
QWidget.__init__(self, parent=parent)
|
||||
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
|
||||
|
||||
self.position = 50
|
||||
self.min_value = 0
|
||||
self.max_value = 100
|
||||
|
||||
@@ -47,6 +47,7 @@ class PositionerBoxBase(BECWidget, CompactPopupWidget):
|
||||
|
||||
current_path = ""
|
||||
ICON_NAME = "switch_right"
|
||||
RPC = False
|
||||
|
||||
def __init__(self, parent=None, **kwargs):
|
||||
"""Initialize the PositionerBox widget.
|
||||
@@ -55,8 +56,7 @@ class PositionerBoxBase(BECWidget, CompactPopupWidget):
|
||||
parent: The parent widget.
|
||||
device (Positioner): The device to control.
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
CompactPopupWidget.__init__(self, parent=parent, layout=QVBoxLayout)
|
||||
super().__init__(parent=parent, layout=QVBoxLayout, **kwargs)
|
||||
self._dialog = None
|
||||
self.get_bec_shortcuts()
|
||||
|
||||
|
||||
@@ -69,8 +69,7 @@ class PositionerGroup(BECWidget, QWidget):
|
||||
Args:
|
||||
parent: The parent widget.
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
QWidget.__init__(self, parent)
|
||||
super().__init__(parent=parent, **kwargs)
|
||||
|
||||
self.get_bec_shortcuts()
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ class DeviceSignalInputBase(BECWidget):
|
||||
signal object based on the current text of the widget.
|
||||
"""
|
||||
|
||||
RPC = False
|
||||
_filter_handler = {
|
||||
Kind.hinted: "include_hinted_signals",
|
||||
Kind.normal: "include_normal_signals",
|
||||
|
||||
@@ -47,8 +47,7 @@ class DeviceComboBox(DeviceInputBase, QComboBox):
|
||||
arg_name: str | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(client=client, config=config, gui_id=gui_id, **kwargs)
|
||||
QComboBox.__init__(self, parent=parent)
|
||||
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
|
||||
if arg_name is not None:
|
||||
self.config.arg_name = arg_name
|
||||
self.arg_name = arg_name
|
||||
|
||||
@@ -53,8 +53,7 @@ class DeviceLineEdit(DeviceInputBase, QLineEdit):
|
||||
self._callback_id = None
|
||||
self._is_valid_input = False
|
||||
self._accent_colors = get_accent_colors()
|
||||
super().__init__(client=client, config=config, gui_id=gui_id, **kwargs)
|
||||
QLineEdit.__init__(self, parent=parent)
|
||||
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
|
||||
self.completer = QCompleter(self)
|
||||
self.setCompleter(self.completer)
|
||||
|
||||
|
||||
@@ -40,8 +40,7 @@ class SignalComboBox(DeviceSignalInputBase, QComboBox):
|
||||
arg_name: str | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(client=client, config=config, gui_id=gui_id, **kwargs)
|
||||
QComboBox.__init__(self, parent=parent)
|
||||
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
|
||||
if arg_name is not None:
|
||||
self.config.arg_name = arg_name
|
||||
self.arg_name = arg_name
|
||||
|
||||
@@ -42,8 +42,7 @@ class SignalLineEdit(DeviceSignalInputBase, QLineEdit):
|
||||
**kwargs,
|
||||
):
|
||||
self._is_valid_input = False
|
||||
super().__init__(client=client, config=config, gui_id=gui_id, **kwargs)
|
||||
QLineEdit.__init__(self, parent=parent)
|
||||
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
|
||||
self._accent_colors = get_accent_colors()
|
||||
self.completer = QCompleter(self)
|
||||
self.setCompleter(self.completer)
|
||||
|
||||
@@ -65,8 +65,7 @@ class ScanControl(BECWidget, QWidget):
|
||||
config = ScanControlConfig(
|
||||
widget_class=self.__class__.__name__, allowed_scans=allowed_scans
|
||||
)
|
||||
super().__init__(client=client, gui_id=gui_id, config=config, **kwargs)
|
||||
QWidget.__init__(self, parent=parent)
|
||||
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
|
||||
|
||||
self._hide_add_remove_buttons = False
|
||||
|
||||
|
||||
@@ -44,8 +44,7 @@ class DapComboBox(BECWidget, QWidget):
|
||||
default_fit: str | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(client=client, gui_id=gui_id, **kwargs)
|
||||
QWidget.__init__(self, parent=parent)
|
||||
super().__init__(parent=parent, client=client, gui_id=gui_id, **kwargs)
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.fit_model_combobox = QComboBox(self)
|
||||
self.layout.addWidget(self.fit_model_combobox)
|
||||
|
||||
@@ -17,6 +17,7 @@ class LMFitDialog(BECWidget, QWidget):
|
||||
|
||||
PLUGIN = True
|
||||
ICON_NAME = "monitoring"
|
||||
RPC = False
|
||||
# Signal to emit the currently selected fit curve_id
|
||||
selected_fit = Signal(str)
|
||||
# Signal to emit a move action in form of a tuple (param_name, value)
|
||||
@@ -43,10 +44,8 @@ class LMFitDialog(BECWidget, QWidget):
|
||||
gui_id (str): GUI ID.
|
||||
ui_file (str): The UI file to be loaded.
|
||||
"""
|
||||
super().__init__(client=client, config=config, gui_id=gui_id, **kwargs)
|
||||
QWidget.__init__(self, parent=parent)
|
||||
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
|
||||
self.setProperty("skip_settings", True)
|
||||
self.setObjectName("LMFitDialog")
|
||||
self._ui_file = ui_file
|
||||
self.target_widget = target_widget
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ class ScanMetadata(BECWidget, QWidget):
|
||||
|
||||
metadata_updated = Signal(dict)
|
||||
metadata_cleared = Signal(NoneType)
|
||||
RPC = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -51,8 +52,7 @@ class ScanMetadata(BECWidget, QWidget):
|
||||
initial_extras: list[list[str]] | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(client=client, **kwargs)
|
||||
QWidget.__init__(self, parent=parent)
|
||||
super().__init__(parent=parent, client=client, **kwargs)
|
||||
|
||||
self.set_schema(scan_name)
|
||||
|
||||
|
||||
@@ -49,8 +49,7 @@ class TextBox(BECWidget, QWidget):
|
||||
if isinstance(config, dict):
|
||||
config = TextBoxConfig(**config)
|
||||
self.config = config
|
||||
super().__init__(client=client, config=config, gui_id=gui_id, **kwargs)
|
||||
QWidget.__init__(self, parent)
|
||||
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.text_box_text_edit = QTextEdit(parent=self)
|
||||
self.layout.addWidget(self.text_box_text_edit)
|
||||
|
||||
@@ -26,8 +26,7 @@ class WebsiteWidget(BECWidget, QWidget):
|
||||
def __init__(
|
||||
self, parent=None, url: str = None, config=None, client=None, gui_id=None, **kwargs
|
||||
):
|
||||
super().__init__(client=client, config=config, gui_id=gui_id, **kwargs)
|
||||
QWidget.__init__(self, parent=parent)
|
||||
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
|
||||
layout = QVBoxLayout()
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.website = QWebEngineView()
|
||||
|
||||
@@ -144,10 +144,10 @@ class Minesweeper(BECWidget, QWidget):
|
||||
PLUGIN = True
|
||||
ICON_NAME = "videogame_asset"
|
||||
USER_ACCESS = []
|
||||
RPC = False
|
||||
|
||||
def __init__(self, parent=None, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
QWidget.__init__(self, parent=parent)
|
||||
super().__init__(parent=parent, *args, **kwargs)
|
||||
|
||||
self._ui_initialised = False
|
||||
self._timer_start_num_seconds = 0
|
||||
|
||||
@@ -125,16 +125,15 @@ class Image(PlotBase):
|
||||
popups: bool = True,
|
||||
**kwargs,
|
||||
):
|
||||
self._main_image = ImageItem(parent_image=self)
|
||||
self._color_bar = None
|
||||
if config is None:
|
||||
config = ImageConfig(widget_class=self.__class__.__name__)
|
||||
self.gui_id = config.gui_id
|
||||
self._color_bar = None
|
||||
self._main_image = ImageItem()
|
||||
super().__init__(
|
||||
parent=parent, config=config, client=client, gui_id=gui_id, popups=popups, **kwargs
|
||||
)
|
||||
|
||||
# For PropertyManager identification
|
||||
self.setObjectName("Image")
|
||||
self._main_image.parent_image = self
|
||||
|
||||
self.plot_item.addItem(self._main_image)
|
||||
self.scan_id = None
|
||||
@@ -914,10 +913,14 @@ class Image(PlotBase):
|
||||
"""
|
||||
Disconnect the image update signals and clean up the image.
|
||||
"""
|
||||
# Main Image cleanup
|
||||
if self._main_image.config.monitor is not None:
|
||||
self.disconnect_monitor(self._main_image.config.monitor)
|
||||
self._main_image.config.monitor = None
|
||||
self.plot_item.removeItem(self._main_image)
|
||||
self._main_image = None
|
||||
|
||||
# Colorbar Cleanup
|
||||
if self._color_bar:
|
||||
if self.config.color_bar == "full":
|
||||
self.cleanup_histogram_lut_item(self._color_bar)
|
||||
@@ -926,6 +929,10 @@ class Image(PlotBase):
|
||||
self._color_bar.deleteLater()
|
||||
self._color_bar = None
|
||||
|
||||
# Toolbar cleanup
|
||||
self.toolbar.widgets["monitor"].widget.close()
|
||||
self.toolbar.widgets["monitor"].widget.deleteLater()
|
||||
|
||||
super().cleanup()
|
||||
|
||||
|
||||
|
||||
@@ -82,10 +82,12 @@ class ImageItem(BECConnector, pg.ImageItem):
|
||||
self.config = config
|
||||
else:
|
||||
self.config = config
|
||||
super().__init__(config=config, gui_id=gui_id)
|
||||
pg.ImageItem.__init__(self)
|
||||
|
||||
self.parent_image = parent_image
|
||||
if parent_image is not None:
|
||||
self.set_parent(parent_image)
|
||||
else:
|
||||
self.parent_image = None
|
||||
self.parent_id = None
|
||||
super().__init__(config=config, gui_id=gui_id, **kwargs)
|
||||
|
||||
self.raw_data = None
|
||||
self.buffer = []
|
||||
@@ -94,6 +96,13 @@ class ImageItem(BECConnector, pg.ImageItem):
|
||||
# Image processor will handle any setting of data
|
||||
self._image_processor = ImageProcessor(config=self.config.processing)
|
||||
|
||||
def set_parent(self, parent: BECConnector):
|
||||
self.parent_image = parent
|
||||
self.parent_id = parent.gui_id
|
||||
|
||||
def parent(self):
|
||||
return self.parent_image
|
||||
|
||||
def set_data(self, data: np.ndarray):
|
||||
self.raw_data = data
|
||||
self._process_image()
|
||||
|
||||
@@ -28,7 +28,9 @@ class MonitorSelectionToolbarBundle(ToolbarBundle):
|
||||
|
||||
# 1) Device combo box
|
||||
self.device_combo_box = DeviceComboBox(
|
||||
device_filter=BECDeviceFilter.DEVICE, readout_priority_filter=[ReadoutPriority.ASYNC]
|
||||
parent=self.target_widget,
|
||||
device_filter=BECDeviceFilter.DEVICE,
|
||||
readout_priority_filter=[ReadoutPriority.ASYNC],
|
||||
)
|
||||
self.device_combo_box.addItem("", None)
|
||||
self.device_combo_box.setCurrentText("")
|
||||
|
||||
@@ -161,9 +161,6 @@ class MotorMap(PlotBase):
|
||||
parent=parent, config=config, client=client, gui_id=gui_id, popups=popups, **kwargs
|
||||
)
|
||||
|
||||
# For PropertyManager identification
|
||||
self.setObjectName("MotorMap")
|
||||
|
||||
# Default values for PlotBase
|
||||
self.x_grid = True
|
||||
self.y_grid = True
|
||||
|
||||
@@ -20,7 +20,6 @@ class MotorMapSettings(SettingWidget):
|
||||
super().__init__(parent=parent, *args, **kwargs)
|
||||
|
||||
self.setProperty("skip_settings", True)
|
||||
self.setObjectName("MotorMapSettings")
|
||||
current_path = os.path.dirname(__file__)
|
||||
|
||||
form = UILoader().load_ui(os.path.join(current_path, "motor_map_settings.ui"), self)
|
||||
|
||||
@@ -126,9 +126,6 @@ class MultiWaveform(PlotBase):
|
||||
parent=parent, config=config, client=client, gui_id=gui_id, popups=popups, **kwargs
|
||||
)
|
||||
|
||||
# For PropertyManager identification
|
||||
self.setObjectName("MultiWaveform")
|
||||
|
||||
# Scan Data
|
||||
self.old_scan_id = None
|
||||
self.scan_id = None
|
||||
|
||||
@@ -20,7 +20,6 @@ class MultiWaveformControlPanel(SettingWidget):
|
||||
super().__init__(parent=parent, *args, **kwargs)
|
||||
|
||||
self.setProperty("skip_settings", True)
|
||||
self.setObjectName("MultiWaveformControlPanel")
|
||||
current_path = os.path.dirname(__file__)
|
||||
|
||||
form = UILoader().load_ui(os.path.join(current_path, "multi_waveform_controls.ui"), self)
|
||||
|
||||
@@ -29,7 +29,9 @@ class MultiWaveformSelectionToolbarBundle(ToolbarBundle):
|
||||
|
||||
# Monitor Selection
|
||||
self.monitor = DeviceComboBox(
|
||||
device_filter=BECDeviceFilter.DEVICE, readout_priority_filter=ReadoutPriority.ASYNC
|
||||
device_filter=BECDeviceFilter.DEVICE,
|
||||
readout_priority_filter=ReadoutPriority.ASYNC,
|
||||
parent_id=self.target_widget.gui_id,
|
||||
)
|
||||
self.monitor.addItem("", None)
|
||||
self.monitor.setCurrentText("")
|
||||
@@ -38,7 +40,7 @@ class MultiWaveformSelectionToolbarBundle(ToolbarBundle):
|
||||
self.add_action("monitor", WidgetAction(widget=self.monitor, adjust_size=False))
|
||||
|
||||
# Colormap Selection
|
||||
self.colormap_widget = BECColorMapWidget(cmap="magma")
|
||||
self.colormap_widget = BECColorMapWidget(cmap="magma", parent_id=self.target_widget.gui_id)
|
||||
self.add_action("color_map", WidgetAction(widget=self.colormap_widget, adjust_size=False))
|
||||
|
||||
# Connect slots, a device will be connected upon change of any combobox
|
||||
|
||||
@@ -74,11 +74,9 @@ class PlotBase(BECWidget, QWidget):
|
||||
) -> None:
|
||||
if config is None:
|
||||
config = ConnectionConfig(widget_class=self.__class__.__name__)
|
||||
super().__init__(client=client, gui_id=gui_id, config=config, **kwargs)
|
||||
QWidget.__init__(self, parent=parent)
|
||||
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
|
||||
|
||||
# For PropertyManager identification
|
||||
self.setObjectName("PlotBase")
|
||||
self.get_bec_shortcuts()
|
||||
|
||||
# Layout Management
|
||||
@@ -1018,7 +1016,7 @@ if __name__ == "__main__": # pragma: no cover:
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
window = DemoPlotBase()
|
||||
window = PlotBase()
|
||||
window.show()
|
||||
|
||||
sys.exit(app.exec_())
|
||||
|
||||
@@ -77,13 +77,16 @@ class ScatterCurve(BECConnector, pg.PlotDataItem):
|
||||
else:
|
||||
self.config = config
|
||||
name = config.label
|
||||
super().__init__(config=config, gui_id=gui_id)
|
||||
pg.PlotDataItem.__init__(self, name=name)
|
||||
|
||||
self.parent_item = parent_item
|
||||
self.parent_id = self.parent_item.gui_id
|
||||
super().__init__(name=name, config=config, gui_id=gui_id, **kwargs)
|
||||
|
||||
self.data_z = None # color scaling needs to be cashed for changing colormap
|
||||
self.apply_config()
|
||||
|
||||
def parent(self):
|
||||
return self.parent_item
|
||||
|
||||
def apply_config(self, config: dict | ScatterCurveConfig | None = None, **kwargs) -> None:
|
||||
"""
|
||||
Apply the configuration to the curve.
|
||||
|
||||
@@ -111,8 +111,6 @@ class ScatterWaveform(PlotBase):
|
||||
parent=parent, config=config, client=client, gui_id=gui_id, popups=popups, **kwargs
|
||||
)
|
||||
self._main_curve = ScatterCurve(parent_item=self)
|
||||
# For PropertyManager identification
|
||||
self.setObjectName("ScatterWaveform")
|
||||
|
||||
# Specific GUI elements
|
||||
self.scatter_dialog = None
|
||||
@@ -130,13 +128,18 @@ class ScatterWaveform(PlotBase):
|
||||
self.proxy_update_sync = pg.SignalProxy(
|
||||
self.sync_signal_update, rateLimit=25, slot=self.update_sync_curves
|
||||
)
|
||||
|
||||
self._init_scatter_curve_settings()
|
||||
self.update_with_scan_history(-1)
|
||||
|
||||
################################################################################
|
||||
# Widget Specific GUI interactions
|
||||
################################################################################
|
||||
def add_side_menus(self):
|
||||
"""
|
||||
Add the side menus to the ScatterWaveform widget.
|
||||
"""
|
||||
super().add_side_menus()
|
||||
self._init_scatter_curve_settings()
|
||||
|
||||
def _init_scatter_curve_settings(self):
|
||||
"""
|
||||
Initialize the scatter curve settings menu.
|
||||
@@ -489,6 +492,16 @@ class ScatterWaveform(PlotBase):
|
||||
self.crosshair.clear_markers()
|
||||
self._main_curve.clear()
|
||||
|
||||
def cleanup(self):
|
||||
"""
|
||||
Cleanup the widget and disconnect all signals.
|
||||
"""
|
||||
self.bec_dispatcher.disconnect_slot(self.on_scan_status, MessageEndpoints.scan_status())
|
||||
self.bec_dispatcher.disconnect_slot(self.on_scan_progress, MessageEndpoints.scan_progress())
|
||||
self.plot_item.removeItem(self._main_curve)
|
||||
self._main_curve = None
|
||||
super().cleanup()
|
||||
|
||||
|
||||
class DemoApp(QMainWindow): # pragma: no cover
|
||||
def __init__(self):
|
||||
|
||||
@@ -15,7 +15,6 @@ class ScatterCurveSettings(SettingWidget):
|
||||
# and should mirror what is in the target widget.
|
||||
# Saving settings for this widget could result in recursively setting the target widget.
|
||||
self.setProperty("skip_settings", True)
|
||||
self.setObjectName("ScatterCurveSettings")
|
||||
|
||||
current_path = os.path.dirname(__file__)
|
||||
if popup:
|
||||
@@ -123,3 +122,17 @@ class ScatterCurveSettings(SettingWidget):
|
||||
color_map=color_map,
|
||||
validate_bec=validate_bec,
|
||||
)
|
||||
|
||||
def cleanup(self):
|
||||
self.ui.x_name.close()
|
||||
self.ui.x_name.deleteLater()
|
||||
self.ui.x_entry.close()
|
||||
self.ui.x_entry.deleteLater()
|
||||
self.ui.y_name.close()
|
||||
self.ui.y_name.deleteLater()
|
||||
self.ui.y_entry.close()
|
||||
self.ui.y_entry.deleteLater()
|
||||
self.ui.z_name.close()
|
||||
self.ui.z_name.deleteLater()
|
||||
self.ui.z_entry.close()
|
||||
self.ui.z_entry.deleteLater()
|
||||
|
||||
@@ -16,7 +16,6 @@ class AxisSettings(SettingWidget):
|
||||
# and should mirror what is in the target widget.
|
||||
# Saving settings for this widget could result in recursively setting the target widget.
|
||||
self.setProperty("skip_settings", True)
|
||||
self.setObjectName("AxisSettings")
|
||||
current_path = os.path.dirname(__file__)
|
||||
if popup:
|
||||
form = UILoader().load_ui(
|
||||
|
||||
@@ -90,16 +90,19 @@ class Curve(BECConnector, pg.PlotDataItem):
|
||||
self.config = config
|
||||
else:
|
||||
self.config = config
|
||||
super().__init__(config=config, gui_id=gui_id)
|
||||
pg.PlotDataItem.__init__(self, name=name)
|
||||
|
||||
self.parent_item = parent_item
|
||||
self.parent_id = self.parent_item.gui_id
|
||||
super().__init__(name=name, config=config, gui_id=gui_id, **kwargs)
|
||||
|
||||
self.apply_config()
|
||||
self.dap_params = None
|
||||
self.dap_summary = None
|
||||
if kwargs:
|
||||
self.set(**kwargs)
|
||||
|
||||
def parent(self):
|
||||
return self.parent_item
|
||||
|
||||
def apply_config(self, config: dict | CurveConfig | None = None, **kwargs) -> None:
|
||||
"""
|
||||
Apply the configuration to the curve.
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from PySide6.QtGui import QCloseEvent
|
||||
from qtpy.QtWidgets import (
|
||||
QComboBox,
|
||||
QGroupBox,
|
||||
@@ -51,7 +52,7 @@ class CurveSetting(SettingWidget):
|
||||
self.spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
|
||||
self.device_x_label = QLabel("Device")
|
||||
self.device_x = DeviceLineEdit()
|
||||
self.device_x = DeviceLineEdit(parent=self)
|
||||
|
||||
self._get_x_mode_from_waveform()
|
||||
self.switch_x_device_selection()
|
||||
@@ -107,3 +108,10 @@ class CurveSetting(SettingWidget):
|
||||
"""Refresh the curve tree and the x axis combo box in the case Waveform is modified from rpc."""
|
||||
self.curve_manager.refresh_from_waveform()
|
||||
self._get_x_mode_from_waveform()
|
||||
|
||||
def cleanup(self):
|
||||
"""Cleanup the widget."""
|
||||
self.device_x.close()
|
||||
self.device_x.deleteLater()
|
||||
self.curve_manager.close()
|
||||
self.curve_manager.deleteLater()
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
import json
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
from bec_qthemes._icon.material_icons import material_icon
|
||||
from qtpy.QtGui import QColor
|
||||
from qtpy.QtWidgets import (
|
||||
@@ -36,6 +37,9 @@ if TYPE_CHECKING: # pragma: no cover
|
||||
from bec_widgets.widgets.plots.waveform.waveform import Waveform
|
||||
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class ColorButton(QPushButton):
|
||||
"""A QPushButton subclass that displays a color.
|
||||
|
||||
@@ -155,7 +159,7 @@ class CurveRow(QTreeWidgetItem):
|
||||
"""Create columns 1 and 2. For device rows, we have device/entry edits; for dap rows, label/model combo."""
|
||||
if self.source == "device":
|
||||
# Device row: columns 1..2 are device line edits
|
||||
self.device_edit = DeviceLineEdit()
|
||||
self.device_edit = DeviceLineEdit(parent=self.tree)
|
||||
self.entry_edit = QLineEdit() # TODO in future will be signal line edit
|
||||
if self.config.signal:
|
||||
self.device_edit.setText(self.config.signal.name or "")
|
||||
@@ -168,7 +172,7 @@ class CurveRow(QTreeWidgetItem):
|
||||
# DAP row: column1= "Model" label, column2= DapComboBox
|
||||
self.label_widget = QLabel("Model")
|
||||
self.tree.setItemWidget(self, 1, self.label_widget)
|
||||
self.dap_combo = DapComboBox()
|
||||
self.dap_combo = DapComboBox(parent=self.tree)
|
||||
self.dap_combo.populate_fit_model_combobox()
|
||||
# If config.signal has a dap
|
||||
if self.config.signal and self.config.signal.dap:
|
||||
@@ -320,6 +324,10 @@ class CurveRow(QTreeWidgetItem):
|
||||
|
||||
return self.config.model_dump()
|
||||
|
||||
def closeEvent(self, event) -> None:
|
||||
logger.info(f"CurveRow closeEvent: {self.config.label}")
|
||||
return super().closeEvent(event)
|
||||
|
||||
|
||||
class CurveTree(BECWidget, QWidget):
|
||||
"""A tree widget that manages device and DAP curves."""
|
||||
@@ -334,11 +342,11 @@ class CurveTree(BECWidget, QWidget):
|
||||
client=None,
|
||||
gui_id: str | None = None,
|
||||
waveform: Waveform | None = None,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
if config is None:
|
||||
config = ConnectionConfig(widget_class=self.__class__.__name__)
|
||||
super().__init__(client=client, gui_id=gui_id, config=config)
|
||||
QWidget.__init__(self, parent=parent)
|
||||
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
|
||||
|
||||
self.waveform = waveform
|
||||
if self.waveform and hasattr(self.waveform, "color_palette"):
|
||||
@@ -535,3 +543,17 @@ class CurveTree(BECWidget, QWidget):
|
||||
for dap in dap_curves:
|
||||
if dap.config.parent_label == dev.config.label:
|
||||
CurveRow(self.tree, parent_item=dr, config=dap.config, device_manager=self.dev)
|
||||
|
||||
def cleanup(self):
|
||||
"""Cleanup the widget."""
|
||||
for item in self.all_items:
|
||||
if hasattr(item, "device_edit"):
|
||||
item.device_edit.close()
|
||||
item.device_edit.deleteLater()
|
||||
if hasattr(item, "dap_combo"):
|
||||
item.dap_combo.close()
|
||||
item.dap_combo.deleteLater()
|
||||
|
||||
def closeEvent(self, event):
|
||||
self.cleanup()
|
||||
return super().closeEvent(event)
|
||||
|
||||
@@ -10,7 +10,15 @@ from bec_lib import bec_logger, messages
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from pydantic import Field, ValidationError, field_validator
|
||||
from qtpy.QtCore import QTimer, Signal
|
||||
from qtpy.QtWidgets import QDialog, QHBoxLayout, QMainWindow, QVBoxLayout, QWidget
|
||||
from qtpy.QtWidgets import (
|
||||
QApplication,
|
||||
QDialog,
|
||||
QHBoxLayout,
|
||||
QMainWindow,
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from bec_widgets.utils import ConnectionConfig
|
||||
from bec_widgets.utils.bec_signal_proxy import BECSignalProxy
|
||||
@@ -18,6 +26,7 @@ from bec_widgets.utils.colors import Colors, set_theme
|
||||
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
||||
from bec_widgets.utils.settings_dialog import SettingsDialog
|
||||
from bec_widgets.utils.toolbar import MaterialIconAction
|
||||
from bec_widgets.utils.widget_io import WidgetHierarchy
|
||||
from bec_widgets.widgets.dap.lmfit_dialog.lmfit_dialog import LMFitDialog
|
||||
from bec_widgets.widgets.plots.plot_base import PlotBase
|
||||
from bec_widgets.widgets.plots.waveform.curve import Curve, CurveConfig, DeviceSignal
|
||||
@@ -128,9 +137,6 @@ class Waveform(PlotBase):
|
||||
parent=parent, config=config, client=client, gui_id=gui_id, popups=popups, **kwargs
|
||||
)
|
||||
|
||||
# For PropertyManager identification
|
||||
self.setObjectName("Waveform")
|
||||
|
||||
# Curve data
|
||||
self._sync_curves = []
|
||||
self._async_curves = []
|
||||
@@ -282,6 +288,8 @@ class Waveform(PlotBase):
|
||||
"""
|
||||
Slot for when the axis settings dialog is closed.
|
||||
"""
|
||||
self.curve_settings_dialog.close()
|
||||
self.curve_settings_dialog.deleteLater()
|
||||
self.curve_settings_dialog = None
|
||||
self.toolbar.widgets["curve"].action.setChecked(False)
|
||||
|
||||
@@ -876,6 +884,7 @@ class Waveform(PlotBase):
|
||||
self.on_async_readback,
|
||||
MessageEndpoints.device_async_readback(self.scan_id, curve.name()),
|
||||
)
|
||||
curve.rpc_register.remove_rpc(curve)
|
||||
|
||||
# Remove itself from the DAP summary only for side panels
|
||||
if (
|
||||
@@ -1406,7 +1415,6 @@ class Waveform(PlotBase):
|
||||
found_sync = True
|
||||
else:
|
||||
logger.warning("Device {dev_name} not found in readout priority list.")
|
||||
|
||||
# Determine the mode of the scan
|
||||
if found_async and found_sync:
|
||||
mode = "mixed"
|
||||
@@ -1440,18 +1448,31 @@ class Waveform(PlotBase):
|
||||
logger.warning(f"Neither scan_id or scan_number was provided, fetching the latest scan")
|
||||
scan_index = -1
|
||||
|
||||
if scan_index is not None:
|
||||
if len(self.client.history) == 0:
|
||||
logger.info("No scans executed so far. Skipping scan history update.")
|
||||
return
|
||||
|
||||
self.scan_item = self.client.history[scan_index]
|
||||
metadata = self.scan_item.metadata
|
||||
self.scan_id = metadata["bec"]["scan_id"]
|
||||
else:
|
||||
if scan_index is None:
|
||||
self.scan_id = scan_id
|
||||
self.scan_item = self.client.history.get_by_scan_id(scan_id)
|
||||
self._emit_signal_update()
|
||||
return
|
||||
|
||||
if scan_index == -1:
|
||||
scan_item = self.client.queue.scan_storage.current_scan
|
||||
if scan_item is not None:
|
||||
self.scan_item = scan_item
|
||||
self.scan_id = scan_item.scan_id
|
||||
self._emit_signal_update()
|
||||
return
|
||||
|
||||
if len(self.client.history) == 0:
|
||||
logger.info("No scans executed so far. Skipping scan history update.")
|
||||
return
|
||||
|
||||
self.scan_item = self.client.history[scan_index]
|
||||
metadata = self.scan_item.metadata
|
||||
self.scan_id = metadata["bec"]["scan_id"]
|
||||
|
||||
self._emit_signal_update()
|
||||
|
||||
def _emit_signal_update(self):
|
||||
self._categorise_device_curves()
|
||||
|
||||
self.setup_dap_for_scan()
|
||||
@@ -1574,9 +1595,11 @@ class Waveform(PlotBase):
|
||||
self.clear_all()
|
||||
if self.curve_settings_dialog is not None:
|
||||
self.curve_settings_dialog.close()
|
||||
self.curve_settings_dialog.deleteLater()
|
||||
self.curve_settings_dialog = None
|
||||
if self.dap_summary_dialog is not None:
|
||||
self.dap_summary_dialog.close()
|
||||
self.dap_summary_dialog.deleteLater()
|
||||
self.dap_summary_dialog = None
|
||||
super().cleanup()
|
||||
|
||||
@@ -1586,7 +1609,7 @@ class DemoApp(QMainWindow): # pragma: no cover
|
||||
super().__init__()
|
||||
self.setWindowTitle("Waveform Demo")
|
||||
self.resize(800, 600)
|
||||
self.main_widget = QWidget()
|
||||
self.main_widget = QWidget(self)
|
||||
self.layout = QHBoxLayout(self.main_widget)
|
||||
self.setCentralWidget(self.main_widget)
|
||||
|
||||
@@ -1597,15 +1620,26 @@ class DemoApp(QMainWindow): # pragma: no cover
|
||||
self.waveform_side.plot(y_name="bpm4i", y_entry="bpm4i", dap="GaussianModel")
|
||||
self.waveform_side.plot(y_name="bpm3a", y_entry="bpm3a")
|
||||
|
||||
self.hierarchy_button = QPushButton()
|
||||
self.hierarchy_button.setText("Hierarchy")
|
||||
self.hierarchy_button.clicked.connect(self.hierarchy)
|
||||
|
||||
self.layout.addWidget(self.waveform_side)
|
||||
self.layout.addWidget(self.waveform_popup)
|
||||
self.layout.addWidget(self.hierarchy_button)
|
||||
|
||||
def hierarchy(self):
|
||||
print("getting app")
|
||||
WidgetHierarchy.print_becconnector_hierarchy_from_app() # , only_bec_widgets=True)
|
||||
print("Waveform popup")
|
||||
print(self.waveform_popup.objectName())
|
||||
print("Waveform side")
|
||||
print(self.waveform_side.objectName())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
set_theme("dark")
|
||||
widget = DemoApp()
|
||||
|
||||
@@ -25,8 +25,7 @@ class BECProgressBar(BECWidget, QWidget):
|
||||
ICON_NAME = "page_control"
|
||||
|
||||
def __init__(self, parent=None, client=None, config=None, gui_id=None, **kwargs):
|
||||
super().__init__(client=client, config=config, gui_id=gui_id, **kwargs)
|
||||
QWidget.__init__(self, parent=parent)
|
||||
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
|
||||
|
||||
accent_colors = get_accent_colors()
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from bec_lib.endpoints import EndpointInfo, MessageEndpoints
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from pydantic_core import PydanticCustomError
|
||||
from qtpy import QtGui
|
||||
from qtpy.QtCore import QObject
|
||||
|
||||
from bec_widgets.utils import BECConnector, ConnectionConfig
|
||||
|
||||
@@ -77,7 +78,7 @@ class RingConfig(ProgressbarConfig):
|
||||
)
|
||||
|
||||
|
||||
class Ring(BECConnector):
|
||||
class Ring(BECConnector, QObject):
|
||||
USER_ACCESS = [
|
||||
"_get_all_rpc",
|
||||
"_rpc_id",
|
||||
@@ -108,7 +109,7 @@ class Ring(BECConnector):
|
||||
if isinstance(config, dict):
|
||||
config = RingConfig(**config)
|
||||
self.config = config
|
||||
super().__init__(client=client, config=config, gui_id=gui_id, **kwargs)
|
||||
super().__init__(parent=parent, client=client, config=config, gui_id=gui_id, **kwargs)
|
||||
|
||||
self.parent_progress_widget = parent_progress_widget
|
||||
self.color = None
|
||||
|
||||
@@ -110,8 +110,7 @@ class RingProgressBar(BECWidget, QWidget):
|
||||
if isinstance(config, dict):
|
||||
config = RingProgressBarConfig(**config, widget_class=self.__class__.__name__)
|
||||
self.config = config
|
||||
super().__init__(client=client, config=config, gui_id=gui_id, **kwargs)
|
||||
QWidget.__init__(self, parent=parent)
|
||||
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
|
||||
|
||||
self.get_bec_shortcuts()
|
||||
self.entry_validator = EntryValidator(self.dev)
|
||||
|
||||
@@ -44,8 +44,9 @@ class BECQueue(BECWidget, CompactPopupWidget):
|
||||
refresh_upon_start: bool = True,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(client=client, config=config, gui_id=gui_id, **kwargs)
|
||||
CompactPopupWidget.__init__(self, parent=parent, layout=QVBoxLayout)
|
||||
super().__init__(
|
||||
parent=parent, layout=QVBoxLayout, client=client, gui_id=gui_id, config=config, **kwargs
|
||||
)
|
||||
self.layout.setSpacing(0)
|
||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
|
||||
@@ -89,8 +89,7 @@ class BECStatusBox(BECWidget, CompactPopupWidget):
|
||||
gui_id: str = None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(client=client, gui_id=gui_id, **kwargs)
|
||||
CompactPopupWidget.__init__(self, parent=parent, layout=QHBoxLayout)
|
||||
super().__init__(parent=parent, layout=QHBoxLayout, client=client, gui_id=gui_id, **kwargs)
|
||||
|
||||
self.box_name = box_name
|
||||
self.status_container = defaultdict(lambda: {"info": None, "item": None, "widget": None})
|
||||
|
||||
@@ -25,8 +25,7 @@ class DeviceBrowser(BECWidget, QWidget):
|
||||
gui_id: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
super().__init__(client=client, config=config, gui_id=gui_id, **kwargs)
|
||||
QWidget.__init__(self, parent)
|
||||
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
|
||||
|
||||
self.get_bec_shortcuts()
|
||||
self.ui = None
|
||||
|
||||
@@ -104,7 +104,7 @@ class BecLogsQueue:
|
||||
if self._new_message_signal:
|
||||
self._new_message_signal.emit()
|
||||
except Exception:
|
||||
logger.warning("Error in LogPanel incoming message callback!")
|
||||
pass
|
||||
|
||||
def _set_formatter_and_update_filter(self, line_formatter: LineFormatter = noop_format):
|
||||
self._line_formatter: LineFormatter = line_formatter
|
||||
|
||||
@@ -30,10 +30,8 @@ class BECSpinBox(BECWidget, QDoubleSpinBox):
|
||||
) -> None:
|
||||
if config is None:
|
||||
config = ConnectionConfig(widget_class=self.__class__.__name__)
|
||||
super().__init__(client=client, gui_id=gui_id, config=config, **kwargs)
|
||||
QDoubleSpinBox.__init__(self, parent=parent)
|
||||
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
|
||||
|
||||
self.setObjectName("BECSpinBox")
|
||||
# Make the widget as compact as possible horizontally.
|
||||
self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
|
||||
self.setAlignment(Qt.AlignHCenter)
|
||||
|
||||
@@ -9,13 +9,11 @@ from bec_widgets.utils.bec_widget import BECWidget
|
||||
class BECColorMapWidget(BECWidget, QWidget):
|
||||
colormap_changed_signal = Signal(str)
|
||||
ICON_NAME = "palette"
|
||||
USER_ACCESS = ["colormap"]
|
||||
PLUGIN = True
|
||||
RPC = False
|
||||
|
||||
def __init__(self, parent=None, cmap: str = "magma", **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
QWidget.__init__(self, parent=parent)
|
||||
|
||||
super().__init__(parent=parent, **kwargs)
|
||||
# Create the ColorMapButton
|
||||
self.button = ColorMapButton()
|
||||
|
||||
|
||||
@@ -9,10 +9,10 @@ from bec_widgets.utils.colors import set_theme
|
||||
|
||||
|
||||
class DarkModeButton(BECWidget, QWidget):
|
||||
USER_ACCESS = ["toggle_dark_mode"]
|
||||
|
||||
ICON_NAME = "dark_mode"
|
||||
PLUGIN = True
|
||||
RPC = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -22,8 +22,7 @@ class DarkModeButton(BECWidget, QWidget):
|
||||
toolbar: bool = False,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
super().__init__(client=client, gui_id=gui_id, theme_update=True, **kwargs)
|
||||
QWidget.__init__(self, parent)
|
||||
super().__init__(parent=parent, client=client, gui_id=gui_id, theme_update=True, **kwargs)
|
||||
|
||||
self._dark_mode_enabled = False
|
||||
self.layout = QHBoxLayout(self)
|
||||
@@ -99,9 +98,6 @@ class DarkModeButton(BECWidget, QWidget):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from bec_widgets.utils.colors import set_theme
|
||||
|
||||
app = QApplication([])
|
||||
set_theme("auto")
|
||||
|
||||
@@ -144,7 +144,7 @@ def test_ring_bar(qtbot, connected_client_gui_obj):
|
||||
def test_rpc_gui_obj(connected_client_gui_obj, qtbot):
|
||||
gui = connected_client_gui_obj
|
||||
|
||||
assert len(gui.windows) == 1
|
||||
qtbot.waitUntil(lambda: len(gui.windows) == 1, timeout=3000)
|
||||
assert gui.windows["bec"] is gui.bec
|
||||
mw = gui.bec
|
||||
assert mw.__class__.__name__ == "RPCReference"
|
||||
@@ -155,22 +155,6 @@ def test_rpc_gui_obj(connected_client_gui_obj, qtbot):
|
||||
assert gui._ipython_registry[xw._gui_id].__class__.__name__ == "BECDockArea"
|
||||
assert len(gui.windows) == 2
|
||||
|
||||
gui_info = gui._dump()
|
||||
mw_info = gui_info[mw._gui_id]
|
||||
assert mw_info["title"] == "BEC"
|
||||
assert mw_info["visible"]
|
||||
xw_info = gui_info[xw._gui_id]
|
||||
assert xw_info["title"] == "BEC - X"
|
||||
assert xw_info["visible"]
|
||||
|
||||
gui.hide()
|
||||
gui_info = gui._dump() #
|
||||
assert not any(windows["visible"] for windows in gui_info.values())
|
||||
|
||||
gui.show()
|
||||
gui_info = gui._dump()
|
||||
assert all(windows["visible"] for windows in gui_info.values())
|
||||
|
||||
assert gui._gui_is_alive()
|
||||
gui.kill_server()
|
||||
assert not gui._gui_is_alive()
|
||||
@@ -186,10 +170,8 @@ def test_rpc_gui_obj(connected_client_gui_obj, qtbot):
|
||||
qtbot.waitUntil(wait_for_gui_started, timeout=3000)
|
||||
# gui.windows should have bec with gui_id 'bec'
|
||||
assert len(gui.windows) == 1
|
||||
assert gui.windows["bec"]._gui_id == mw._gui_id
|
||||
|
||||
# communication should work, main dock area should have same id and be visible
|
||||
gui_info = gui._dump()
|
||||
assert gui_info[mw._gui_id]["visible"]
|
||||
|
||||
yw = gui.new("Y")
|
||||
assert len(gui.windows) == 2
|
||||
|
||||
@@ -60,18 +60,18 @@ def test_rpc_plotting_shortcuts_init_configs(qtbot, connected_client_gui_obj):
|
||||
|
||||
# check if the correct devices are set
|
||||
# Curve
|
||||
assert c1._config["signal"] == {
|
||||
assert c1._config_dict["signal"] == {
|
||||
"dap": None,
|
||||
"name": "bpm4i",
|
||||
"entry": "bpm4i",
|
||||
"dap_oversample": 1,
|
||||
}
|
||||
assert c1._config["source"] == "device"
|
||||
assert c1._config["label"] == "bpm4i-bpm4i"
|
||||
assert c1._config_dict["source"] == "device"
|
||||
assert c1._config_dict["label"] == "bpm4i-bpm4i"
|
||||
|
||||
# Image Item
|
||||
assert im_item._config["monitor"] == "eiger"
|
||||
assert im_item._config["source"] == "auto"
|
||||
# # Image Item
|
||||
# assert im_item._config["monitor"] == "eiger"
|
||||
# assert im_item._config["source"] == "auto"
|
||||
|
||||
|
||||
def test_rpc_waveform_scan(qtbot, bec_client_lib, connected_client_gui_obj):
|
||||
|
||||
@@ -7,6 +7,7 @@ from qtpy.QtWidgets import QApplication
|
||||
from bec_widgets.cli.rpc.rpc_register import RPCRegister
|
||||
from bec_widgets.utils import bec_dispatcher as bec_dispatcher_module
|
||||
from bec_widgets.utils import error_popups
|
||||
from bec_widgets.utils.bec_qapp import BECApplication
|
||||
|
||||
|
||||
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
|
||||
@@ -28,8 +29,10 @@ def qapplication(qtbot, request, testable_qtimer_class): # pylint: disable=unus
|
||||
print("Test failed, skipping cleanup checks")
|
||||
return
|
||||
|
||||
testable_qtimer_class.check_all_stopped(qtbot)
|
||||
# qapp = BECApplication()
|
||||
# qapp.shutdown()
|
||||
|
||||
testable_qtimer_class.check_all_stopped(qtbot)
|
||||
qapp = QApplication.instance()
|
||||
qapp.processEvents()
|
||||
if hasattr(qapp, "os_listener") and qapp.os_listener:
|
||||
|
||||
@@ -31,7 +31,7 @@ def test_rpc_call_new_dock(cli_dock_area):
|
||||
)
|
||||
def test_client_utils_start_plot_process(config, call_config):
|
||||
with mock.patch("bec_widgets.cli.client_utils.subprocess.Popen") as mock_popen:
|
||||
_start_plot_process("gui_id", BECDockArea, "bec", config)
|
||||
_start_plot_process("gui_id", "bec", config, gui_class="BECDockArea")
|
||||
command = [
|
||||
"bec-gui-server",
|
||||
"--id",
|
||||
@@ -82,7 +82,6 @@ def test_client_utils_passes_client_config_to_server(bec_dispatcher):
|
||||
) # the started event will not be set, wait=True would block forever
|
||||
mock_start_plot.assert_called_once_with(
|
||||
"gui_id",
|
||||
BECGuiClient,
|
||||
gui_class_id="bec",
|
||||
config=mixin._client._service_config.config,
|
||||
logger=mock.ANY,
|
||||
|
||||
@@ -130,11 +130,11 @@ class ExamplePlotWidget(BECWidget, QWidget):
|
||||
config: ConnectionConfig | None = None,
|
||||
client=None,
|
||||
gui_id: str | None = None,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
if config is None:
|
||||
config = ConnectionConfig(widget_class=self.__class__.__name__)
|
||||
super().__init__(client=client, gui_id=gui_id, config=config)
|
||||
QWidget.__init__(self, parent=parent)
|
||||
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
|
||||
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.glw = pg.GraphicsLayoutWidget()
|
||||
|
||||
@@ -17,9 +17,8 @@ from .conftest import create_widget
|
||||
class DeviceInputWidget(DeviceInputBase, QWidget):
|
||||
"""Thin wrapper around DeviceInputBase to make it a QWidget"""
|
||||
|
||||
def __init__(self, parent=None, client=None, config=None, gui_id=None):
|
||||
super().__init__(client=client, config=config, gui_id=gui_id)
|
||||
QWidget.__init__(self, parent=parent)
|
||||
def __init__(self, parent=None, client=None, config=None, gui_id=None, **kwargs):
|
||||
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -22,8 +22,7 @@ class DeviceInputWidget(DeviceSignalInputBase, QWidget):
|
||||
"""Thin wrapper around DeviceInputBase to make it a QWidget"""
|
||||
|
||||
def __init__(self, parent=None, client=None, config=None, gui_id=None):
|
||||
super().__init__(client=client, config=config, gui_id=gui_id)
|
||||
QWidget.__init__(self, parent=parent)
|
||||
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -5,7 +5,7 @@ from bec_widgets.cli.rpc.rpc_base import DeletedWidgetError, RPCBase, RPCReferen
|
||||
|
||||
@pytest.fixture
|
||||
def rpc_base():
|
||||
yield RPCBase(gui_id="rpc_base_test", name="test")
|
||||
yield RPCBase(gui_id="rpc_base_test", object_name="test")
|
||||
|
||||
|
||||
def test_rpc_base(rpc_base):
|
||||
|
||||
@@ -1,45 +1,56 @@
|
||||
import argparse
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from bec_lib.service_config import ServiceConfig
|
||||
|
||||
from bec_widgets.cli.server import _start_server
|
||||
from bec_widgets.widgets.containers.dock import BECDockArea
|
||||
from bec_widgets.cli.server import GUIServer
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mocked_cli_server():
|
||||
with mock.patch("bec_widgets.cli.server.BECWidgetsCLIServer") as mock_server:
|
||||
with mock.patch("bec_widgets.cli.server.ServiceConfig") as mock_config:
|
||||
with mock.patch("bec_lib.logger.bec_logger.configure") as mock_logger:
|
||||
yield mock_server, mock_config, mock_logger
|
||||
def gui_server():
|
||||
args = argparse.Namespace(
|
||||
config=None, id="gui_id", gui_class="LaunchWindow", gui_class_id="bec", hide=False
|
||||
)
|
||||
return GUIServer(args=args)
|
||||
|
||||
|
||||
def test_rpc_server_start_server_without_service_config(mocked_cli_server):
|
||||
def test_gui_server_start_server_without_service_config(gui_server):
|
||||
"""
|
||||
Test that the server is started with the correct arguments.
|
||||
"""
|
||||
mock_server, mock_config, _ = mocked_cli_server
|
||||
assert gui_server.config is None
|
||||
assert gui_server.gui_id == "gui_id"
|
||||
assert gui_server.gui_class == "LaunchWindow"
|
||||
assert gui_server.gui_class_id == "bec"
|
||||
assert gui_server.hide is False
|
||||
|
||||
_start_server("gui_id", BECDockArea, config=None)
|
||||
mock_server.assert_called_once_with(
|
||||
gui_id="gui_id", config=mock_config(), gui_class=BECDockArea, gui_class_id="bec"
|
||||
)
|
||||
|
||||
def test_gui_server_get_service_config(gui_server):
|
||||
"""
|
||||
Test that the server is started with the correct arguments.
|
||||
"""
|
||||
assert gui_server._get_service_config().config is ServiceConfig().config
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"config, call_config",
|
||||
"connections, hide",
|
||||
[
|
||||
("/path/to/config.yml", {"config_path": "/path/to/config.yml"}),
|
||||
({"key": "value"}, {"config": {"key": "value"}}),
|
||||
({}, False),
|
||||
({"launcher": mock.MagicMock()}, False),
|
||||
({"launcher": mock.MagicMock(), "dock_area": mock.MagicMock()}, True),
|
||||
],
|
||||
)
|
||||
def test_rpc_server_start_server_with_service_config(mocked_cli_server, config, call_config):
|
||||
"""
|
||||
Test that the server is started with the correct arguments.
|
||||
"""
|
||||
mock_server, mock_config, _ = mocked_cli_server
|
||||
config = mock_config(**call_config)
|
||||
_start_server("gui_id", BECDockArea, config=config)
|
||||
mock_server.assert_called_once_with(
|
||||
gui_id="gui_id", config=config, gui_class=BECDockArea, gui_class_id="bec"
|
||||
)
|
||||
def test_gui_server_turns_off_the_lights(gui_server, connections, hide):
|
||||
with mock.patch.object(gui_server, "launcher_window") as mock_launcher_window:
|
||||
with mock.patch.object(gui_server, "app") as mock_app:
|
||||
gui_server._turn_off_the_lights(connections)
|
||||
|
||||
if not hide:
|
||||
mock_launcher_window.show.assert_called_once()
|
||||
mock_launcher_window.activateWindow.assert_called_once()
|
||||
mock_launcher_window.raise_.assert_called_once()
|
||||
mock_app.setQuitOnLastWindowClosed.assert_called_once_with(True)
|
||||
else:
|
||||
mock_launcher_window.hide.assert_called_once()
|
||||
mock_app.setQuitOnLastWindowClosed.assert_called_once_with(False)
|
||||
|
||||
Reference in New Issue
Block a user