0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-14 03:31:50 +02:00

feat(widget): add 2d positioner box widget

This commit is contained in:
2025-01-10 10:32:45 +01:00
parent 3770db51be
commit d2ffddb6d8
15 changed files with 1215 additions and 76 deletions

View File

@ -34,6 +34,7 @@ class Widgets(str, enum.Enum):
Minesweeper = "Minesweeper" Minesweeper = "Minesweeper"
PositionIndicator = "PositionIndicator" PositionIndicator = "PositionIndicator"
PositionerBox = "PositionerBox" PositionerBox = "PositionerBox"
PositionerBox2D = "PositionerBox2D"
PositionerControlLine = "PositionerControlLine" PositionerControlLine = "PositionerControlLine"
ResetButton = "ResetButton" ResetButton = "ResetButton"
ResumeButton = "ResumeButton" ResumeButton = "ResumeButton"
@ -3235,6 +3236,26 @@ class PositionerBox(RPCBase):
""" """
class PositionerBox2D(RPCBase):
@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): class PositionerBoxBase(RPCBase):
@property @property
@rpc_call @rpc_call

View File

@ -1,8 +1,11 @@
from bec_widgets.widgets.control.device_control.positioner_box.positioner_box.positioner_box import ( from bec_widgets.widgets.control.device_control.positioner_box.positioner_box.positioner_box import (
PositionerBox, PositionerBox,
) )
from bec_widgets.widgets.control.device_control.positioner_box.positioner_box_2d.positioner_box_2d import (
PositionerBox2D,
)
from bec_widgets.widgets.control.device_control.positioner_box.positioner_control_line.positioner_control_line import ( from bec_widgets.widgets.control.device_control.positioner_box.positioner_control_line.positioner_control_line import (
PositionerControlLine, PositionerControlLine,
) )
__ALL__ = ["PositionerBox", "PositionerControlLine"] __ALL__ = ["PositionerBox", "PositionerControlLine", "PositionerBox2D"]

View File

@ -1,19 +1,31 @@
from ast import Tuple
import uuid import uuid
from abc import abstractmethod from abc import abstractmethod
from ast import Tuple
from typing import Callable, TypedDict from typing import Callable, TypedDict
from bec_lib.device import Positioner from bec_lib.device import Positioner
from bec_lib.endpoints import MessageEndpoints from bec_lib.endpoints import MessageEndpoints
from bec_lib.logger import bec_logger from bec_lib.logger import bec_logger
from bec_lib.messages import ScanQueueMessage from bec_lib.messages import ScanQueueMessage
from qtpy.QtWidgets import QGroupBox, QDoubleSpinBox, QPushButton, QVBoxLayout, QLabel, QLineEdit from qtpy.QtWidgets import (
QDialog,
QDoubleSpinBox,
QGroupBox,
QLabel,
QLineEdit,
QPushButton,
QVBoxLayout,
)
from bec_widgets.qt_utils.compact_popup import CompactPopupWidget from bec_widgets.qt_utils.compact_popup import CompactPopupWidget
from bec_widgets.utils.bec_widget import BECWidget from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.widgets.control.device_control.position_indicator.position_indicator import ( from bec_widgets.widgets.control.device_control.position_indicator.position_indicator import (
PositionIndicator, PositionIndicator,
) )
from bec_widgets.widgets.control.device_input.base_classes.device_input_base import BECDeviceFilter
from bec_widgets.widgets.control.device_input.device_line_edit.device_line_edit import (
DeviceLineEdit,
)
from bec_widgets.widgets.utility.spinner.spinner import SpinnerWidget from bec_widgets.widgets.utility.spinner.spinner import SpinnerWidget
logger = bec_logger.logger logger = bec_logger.logger
@ -26,6 +38,9 @@ class DeviceUpdateUIComponents(TypedDict):
position_indicator: PositionIndicator position_indicator: PositionIndicator
step_size: QDoubleSpinBox step_size: QDoubleSpinBox
device_box: QGroupBox device_box: QGroupBox
stop: QPushButton
tweak_increase: QPushButton
tweak_decrease: QPushButton
class PositionerBoxBase(BECWidget, CompactPopupWidget): class PositionerBoxBase(BECWidget, CompactPopupWidget):
@ -43,6 +58,7 @@ class PositionerBoxBase(BECWidget, CompactPopupWidget):
""" """
super().__init__(**kwargs) super().__init__(**kwargs)
CompactPopupWidget.__init__(self, parent=parent, layout=QVBoxLayout) CompactPopupWidget.__init__(self, parent=parent, layout=QVBoxLayout)
self._dialog = None
self.get_bec_shortcuts() self.get_bec_shortcuts()
def _check_device_is_valid(self, device: str): def _check_device_is_valid(self, device: str):
@ -178,3 +194,50 @@ class PositionerBoxBase(BECWidget, CompactPopupWidget):
def _swap_readback_signal_connection(self, slot, old_device, new_device): def _swap_readback_signal_connection(self, slot, old_device, new_device):
self.bec_dispatcher.disconnect_slot(slot, MessageEndpoints.device_readback(old_device)) self.bec_dispatcher.disconnect_slot(slot, MessageEndpoints.device_readback(old_device))
self.bec_dispatcher.connect_slot(slot, MessageEndpoints.device_readback(new_device)) self.bec_dispatcher.connect_slot(slot, MessageEndpoints.device_readback(new_device))
def _toggle_enable_buttons(self, ui: DeviceUpdateUIComponents, enable: bool) -> None:
"""Toogle enable/disable on available buttons
Args:
enable (bool): Enable buttons
"""
ui["tweak_increase"].setEnabled(enable)
ui["tweak_decrease"].setEnabled(enable)
ui["stop"].setEnabled(enable)
ui["setpoint"].setEnabled(enable)
ui["step_size"].setEnabled(enable)
def _on_device_change(
self,
old_device: str,
new_device: str,
position_emit: Callable[[float], None],
limit_update: Callable[[tuple[float, float]], None],
on_device_readback: Callable,
ui: DeviceUpdateUIComponents,
):
logger.info(f"Device changed from {old_device} to {new_device}")
self._toggle_enable_buttons(ui, True)
self._init_device(new_device, position_emit, limit_update)
self._swap_readback_signal_connection(on_device_readback, old_device, new_device)
self._update_device_ui(new_device, ui)
def _open_dialog_selection(self, set_positioner: Callable):
def _ods():
"""Open dialog window for positioner selection"""
self._dialog = QDialog(self)
self._dialog.setWindowTitle("Positioner Selection")
layout = QVBoxLayout()
line_edit = DeviceLineEdit(
self, client=self.client, device_filter=[BECDeviceFilter.POSITIONER]
)
line_edit.textChanged.connect(set_positioner)
layout.addWidget(line_edit)
close_button = QPushButton("Close")
close_button.clicked.connect(self._dialog.accept)
layout.addWidget(close_button)
self._dialog.setLayout(layout)
self._dialog.exec()
self._dialog = None
return _ods

