feat(xray_eye): add XRayEye widget and plugin for GUI integration
This commit is contained in:
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
|
||||
from bec_widgets.cli.rpc.rpc_base import RPCBase, rpc_call
|
||||
from bec_widgets.cli.rpc.rpc_base import RPCBase, rpc_call, rpc_timeout
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
@@ -14,6 +14,7 @@ logger = bec_logger.logger
|
||||
|
||||
_Widgets = {
|
||||
"OmnyAlignment": "OmnyAlignment",
|
||||
"XRayEye": "XRayEye",
|
||||
}
|
||||
|
||||
|
||||
@@ -73,3 +74,67 @@ class OmnyAlignment(RPCBase):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
|
||||
class XRayEye(RPCBase):
|
||||
@rpc_call
|
||||
def active_roi(self) -> "BaseROI | None":
|
||||
"""
|
||||
Return the currently active ROI, or None if no ROI is active.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def enable_live_view(self):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
@enable_live_view.setter
|
||||
@rpc_call
|
||||
def enable_live_view(self):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def user_message(self):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
@user_message.setter
|
||||
@rpc_call
|
||||
def user_message(self):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def sample_name(self):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
@sample_name.setter
|
||||
@rpc_call
|
||||
def sample_name(self):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def enable_move_buttons(self):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
@enable_move_buttons.setter
|
||||
@rpc_call
|
||||
def enable_move_buttons(self):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
0
csaxs_bec/bec_widgets/widgets/xray_eye/__init__.py
Normal file
0
csaxs_bec/bec_widgets/widgets/xray_eye/__init__.py
Normal file
15
csaxs_bec/bec_widgets/widgets/xray_eye/register_x_ray_eye.py
Normal file
15
csaxs_bec/bec_widgets/widgets/xray_eye/register_x_ray_eye.py
Normal file
@@ -0,0 +1,15 @@
|
||||
def main(): # pragma: no cover
|
||||
from qtpy import PYSIDE6
|
||||
|
||||
if not PYSIDE6:
|
||||
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
|
||||
return
|
||||
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
||||
|
||||
from csaxs_bec.bec_widgets.widgets.xray_eye.x_ray_eye_plugin import XRayEyePlugin
|
||||
|
||||
QPyDesignerCustomWidgetCollection.addCustomWidget(XRayEyePlugin())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
218
csaxs_bec/bec_widgets/widgets/xray_eye/x_ray_eye.py
Normal file
218
csaxs_bec/bec_widgets/widgets/xray_eye/x_ray_eye.py
Normal file
@@ -0,0 +1,218 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtWidgets import QDoubleSpinBox
|
||||
from qtpy.QtWidgets import QPushButton
|
||||
from qtpy.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QLabel, QLineEdit, QSizePolicy, QFrame
|
||||
|
||||
from bec_lib import bec_logger
|
||||
from bec_widgets import BECWidget, SafeSlot, SafeProperty
|
||||
from bec_widgets.widgets.control.device_control.positioner_box import PositionerBox2D
|
||||
from bec_widgets.widgets.plots.image.image import Image
|
||||
from bec_widgets.widgets.plots.image.setting_widgets.image_roi_tree import ROIPropertyTree
|
||||
from bec_widgets.widgets.plots.roi.image_roi import BaseROI
|
||||
from bec_widgets.widgets.utility.toggle.toggle import ToggleSwitch
|
||||
|
||||
logger = bec_logger.logger
|
||||
#TODO replace with actual device names, these are just placeholders from simulation framework
|
||||
DEVICE_HORIZONTAL = "samx"
|
||||
DEVICE_VERTICAL = "samy"
|
||||
CAMERA = "eiger"
|
||||
|
||||
class XRayEye(BECWidget, QWidget):
|
||||
USER_ACCESS = ["active_roi","enable_live_view", "enable_live_view.setter", "user_message", "user_message.setter","sample_name", "sample_name.setter", "enable_move_buttons", "enable_move_buttons.setter"]
|
||||
PLUGIN = True
|
||||
|
||||
def __init__(self, parent=None, **kwargs):
|
||||
super().__init__(parent=parent, **kwargs)
|
||||
|
||||
self._init_ui()
|
||||
self._make_connections()
|
||||
|
||||
def _init_ui(self):
|
||||
self.core_layout = QHBoxLayout(self)
|
||||
|
||||
self.image = Image(parent=self)
|
||||
self.image.enable_toolbar = False # Disable default toolbar to not allow to user set anything
|
||||
self.image.inner_axes = False # Disable inner axes to maximize image area
|
||||
|
||||
# Control panel on the right: vertical layout inside a fixed-width widget
|
||||
self.control_panel = QWidget(parent=self)
|
||||
self.control_panel_layout = QVBoxLayout(self.control_panel)
|
||||
self.control_panel_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.control_panel_layout.setSpacing(10)
|
||||
|
||||
# ROI toolbar + Live toggle (header row)
|
||||
self.roi_manager = ROIPropertyTree(parent=self, image_widget=self.image, compact=True, compact_orientation="horizontal")
|
||||
header_row = QHBoxLayout()
|
||||
header_row.setContentsMargins(0, 0, 0, 0)
|
||||
header_row.setSpacing(8)
|
||||
header_row.addWidget(self.roi_manager, 0)
|
||||
header_row.addStretch()
|
||||
self.live_preview_label = QLabel("Live Preview", parent=self)
|
||||
self.live_preview_toggle = ToggleSwitch(parent=self)
|
||||
header_row.addWidget(self.live_preview_label, 0, Qt.AlignVCenter)
|
||||
header_row.addWidget(self.live_preview_toggle, 0, Qt.AlignVCenter)
|
||||
self.control_panel_layout.addLayout(header_row)
|
||||
|
||||
# separator
|
||||
self.control_panel_layout.addWidget(self._create_separator())
|
||||
|
||||
# 2D Positioner (fixed size)
|
||||
self.motor_control_2d = PositionerBox2D(parent=self, device_hor=DEVICE_HORIZONTAL, device_ver=DEVICE_VERTICAL)
|
||||
self.motor_control_2d.hide_device_boxes = True
|
||||
self.motor_control_2d.adjustSize()
|
||||
m_hint = self.motor_control_2d.sizeHint()
|
||||
self.motor_control_2d.setFixedWidth(m_hint.width())
|
||||
self.motor_control_2d.setMaximumHeight(m_hint.height())
|
||||
self.motor_control_2d.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
||||
self.control_panel_layout.addWidget(self.motor_control_2d, 0, Qt.AlignTop | Qt.AlignRight)
|
||||
|
||||
# separator
|
||||
self.control_panel_layout.addWidget(self._create_separator())
|
||||
|
||||
# Step size label
|
||||
step_size_form = QGridLayout()
|
||||
#Horizontal
|
||||
self.step_size_horizontal = QDoubleSpinBox(parent=self)
|
||||
self.step_size_horizontal.setDecimals(4)
|
||||
self.step_size_horizontal.setRange(0.0001, 100000)
|
||||
self.step_size_horizontal.setSingleStep(0.1)
|
||||
self.step_size_horizontal.setValue(1.0)
|
||||
#Vertical
|
||||
self.step_size_vertical = QDoubleSpinBox(parent=self)
|
||||
self.step_size_vertical.setDecimals(4)
|
||||
self.step_size_vertical.setRange(0.0001, 100000)
|
||||
self.step_size_vertical.setSingleStep(0.1)
|
||||
self.step_size_vertical.setValue(1.0)
|
||||
# Submit button
|
||||
self.submit_button = QPushButton("Get ROI Coordinates", parent=self)
|
||||
self.submit_button.setStyleSheet("""background-color: #4CAF50; color: white; padding: 5px 10px; border: none; border-radius: 4px;""")
|
||||
# Add to layout form
|
||||
step_size_form.addWidget(QLabel("Horizontal", parent=self),0,0)
|
||||
step_size_form.addWidget(self.step_size_horizontal,0,1)
|
||||
step_size_form.addWidget(QLabel("Vertical", parent=self),1,0)
|
||||
step_size_form.addWidget(self.step_size_vertical,1,1)
|
||||
step_size_form.addWidget(self.submit_button,2,0,1,2)
|
||||
|
||||
# Add form to control panel
|
||||
self.control_panel_layout.addLayout(step_size_form)
|
||||
|
||||
# Push form to bottom
|
||||
self.control_panel_layout.addStretch()
|
||||
|
||||
# Sample/Message form (bottom)
|
||||
form = QGridLayout()
|
||||
self.sample_name_line_edit = QLineEdit(parent=self)
|
||||
form.addWidget(QLabel("Sample", parent=self),0,0)
|
||||
form.addWidget(self.sample_name_line_edit,0,1)
|
||||
self.message_line_edit = QLineEdit(parent=self)
|
||||
form.addWidget(QLabel("Message", parent=self),1,0)
|
||||
form.addWidget(self.message_line_edit,1,1)
|
||||
self.control_panel_layout.addLayout(form)
|
||||
|
||||
# Fix panel width and allow vertical expansion
|
||||
self.control_panel.adjustSize()
|
||||
p_hint = self.control_panel.sizeHint()
|
||||
self.control_panel.setFixedWidth(p_hint.width())
|
||||
self.control_panel.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)
|
||||
|
||||
# Core Layout: image (expanding) | control panel (fixed)
|
||||
self.core_layout.addWidget(self.image)
|
||||
self.core_layout.addWidget(self.control_panel)
|
||||
|
||||
def _make_connections(self):
|
||||
# Fetch initial state
|
||||
self.on_live_view_enabled(True)
|
||||
self.step_size_horizontal.setValue(self.motor_control_2d.step_size_hor)
|
||||
self.step_size_vertical.setValue(self.motor_control_2d.step_size_ver)
|
||||
|
||||
# Make connections
|
||||
self.live_preview_toggle.enabled.connect(self.on_live_view_enabled)
|
||||
self.step_size_horizontal.valueChanged.connect(lambda x: self.motor_control_2d.setProperty("step_size_hor",x))
|
||||
self.step_size_vertical.valueChanged.connect(lambda x: self.motor_control_2d.setProperty("step_size_ver",x))
|
||||
self.submit_button.clicked.connect(self.get_roi_coordinates)
|
||||
|
||||
def _create_separator(self):
|
||||
sep = QFrame(parent=self)
|
||||
sep.setFrameShape(QFrame.HLine)
|
||||
sep.setFrameShadow(QFrame.Sunken)
|
||||
sep.setLineWidth(1)
|
||||
return sep
|
||||
|
||||
################################################################################
|
||||
# Properties ported from the original OmnyAlignment, can be adjusted as needed
|
||||
################################################################################
|
||||
@SafeProperty(bool)
|
||||
def enable_live_view(self):
|
||||
return self.live_preview_toggle.checked
|
||||
|
||||
@enable_live_view.setter
|
||||
def enable_live_view(self, enable: bool):
|
||||
self.live_preview_toggle.checked = enable
|
||||
|
||||
@SafeProperty(str)
|
||||
def user_message(self):
|
||||
return self.message_line_edit.text()
|
||||
|
||||
@user_message.setter
|
||||
def user_message(self, message: str):
|
||||
self.message_line_edit.setText(message)
|
||||
|
||||
@SafeProperty(str)
|
||||
def sample_name(self):
|
||||
return self.sample_name_line_edit.text()
|
||||
|
||||
@sample_name.setter
|
||||
def sample_name(self, message: str):
|
||||
self.sample_name_line_edit.setText(message)
|
||||
|
||||
@SafeProperty(bool)
|
||||
def enable_move_buttons(self):
|
||||
return self.motor_control_2d.isEnabled()
|
||||
|
||||
@enable_move_buttons.setter
|
||||
def enable_move_buttons(self, enabled: bool):
|
||||
self.motor_control_2d.setEnabled(enabled)
|
||||
|
||||
def active_roi(self)->BaseROI|None:
|
||||
"""Return the currently active ROI, or None if no ROI is active."""
|
||||
return self.roi_manager.single_active_roi
|
||||
|
||||
|
||||
################################################################################
|
||||
# Slots ported from the original OmnyAlignment, can be adjusted as needed
|
||||
################################################################################
|
||||
|
||||
@SafeSlot()
|
||||
def get_roi_coordinates(self) -> dict | None:
|
||||
"""Get the coordinates of the currently active ROI."""
|
||||
roi = self.roi_manager.single_active_roi
|
||||
if roi is None:
|
||||
logger.warning("No active ROI")
|
||||
return None
|
||||
logger.info(f"Active ROI coordinates: {roi.get_coordinates()}")
|
||||
return roi.get_coordinates()
|
||||
|
||||
|
||||
@SafeSlot(bool)
|
||||
def on_live_view_enabled(self, enabled: bool):
|
||||
logger.info(f"Live view is enabled: {enabled}")
|
||||
if enabled:
|
||||
self.image.image(CAMERA)
|
||||
return
|
||||
|
||||
self.image.disconnect_monitor(CAMERA)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
import sys
|
||||
from qtpy.QtWidgets import QApplication
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
win = XRayEye()
|
||||
|
||||
win.resize(1000, 800)
|
||||
win.show()
|
||||
sys.exit(app.exec_())
|
||||
@@ -0,0 +1 @@
|
||||
{'files': ['x_ray_eye.py']}
|
||||
57
csaxs_bec/bec_widgets/widgets/xray_eye/x_ray_eye_plugin.py
Normal file
57
csaxs_bec/bec_widgets/widgets/xray_eye/x_ray_eye_plugin.py
Normal file
@@ -0,0 +1,57 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from csaxs_bec.bec_widgets.widgets.xray_eye.x_ray_eye import XRayEye
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
<widget class='XRayEye' name='x_ray_eye'>
|
||||
</widget>
|
||||
</ui>
|
||||
"""
|
||||
|
||||
|
||||
class XRayEyePlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
if parent is None:
|
||||
return QWidget()
|
||||
t = XRayEye(parent)
|
||||
return t
|
||||
|
||||
def domXml(self):
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return ""
|
||||
|
||||
def icon(self):
|
||||
return designer_material_icon(XRayEye.ICON_NAME)
|
||||
|
||||
def includeFile(self):
|
||||
return "x_ray_eye"
|
||||
|
||||
def initialize(self, form_editor):
|
||||
self._form_editor = form_editor
|
||||
|
||||
def isContainer(self):
|
||||
return False
|
||||
|
||||
def isInitialized(self):
|
||||
return self._form_editor is not None
|
||||
|
||||
def name(self):
|
||||
return "XRayEye"
|
||||
|
||||
def toolTip(self):
|
||||
return "XRayEye"
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
Reference in New Issue
Block a user