View File

@ -5,24 +5,19 @@ from __future__ import annotations
import os import os
from bec_lib.device import Positioner from bec_lib.device import Positioner
from bec_lib.endpoints import MessageEndpoints
from bec_lib.logger import bec_logger from bec_lib.logger import bec_logger
from bec_qthemes import material_icon from bec_qthemes import material_icon
from qtpy.QtCore import Property, Signal, Slot from qtpy.QtCore import Signal
from qtpy.QtGui import QDoubleValidator from qtpy.QtGui import QDoubleValidator
from qtpy.QtWidgets import QDialog, QDoubleSpinBox, QPushButton, QVBoxLayout from qtpy.QtWidgets import QDoubleSpinBox
from bec_widgets.widgets.control.device_control.positioner_box._base import PositionerBoxBase from bec_widgets.qt_utils.error_popups import SafeProperty, SafeSlot
from bec_widgets.utils import UILoader from bec_widgets.utils import UILoader
from bec_widgets.utils.colors import get_accent_colors, set_theme from bec_widgets.utils.colors import get_accent_colors, set_theme
from bec_widgets.widgets.control.device_control.positioner_box._base import PositionerBoxBase
from bec_widgets.widgets.control.device_control.positioner_box._base.positioner_box_base import ( from bec_widgets.widgets.control.device_control.positioner_box._base.positioner_box_base import (
DeviceUpdateUIComponents, DeviceUpdateUIComponents,
) )
from bec_widgets.widgets.control.device_input.base_classes.device_input_base import BECDeviceFilter
from bec_widgets.widgets.control.device_input.device_line_edit.device_line_edit import (
DeviceLineEdit,
)
logger = bec_logger.logger logger = bec_logger.logger
@ -53,7 +48,6 @@ class PositionerBox(PositionerBoxBase):
self._device = "" self._device = ""
self._limits = None self._limits = None
self._dialog = None
if self.current_path == "": if self.current_path == "":
self.current_path = os.path.dirname(__file__) self.current_path = os.path.dirname(__file__)
@ -91,40 +85,14 @@ class PositionerBox(PositionerBoxBase):
self.setpoint_validator = QDoubleValidator() self.setpoint_validator = QDoubleValidator()
self.ui.setpoint.setValidator(self.setpoint_validator) self.ui.setpoint.setValidator(self.setpoint_validator)
self.ui.spinner_widget.start() self.ui.spinner_widget.start()
self.ui.tool_button.clicked.connect(self._open_dialog_selection) self.ui.tool_button.clicked.connect(self._open_dialog_selection(self.set_positioner))
icon = material_icon(icon_name="edit_note", size=(16, 16), convert_to_pixmap=False) icon = material_icon(icon_name="edit_note", size=(16, 16), convert_to_pixmap=False)
self.ui.tool_button.setIcon(icon) self.ui.tool_button.setIcon(icon)
def _open_dialog_selection(self): def force_update_readback(self):
"""Open dialog window for positioner selection""" self._init_device(self.device, self.position_update.emit, self.update_limits)
self._dialog = QDialog(self)
self._dialog.setWindowTitle("Positioner Selection")
layout = QVBoxLayout()
line_edit = DeviceLineEdit(
self, client=self.client, device_filter=[BECDeviceFilter.POSITIONER]
)
line_edit.textChanged.connect(self.set_positioner)
layout.addWidget(line_edit)
close_button = QPushButton("Close")
close_button.clicked.connect(self._dialog.accept)
layout.addWidget(close_button)
self._dialog.setLayout(layout)
self._dialog.exec()
self._dialog = None
def _toogle_enable_buttons(self, enable: bool) -> None: @SafeProperty(str)
"""Toogle enable/disable on available buttons
Args:
enable (bool): Enable buttons
"""
self.ui.tweak_left.setEnabled(enable)
self.ui.tweak_right.setEnabled(enable)
self.ui.stop.setEnabled(enable)
self.ui.setpoint.setEnabled(enable)
self.ui.step_size.setEnabled(enable)
@Property(str)
def device(self): def device(self):
"""Property to set the device""" """Property to set the device"""
return self._device return self._device
@ -142,7 +110,7 @@ class PositionerBox(PositionerBoxBase):
self.label = value self.label = value
self.device_changed.emit(old_device, value) self.device_changed.emit(old_device, value)
@Property(bool) @SafeProperty(bool)
def hide_device_selection(self): def hide_device_selection(self):
"""Hide the device selection""" """Hide the device selection"""
return not self.ui.tool_button.isVisible() return not self.ui.tool_button.isVisible()
@ -152,7 +120,7 @@ class PositionerBox(PositionerBoxBase):
"""Set the device selection visibility""" """Set the device selection visibility"""
self.ui.tool_button.setVisible(not value) self.ui.tool_button.setVisible(not value)
@Slot(bool) @SafeSlot(bool)
def show_device_selection(self, value: bool): def show_device_selection(self, value: bool):
"""Show the device selection """Show the device selection
@ -161,7 +129,7 @@ class PositionerBox(PositionerBoxBase):
""" """
self.hide_device_selection = not value self.hide_device_selection = not value
@Slot(str) @SafeSlot(str)
def set_positioner(self, positioner: str | Positioner): def set_positioner(self, positioner: str | Positioner):
"""Set the device """Set the device
@ -172,7 +140,7 @@ class PositionerBox(PositionerBoxBase):
positioner = positioner.name positioner = positioner.name
self.device = positioner self.device = positioner
@Slot(str, str) @SafeSlot(str, str)
def on_device_change(self, old_device: str, new_device: str): def on_device_change(self, old_device: str, new_device: str):
"""Upon changing the device, a check will be performed if the device is a Positioner. """Upon changing the device, a check will be performed if the device is a Positioner.
@ -182,11 +150,14 @@ class PositionerBox(PositionerBoxBase):
""" """
if not self._check_device_is_valid(new_device): if not self._check_device_is_valid(new_device):
return return
logger.info(f"Device changed from {old_device} to {new_device}") self._on_device_change(
self._toogle_enable_buttons(True) old_device,
self._init_device(new_device, self.position_update.emit, self.update_limits) new_device,
self._swap_readback_signal_connection(self.on_device_readback, old_device, new_device) self.position_update.emit,
self._update_device_ui(new_device, self._device_ui_components(new_device)) self.update_limits,
self.on_device_readback,
self._device_ui_components(new_device),
)
def _device_ui_components(self, device: str) -> DeviceUpdateUIComponents: def _device_ui_components(self, device: str) -> DeviceUpdateUIComponents:
return { return {
@ -196,9 +167,12 @@ class PositionerBox(PositionerBoxBase):
"setpoint": self.ui.setpoint, "setpoint": self.ui.setpoint,
"step_size": self.ui.step_size, "step_size": self.ui.step_size,
"device_box": self.ui.device_box, "device_box": self.ui.device_box,
"stop": self.ui.stop,
"tweak_increase": self.ui.tweak_right,
"tweak_decrease": self.ui.tweak_left,
} }
@Slot(dict, dict) @SafeSlot(dict, dict)
def on_device_readback(self, msg_content: dict, metadata: dict): def on_device_readback(self, msg_content: dict, metadata: dict):
"""Callback for device readback. """Callback for device readback.
@ -226,7 +200,7 @@ class PositionerBox(PositionerBoxBase):
self._limits = limits self._limits = limits
self._update_limits_ui(limits, self.ui.position_indicator, self.setpoint_validator) self._update_limits_ui(limits, self.ui.position_indicator, self.setpoint_validator)
@Slot() @SafeSlot()
def on_stop(self): def on_stop(self):
self._stop_device(self.device) self._stop_device(self.device)
@ -235,17 +209,17 @@ class PositionerBox(PositionerBoxBase):
"""Step size for tweak""" """Step size for tweak"""
return self.ui.step_size.value() return self.ui.step_size.value()
@Slot() @SafeSlot()
def on_tweak_right(self): def on_tweak_right(self):
"""Tweak motor right""" """Tweak motor right"""
self.dev[self.device].move(self.step_size, relative=True) self.dev[self.device].move(self.step_size, relative=True)
@Slot() @SafeSlot()
def on_tweak_left(self): def on_tweak_left(self):
"""Tweak motor left""" """Tweak motor left"""
self.dev[self.device].move(-self.step_size, relative=True) self.dev[self.device].move(-self.step_size, relative=True)
@Slot() @SafeSlot()
def on_setpoint_change(self): def on_setpoint_change(self):
"""Change the setpoint for the motor""" """Change the setpoint for the motor"""
self.ui.setpoint.clearFocus() self.ui.setpoint.clearFocus()

View File

@ -6,7 +6,7 @@ def main(): # pragma: no cover
return return
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
from bec_widgets.widgets.control.device_control.positioner_box.positioner_box_plugin import ( from bec_widgets.widgets.control.device_control.positioner_box.positioner_box.positioner_box_plugin import (
PositionerBoxPlugin, PositionerBoxPlugin,
) )

View File

@ -0,0 +1 @@
{'files': ['positioner_box_2d.py']}

View File

@ -4,36 +4,38 @@
from qtpy.QtDesigner import QDesignerCustomWidgetInterface from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from bec_widgets.utils.bec_designer import designer_material_icon from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.control.device_input.signal_combobox.signal_combobox import SignalComboBox from bec_widgets.widgets.control.device_control.positioner_box.positioner_box_2d.positioner_box_2d import (
PositionerBox2D,
)
DOM_XML = """ DOM_XML = """
<ui language='c++'> <ui language='c++'>
<widget class='SignalComboBox' name='signal_combo_box'> <widget class='PositionerBox2D' name='positioner_box2_d'>
</widget> </widget>
</ui> </ui>
""" """
class SignalComboBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover class PositionerBox2DPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self._form_editor = None self._form_editor = None
def createWidget(self, parent): def createWidget(self, parent):
t = SignalComboBox(parent) t = PositionerBox2D(parent)
return t return t
def domXml(self): def domXml(self):
return DOM_XML return DOM_XML
def group(self): def group(self):
return "BEC Input Widgets" return "Device Control"
def icon(self): def icon(self):
return designer_material_icon(SignalComboBox.ICON_NAME) return designer_material_icon(PositionerBox2D.ICON_NAME)
def includeFile(self): def includeFile(self):
return "signal_combo_box" return "positioner_box2_d"
def initialize(self, form_editor): def initialize(self, form_editor):
self._form_editor = form_editor self._form_editor = form_editor
@ -45,10 +47,10 @@ class SignalComboBoxPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
return self._form_editor is not None return self._form_editor is not None
def name(self): def name(self):
return "SignalComboBox" return "PositionerBox2D"
def toolTip(self): def toolTip(self):
return "" return "Simple Widget to control two positioners in box form"
def whatsThis(self): def whatsThis(self):
return self.toolTip() return self.toolTip()

View File

@ -0,0 +1,482 @@
""" Module for a PositionerBox2D widget to control two positioner devices."""
from __future__ import annotations
import os
from typing import Literal
from bec_lib.device import Positioner
from bec_lib.logger import bec_logger
from bec_qthemes import material_icon
from qtpy.QtCore import Signal
from qtpy.QtGui import QDoubleValidator
from qtpy.QtWidgets import QDoubleSpinBox
from bec_widgets.qt_utils.error_popups import SafeProperty, SafeSlot
from bec_widgets.utils import UILoader
from bec_widgets.utils.colors import set_theme
from bec_widgets.widgets.control.device_control.positioner_box._base import PositionerBoxBase
from bec_widgets.widgets.control.device_control.positioner_box._base.positioner_box_base import (
DeviceUpdateUIComponents,
)
logger = bec_logger.logger
MODULE_PATH = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
DeviceId = Literal["horizontal", "vertical"]
class PositionerBox2D(PositionerBoxBase):
"""Simple Widget to control two positioners in box form"""
ui_file = "positioner_box_2d.ui"
PLUGIN = True
USER_ACCESS = ["set_positioner_hor", "set_positioner_ver"]
device_changed_hor = Signal(str, str)
device_changed_ver = Signal(str, str)
# Signals emitted to inform listeners about a position update
position_update_hor = Signal(float)
position_update_ver = Signal(float)
def __init__(
self,
parent=None,
device_hor: Positioner | str | None = None,
device_ver: Positioner | str | None = None,
**kwargs,
):
"""Initialize the PositionerBox widget.
Args:
parent: The parent widget.
device_hor (Positioner | str): The first device to control - assigned the horizontal axis.
device_ver (Positioner | str): The second device to control - assigned the vertical axis.
"""
super().__init__(parent=parent, **kwargs)
self._device_hor = ""
self._device_ver = ""
self._limits_hor = None
self._limits_ver = None
self._dialog = None
if self.current_path == "":
self.current_path = os.path.dirname(__file__)
self.init_ui()
self.device_hor = device_hor
self.device_ver = device_ver
self.connect_ui()
def init_ui(self):
"""Init the ui"""
self.device_changed_hor.connect(self.on_device_change_hor)
self.device_changed_ver.connect(self.on_device_change_ver)
self.ui = UILoader(self).loader(os.path.join(self.current_path, self.ui_file))
self.setpoint_validator_hor = QDoubleValidator()
self.setpoint_validator_ver = QDoubleValidator()
def connect_ui(self):
"""Connect the UI components to signals, data, or routines"""
self.addWidget(self.ui)
self.layout.setSpacing(0)
self.layout.setContentsMargins(0, 0, 0, 0)
def _init_ui(val: QDoubleValidator, device_id: DeviceId):
ui = self._device_ui_components_hv(device_id)
tweak_inc = (
self.on_tweak_inc_hor if device_id == "horizontal" else self.on_tweak_inc_ver
)
tweak_dec = (
self.on_tweak_dec_hor if device_id == "horizontal" else self.on_tweak_dec_ver
)
ui["setpoint"].setValidator(val)
ui["setpoint"].returnPressed.connect(
self.on_setpoint_change_hor
if device_id == "horizontal"
else self.on_setpoint_change_ver
)
ui["stop"].setToolTip("Stop")
ui["step_size"].setStepType(QDoubleSpinBox.StepType.AdaptiveDecimalStepType)
ui["tweak_increase"].clicked.connect(tweak_inc)
ui["tweak_decrease"].clicked.connect(tweak_dec)
_init_ui(self.setpoint_validator_hor, "horizontal")
_init_ui(self.setpoint_validator_ver, "vertical")
self.ui.stop_button.button.clicked.connect(self.on_stop)
self.ui.step_decrease_hor.clicked.connect(self.on_step_dec_hor)
self.ui.step_decrease_ver.clicked.connect(self.on_step_dec_ver)
self.ui.step_increase_hor.clicked.connect(self.on_step_inc_hor)
self.ui.step_increase_ver.clicked.connect(self.on_step_inc_ver)
self.ui.tool_button_hor.clicked.connect(
self._open_dialog_selection(self.set_positioner_hor)
)
self.ui.tool_button_ver.clicked.connect(
self._open_dialog_selection(self.set_positioner_ver)
)
icon = material_icon(icon_name="edit_note", size=(16, 16), convert_to_pixmap=False)
self.ui.tool_button_hor.setIcon(icon)
self.ui.tool_button_ver.setIcon(icon)
step_tooltip = "Step by the step size"
tweak_tooltip = "Tweak by 1/10th the step size"
for b in [
self.ui.step_increase_hor,
self.ui.step_increase_ver,
self.ui.step_decrease_hor,
self.ui.step_decrease_ver,
]:
b.setToolTip(step_tooltip)
for b in [
self.ui.tweak_increase_hor,
self.ui.tweak_increase_ver,
self.ui.tweak_decrease_hor,
self.ui.tweak_decrease_ver,
]:
b.setToolTip(tweak_tooltip)
icon_options = {"size": (16, 16), "convert_to_pixmap": False}
self.ui.tweak_increase_hor.setIcon(
material_icon(icon_name="keyboard_arrow_right", **icon_options)
)
self.ui.step_increase_hor.setIcon(
material_icon(icon_name="keyboard_double_arrow_right", **icon_options)
)
self.ui.tweak_decrease_hor.setIcon(
material_icon(icon_name="keyboard_arrow_left", **icon_options)
)
self.ui.step_decrease_hor.setIcon(
material_icon(icon_name="keyboard_double_arrow_left", **icon_options)
)
self.ui.tweak_increase_ver.setIcon(
material_icon(icon_name="keyboard_arrow_up", **icon_options)
)
self.ui.step_increase_ver.setIcon(
material_icon(icon_name="keyboard_double_arrow_up", **icon_options)
)
self.ui.tweak_decrease_ver.setIcon(
material_icon(icon_name="keyboard_arrow_down", **icon_options)
)
self.ui.step_decrease_ver.setIcon(
material_icon(icon_name="keyboard_double_arrow_down", **icon_options)
)
@SafeProperty(str)
def device_hor(self):
"""SafeProperty to set the device"""
return self._device_hor
@device_hor.setter
def device_hor(self, value: str):
"""Setter, checks if device is a string"""
if not value or not isinstance(value, str):
return
if not self._check_device_is_valid(value):
return
if value == self.device_ver:
return
old_device = self._device_hor
self._device_hor = value
self.label = f"{self._device_hor}, {self._device_ver}"
self.device_changed_hor.emit(old_device, value)
self._init_device(self.device_hor, self.position_update_hor.emit, self.update_limits_hor)
@SafeProperty(str)
def device_ver(self):
"""SafeProperty to set the device"""
return self._device_ver
@device_ver.setter
def device_ver(self, value: str):
"""Setter, checks if device is a string"""
if not value or not isinstance(value, str):
return
if not self._check_device_is_valid(value):
return
if value == self.device_hor:
return
old_device = self._device_ver
self._device_ver = value
self.label = f"{self._device_hor}, {self._device_ver}"
self.device_changed_ver.emit(old_device, value)
self._init_device(self.device_ver, self.position_update_ver.emit, self.update_limits_ver)
@SafeProperty(bool)
def hide_device_selection(self):
"""Hide the device selection"""
return not self.ui.tool_button_hor.isVisible()
@hide_device_selection.setter
def hide_device_selection(self, value: bool):
"""Set the device selection visibility"""
self.ui.tool_button_hor.setVisible(not value)
self.ui.tool_button_ver.setVisible(not value)
@SafeProperty(bool)
def hide_device_boxes(self):
"""Hide the device selection"""
return not self.ui.device_box_hor.isVisible()
@hide_device_boxes.setter
def hide_device_boxes(self, value: bool):
"""Set the device selection visibility"""
self.ui.device_box_hor.setVisible(not value)
self.ui.device_box_ver.setVisible(not value)
@SafeSlot(bool)
def show_device_selection(self, value: bool):
"""Show the device selection
Args:
value (bool): Show the device selection
"""
self.hide_device_selection = not value
@SafeSlot(str)
def set_positioner_hor(self, positioner: str | Positioner):
"""Set the device
Args:
positioner (Positioner | str) : Positioner to set, accepts str or the device
"""
if isinstance(positioner, Positioner):
positioner = positioner.name
self.device_hor = positioner
@SafeSlot(str)
def set_positioner_ver(self, positioner: str | Positioner):
"""Set the device
Args:
positioner (Positioner | str) : Positioner to set, accepts str or the device
"""
if isinstance(positioner, Positioner):
positioner = positioner.name
self.device_ver = positioner
@SafeSlot(str, str)
def on_device_change_hor(self, old_device: str, new_device: str):
"""Upon changing the device, a check will be performed if the device is a Positioner.
Args:
old_device (str): The old device name.
new_device (str): The new device name.
"""
if not self._check_device_is_valid(new_device):
return
self._on_device_change(
old_device,
new_device,
self.position_update_hor.emit,
self.update_limits_hor,
self.on_device_readback_hor,
self._device_ui_components_hv("horizontal"),
)
@SafeSlot(str, str)
def on_device_change_ver(self, old_device: str, new_device: str):
"""Upon changing the device, a check will be performed if the device is a Positioner.
Args:
old_device (str): The old device name.
new_device (str): The new device name.
"""
if not self._check_device_is_valid(new_device):
return
self._on_device_change(
old_device,
new_device,
self.position_update_ver.emit,
self.update_limits_ver,
self.on_device_readback_ver,
self._device_ui_components_hv("vertical"),
)
def _device_ui_components_hv(self, device: DeviceId) -> DeviceUpdateUIComponents:
if device == "horizontal":
return {
"spinner": self.ui.spinner_widget_hor,
"position_indicator": self.ui.position_indicator_hor,
"readback": self.ui.readback_hor,
"setpoint": self.ui.setpoint_hor,
"step_size": self.ui.step_size_hor,
"device_box": self.ui.device_box_hor,
"stop": self.ui.stop_button,
"tweak_increase": self.ui.tweak_increase_hor,
"tweak_decrease": self.ui.tweak_decrease_hor,
}
elif device == "vertical":
return {
"spinner": self.ui.spinner_widget_ver,
"position_indicator": self.ui.position_indicator_ver,
"readback": self.ui.readback_ver,
"setpoint": self.ui.setpoint_ver,
"step_size": self.ui.step_size_ver,
"device_box": self.ui.device_box_ver,
"stop": self.ui.stop_button,
"tweak_increase": self.ui.tweak_increase_ver,
"tweak_decrease": self.ui.tweak_decrease_ver,
}
else:
raise ValueError(f"Device {device} is not represented by this UI")
def _device_ui_components(self, device: str):
if device == self.device_hor:
return self._device_ui_components_hv("horizontal")
if device == self.device_ver:
return self._device_ui_components_hv("vertical")
@SafeSlot(dict, dict)
def on_device_readback_hor(self, msg_content: dict, metadata: dict):
"""Callback for device readback.
Args:
msg_content (dict): The message content.
metadata (dict): The message metadata.
"""
self._on_device_readback(
self.device_hor,
self._device_ui_components_hv("horizontal"),
msg_content,
metadata,
self.position_update_hor.emit,
self.update_limits_hor,
)
@SafeSlot(dict, dict)
def on_device_readback_ver(self, msg_content: dict, metadata: dict):
"""Callback for device readback.
Args:
msg_content (dict): The message content.
metadata (dict): The message metadata.
"""
self._on_device_readback(
self.device_ver,
self._device_ui_components_hv("vertical"),
msg_content,
metadata,
self.position_update_ver.emit,
self.update_limits_ver,
)
def update_limits_hor(self, limits: tuple):
"""Update limits
Args:
limits (tuple): Limits of the positioner
"""
if limits == self._limits_hor:
return
self._limits_hor = limits
self._update_limits_ui(limits, self.ui.position_indicator_hor, self.setpoint_validator_hor)
def update_limits_ver(self, limits: tuple):
"""Update limits
Args:
limits (tuple): Limits of the positioner
"""
if limits == self._limits_ver:
return
self._limits_ver = limits
self._update_limits_ui(limits, self.ui.position_indicator_ver, self.setpoint_validator_ver)
@SafeSlot()
def on_stop(self):
self._stop_device(f"{self.device_hor} or {self.device_ver}")
@SafeProperty(float)
def step_size_hor(self):
"""Step size for tweak"""
return self.ui.step_size_hor.value()
@step_size_hor.setter
def step_size_hor(self, val: float):
"""Step size for tweak"""
self.ui.step_size_hor.setValue(val)
@SafeProperty(float)
def step_size_ver(self):
"""Step size for tweak"""
return self.ui.step_size_ver.value()
@step_size_ver.setter
def step_size_ver(self, val: float):
"""Step size for tweak"""
self.ui.step_size_ver.setValue(val)
@SafeSlot()
def on_tweak_inc_hor(self):
"""Tweak device a up"""
self.dev[self.device_hor].move(self.step_size_hor / 10, relative=True)
@SafeSlot()
def on_tweak_dec_hor(self):
"""Tweak device a down"""
self.dev[self.device_hor].move(-self.step_size_hor / 10, relative=True)
@SafeSlot()
def on_step_inc_hor(self):
"""Tweak device a up"""
self.dev[self.device_hor].move(self.step_size_hor, relative=True)
@SafeSlot()
def on_step_dec_hor(self):
"""Tweak device a down"""
self.dev[self.device_hor].move(-self.step_size_hor, relative=True)
@SafeSlot()
def on_tweak_inc_ver(self):
"""Tweak device a up"""
self.dev[self.device_ver].move(self.step_size_ver / 10, relative=True)
@SafeSlot()
def on_tweak_dec_ver(self):
"""Tweak device b down"""
self.dev[self.device_ver].move(-self.step_size_ver / 10, relative=True)
@SafeSlot()
def on_step_inc_ver(self):
"""Tweak device b up"""
self.dev[self.device_ver].move(self.step_size_ver, relative=True)
@SafeSlot()
def on_step_dec_ver(self):
"""Tweak device a down"""
self.dev[self.device_ver].move(-self.step_size_ver, relative=True)
@SafeSlot()
def on_setpoint_change_hor(self):
"""Change the setpoint for device a"""
self.ui.setpoint_hor.clearFocus()
setpoint = self.ui.setpoint_hor.text()
self.dev[self.device_hor].move(float(setpoint), relative=False)
@SafeSlot()
def on_setpoint_change_ver(self):
"""Change the setpoint for device b"""
self.ui.setpoint_ver.clearFocus()
setpoint = self.ui.setpoint_ver.text()
self.dev[self.device_ver].move(float(setpoint), relative=False)
if __name__ == "__main__": # pragma: no cover
import sys
from qtpy.QtWidgets import QApplication # pylint: disable=ungrouped-imports
app = QApplication(sys.argv)
set_theme("dark")
widget = PositionerBox2D()
widget.show()
sys.exit(app.exec_())

View File

@ -0,0 +1,504 @@
<?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>326</width>
<height>323</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="1">
<layout class="QGridLayout" name="gridLayout_4">
<item row="0" column="1">
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="2">
<widget class="QGroupBox" name="device_box_ver">
<property name="title">
<string>No positioner selected</string>
</property>
<layout class="QGridLayout" name="gridLayout_6" rowstretch="0,0,0,0,0,0">
<property name="topMargin">
<number>0</number>
</property>
<item row="1" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QToolButton" name="tool_button_ver">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="readback_ver">
<property name="text">
<string>Position</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="SpinnerWidget" name="spinner_widget_ver">
<property name="minimumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<widget class="QLineEdit" name="setpoint_ver">
<property name="enabled">
<bool>false</bool>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
<item row="5" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<widget class="QDoubleSpinBox" name="step_size_ver">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item row="0" column="1">
<widget class="QGroupBox" name="device_box_hor">
<property name="title">
<string>No positioner selected</string>
</property>
<layout class="QGridLayout" name="gridLayout_5" rowstretch="0,0,0,0,0,0,0,0,0,0,0">
<property name="topMargin">
<number>0</number>
</property>
<item row="9" column="0">
<widget class="QLineEdit" name="setpoint_hor">
<property name="enabled">
<bool>false</bool>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
<item row="1" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QToolButton" name="tool_button_hor">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="readback_hor">
<property name="text">
<string>Position</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="SpinnerWidget" name="spinner_widget_hor">
<property name="minimumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>25</width>
<height>25</height>
</size>
</property>
</widget>
</item>
</layout>
</item>
<item row="10" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QDoubleSpinBox" name="step_size_hor">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item row="4" column="0">
<widget class="PositionIndicator" name="position_indicator_ver">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximum" stdset="0">
<double>1.000000000000000</double>
</property>
<property name="vertical" stdset="0">
<bool>true</bool>
</property>
<property name="value" stdset="0">
<double>0.500000000000000</double>
</property>
<property name="indicator_width" stdset="0">
<number>4</number>
</property>
<property name="rounded_corners" stdset="0">
<number>4</number>
</property>
</widget>
</item>
<item row="4" column="1">
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="3">
<widget class="QPushButton" name="step_increase_ver">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="2" column="5">
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="6" column="3">
<widget class="QPushButton" name="step_decrease_ver">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="4" column="5">
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="6" column="2">
<spacer name="horizontalSpacer_16">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>10</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="2" column="1">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="2">
<spacer name="horizontalSpacer_14">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>10</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="4" column="1">
<spacer name="horizontalSpacer_17">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="2" column="2">
<spacer name="horizontalSpacer_7">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>10</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="6" column="1">
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="3" column="3">
<spacer name="horizontalSpacer_6">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="3" column="5">
<widget class="QPushButton" name="step_increase_hor">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="6" column="4">
<spacer name="horizontalSpacer_10">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="3" column="2">
<widget class="QPushButton" name="tweak_decrease_hor">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="4" column="2">
<spacer name="horizontalSpacer_15">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>10</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="1">
<spacer name="horizontalSpacer_35">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="4" column="3">
<widget class="QPushButton" name="tweak_decrease_ver">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QPushButton" name="step_decrease_hor">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="3" column="4">
<widget class="QPushButton" name="tweak_increase_hor">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="2" column="3">
<widget class="QPushButton" name="tweak_increase_ver">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="2" column="4">
<spacer name="horizontalSpacer_12">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="4">
<spacer name="horizontalSpacer_8">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="4" column="4">
<spacer name="horizontalSpacer_11">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="5">
<spacer name="horizontalSpacer_5">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="6" column="5">
<widget class="StopButton" name="stop_button"/>
</item>
</layout>
</item>
<item row="2" column="1">
<widget class="PositionIndicator" name="position_indicator_hor">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximum" stdset="0">
<double>1.000000000000000</double>
</property>
<property name="value" stdset="0">
<double>0.500000000000000</double>
</property>
<property name="indicator_width" stdset="0">
<number>4</number>
</property>
<property name="rounded_corners" stdset="0">
<number>4</number>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>StopButton</class>
<extends>QWidget</extends>
<header>stop_button</header>
</customwidget>
<customwidget>
<class>PositionIndicator</class>
<extends>QWidget</extends>
<header>position_indicator</header>
</customwidget>
<customwidget>
<class>SpinnerWidget</class>
<extends>QWidget</extends>
<header>spinner_widget</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@ -6,11 +6,11 @@ def main(): # pragma: no cover
return return
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
from bec_widgets.widgets.control.device_input.signal_combobox.signal_combo_box_plugin import ( from bec_widgets.widgets.control.device_control.positioner_box.positioner_box_2d.positioner_box2_d_plugin import (
SignalComboBoxPlugin, PositionerBox2DPlugin,
) )
QPyDesignerCustomWidgetCollection.addCustomWidget(SignalComboBoxPlugin()) QPyDesignerCustomWidgetCollection.addCustomWidget(PositionerBox2DPlugin())
if __name__ == "__main__": # pragma: no cover if __name__ == "__main__": # pragma: no cover

View File

@ -4,9 +4,10 @@ from __future__ import annotations
from bec_lib.device import Positioner from bec_lib.device import Positioner
from bec_lib.logger import bec_logger from bec_lib.logger import bec_logger
from qtpy.QtCore import Property, QSize, Signal, Slot from qtpy.QtCore import QSize, Signal
from qtpy.QtWidgets import QGridLayout, QGroupBox, QVBoxLayout, QWidget from qtpy.QtWidgets import QGridLayout, QGroupBox, QVBoxLayout, QWidget
from bec_widgets.qt_utils.error_popups import SafeProperty, SafeSlot
from bec_widgets.utils.bec_widget import BECWidget from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.widgets.control.device_control.positioner_box import PositionerBox from bec_widgets.widgets.control.device_control.positioner_box import PositionerBox
@ -32,7 +33,7 @@ class PositionerGroupBox(QGroupBox):
self.widget.position_update.connect(self._on_position_update) self.widget.position_update.connect(self._on_position_update)
self.widget.expand.connect(self._on_expand) self.widget.expand.connect(self._on_expand)
self.setTitle(self.device_name) self.setTitle(self.device_name)
self.widget.init_device() # force readback self.widget.force_update_readback()
def _on_expand(self, expand): def _on_expand(self, expand):
if expand: if expand:
@ -82,7 +83,7 @@ class PositionerGroup(BECWidget, QWidget):
def minimumSizeHint(self): def minimumSizeHint(self):
return QSize(300, 30) return QSize(300, 30)
@Slot(str) @SafeSlot(str)
def set_positioners(self, device_names: str): def set_positioners(self, device_names: str):
"""Redraw grid with positioners from device_names string """Redraw grid with positioners from device_names string
@ -130,7 +131,7 @@ class PositionerGroup(BECWidget, QWidget):
widget = self.sender() widget = self.sender()
self.device_position_update.emit(widget.title(), pos) self.device_position_update.emit(widget.title(), pos)
@Property(str) @SafeProperty(str)
def devices_list(self): def devices_list(self):
"""Device names string separated by space""" """Device names string separated by space"""
return " ".join(self._device_widgets) return " ".join(self._device_widgets)
@ -144,7 +145,7 @@ class PositionerGroup(BECWidget, QWidget):
return return
self.set_positioners(device_names) self.set_positioners(device_names)
@Property(int) @SafeProperty(int)
def grid_max_cols(self): def grid_max_cols(self):
"""Max number of columns for widgets grid""" """Max number of columns for widgets grid"""
return self._grid_ncols return self._grid_ncols

View File

@ -1 +0,0 @@
{'files': ['signal_combobox.py']}

View File

@ -158,3 +158,10 @@ def test_positioner_box_open_dialog_selection(qtbot, positioner_box):
QTimer.singleShot(100, close_dialog) QTimer.singleShot(100, close_dialog)
qtbot.mouseClick(positioner_box.ui.tool_button, Qt.LeftButton) qtbot.mouseClick(positioner_box.ui.tool_button, Qt.LeftButton)
assert positioner_box.device == "samy" assert positioner_box.device == "samy"
def test_device_validity_check_rejects_non_positioner():
# isinstance checks for PositionerBox are mocked out in the mock client
positioner_box = mock.MagicMock(spec=PositionerBox)
positioner_box.dev = {"test": 5.123}
assert not PositionerBox._check_device_is_valid(positioner_box, "test")

View File

@ -0,0 +1,82 @@
from unittest import mock
import pytest
from bec_widgets.widgets.control.device_control.positioner_box import PositionerBox2D
from .client_mocks import mocked_client
from .conftest import create_widget
@pytest.fixture
def positioner_box_2d(qtbot, mocked_client):
"""Fixture for PositionerBox widget"""
with mock.patch(
"bec_widgets.widgets.control.device_control.positioner_box._base.positioner_box_base.uuid.uuid4"
) as mock_uuid:
mock_uuid.return_value = "fake_uuid"
with mock.patch(
"bec_widgets.widgets.control.device_control.positioner_box._base.positioner_box_base.PositionerBoxBase._check_device_is_valid",
return_value=True,
):
db = create_widget(
qtbot, PositionerBox2D, device_hor="samx", device_ver="samy", client=mocked_client
)
yield db
def test_positioner_box_2d(positioner_box_2d):
"""Test init of 2D positioner box"""
assert positioner_box_2d.device_hor == "samx"
assert positioner_box_2d.device_ver == "samy"
data_hor = positioner_box_2d.dev["samx"].read()
data_ver = positioner_box_2d.dev["samy"].read()
# Avoid check for Positioner class from BEC in _init_device
setpoint_hor_text = positioner_box_2d.ui.setpoint_hor.text()
setpoint_ver_text = positioner_box_2d.ui.setpoint_ver.text()
# check that the setpoint is taken correctly after init
assert float(setpoint_hor_text) == data_hor["samx_setpoint"]["value"]
assert float(setpoint_ver_text) == data_ver["samy_setpoint"]["value"]
# check that the precision is taken correctly after init
precision_hor = positioner_box_2d.dev["samx"].precision
precision_ver = positioner_box_2d.dev["samy"].precision
assert setpoint_hor_text == f"{data_hor['samx_setpoint']['value']:.{precision_hor}f}"
assert setpoint_ver_text == f"{data_ver['samy_setpoint']['value']:.{precision_ver}f}"
# check that the step size is set according to the device precision
assert positioner_box_2d.ui.step_size_hor.value() == 10**-precision_hor * 10
assert positioner_box_2d.ui.step_size_ver.value() == 10**-precision_ver * 10
def test_positioner_box_move_hor_does_not_affect_ver(positioner_box_2d):
"""Test that moving one positioner doesn't affect the other"""
with (
mock.patch.object(positioner_box_2d.dev["samx"], "move") as mock_move_hor,
mock.patch.object(positioner_box_2d.dev["samy"], "move") as mock_move_ver,
):
positioner_box_2d.ui.step_size_hor.setValue(0.1)
positioner_box_2d.on_tweak_inc_hor()
mock_move_hor.assert_called_once_with(0.01, relative=True)
mock_move_ver.assert_not_called()
with (
mock.patch.object(positioner_box_2d.dev["samx"], "move") as mock_move_hor,
mock.patch.object(positioner_box_2d.dev["samy"], "move") as mock_move_ver,
):
positioner_box_2d.ui.step_size_ver.setValue(0.1)
positioner_box_2d.on_step_dec_ver()
mock_move_ver.assert_called_once_with(-0.1, relative=True)
mock_move_hor.assert_not_called()
def test_positioner_box_setpoint_changes(positioner_box_2d: PositionerBox2D):
"""Test positioner box setpoint change"""
with mock.patch.object(positioner_box_2d.dev["samx"], "move") as mock_move:
positioner_box_2d.ui.setpoint_hor.setText("100")
positioner_box_2d.on_setpoint_change_hor()
mock_move.assert_called_once_with(100, relative=False)
with mock.patch.object(positioner_box_2d.dev["samy"], "move") as mock_move:
positioner_box_2d.ui.setpoint_ver.setText("100")
positioner_box_2d.on_setpoint_change_ver()
mock_move.assert_called_once_with(100, relative=False)