0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-13 19:21:50 +02:00

feat: add first draft for alignment_1d GUI

This commit is contained in:
2024-09-10 17:44:44 +02:00
parent efe90eb163
commit 63c24f97a3
6 changed files with 1253 additions and 0 deletions

View File

@ -0,0 +1,225 @@
""" This module contains the GUI for the 1D alignment application.
#TODO at this stage it is a preliminary version of the GUI, which will be added to the main branch although it is not yet fully functional.
It is a work in progress and will be updated in the future.
"""
import os
from typing import Optional
from bec_lib.device import Positioner, Signal
from bec_lib.endpoints import MessageEndpoints
from qtpy.QtCore import Signal as pyqtSignal
from qtpy.QtWidgets import QCheckBox, QDoubleSpinBox, QSpinBox, QVBoxLayout, QWidget
from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
from bec_widgets.qt_utils.toolbar import WidgetAction
from bec_widgets.utils import UILoader
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.widgets.bec_progressbar.bec_progressbar import BECProgressBar
from bec_widgets.widgets.device_line_edit.device_line_edit import DeviceLineEdit
from bec_widgets.widgets.lmfit_dialog.lmfit_dialog import LMFitDialog
from bec_widgets.widgets.positioner_box.positioner_control_line import PositionerControlLine
from bec_widgets.widgets.toggle.toggle import ToggleSwitch
from bec_widgets.widgets.waveform.waveform_widget import BECWaveformWidget
class Alignment1D(BECWidget, QWidget):
"""GUI for 1D alignment"""
motion_is_active = pyqtSignal(bool)
def __init__(
self, parent: Optional[QWidget] = None, client=None, gui_id: Optional[str] = None
) -> None:
"""Initialise the widget
Args:
parent: Parent widget.
config: Configuration of the widget.
client: BEC client object.
gui_id: GUI ID.
"""
super().__init__(client=client, gui_id=gui_id)
QWidget.__init__(self, parent)
self.get_bec_shortcuts()
self.ui_file = "alignment_1d.ui"
self.ui = None
self.progress_bar = None
self.waveform = None
self.init_ui()
def init_ui(self):
"""Initialise the UI from QT Designer file"""
current_path = os.path.dirname(__file__)
self.ui = UILoader(self).loader(os.path.join(current_path, self.ui_file))
layout = QVBoxLayout(self)
layout.addWidget(self.ui)
self.setLayout(layout)
self.waveform = self.ui.findChild(BECWaveformWidget, "bec_waveform_widget")
# Customize the plotting widget
self._customise_bec_waveform_widget()
# Setup filters for comboboxes
self._setup_motor_combobox()
self._setup_signal_combobox()
# Setup arrow item
self._setup_arrow_item()
# Setup Scan Control
self._setup_scan_control()
# Setup progress bar
self._setup_progress_bar()
# Add actions buttons
self._add_action_buttons()
# Hook scaninfo updates
self.bec_dispatcher.connect_slot(self._scan_status_callback, MessageEndpoints.scan_status())
##############################
############ SLOTS ###########
##############################
@Slot(dict, dict)
def _scan_status_callback(self, content: dict, metadata: dict) -> None:
"""This slot allows to enable/disable the UI critical components when a scan is running"""
if content["status"] in ["running", "open"]:
self.motion_is_active.emit(True)
self._enable_ui(False)
elif content["status"] in ["aborted", "halted", "closed"]:
self.motion_is_active.emit(False)
self._enable_ui(True)
@Slot(float)
def move_to_center(self, pos: float) -> None:
"""Move the selected motor to the center"""
motor = self.ui.device_combobox.currentText()
self.dev[motor].move(float(pos), relative=False)
@Slot()
def _reset_progress_bar(self) -> None:
"""Reset the progress bar"""
self.progress_bar.set_value(0)
self.progress_bar.set_minimum(0)
@Slot(dict, dict)
def _update_progress_bar(self, content: dict, metadata: dict) -> None:
"""Hook to update the progress bar
Args:
content: Content of the scan progress message.
metadata: Metadata of the message.
"""
if content["max_value"] == 0:
self.progress_bar.set_value(0)
return
self.progress_bar.set_maximum(content["max_value"])
self.progress_bar.set_value(content["value"])
##############################
######## END OF SLOTS ########
##############################
def _enable_ui(self, enable: bool) -> None:
"""Enable or disable the UI components"""
# Device selection comboboxes
self.ui.device_combobox.setEnabled(enable)
self.ui.device_combobox_2.setEnabled(enable)
self.ui.dap_combo_box.setEnabled(enable)
# Scan button
self.ui.scan_button.setEnabled(enable)
# Positioner control line
# pylint: disable=protected-access
self.ui.positioner_control_line._toogle_enable_buttons(enable)
# Send report to scilog
self.ui.button_send_summary_scilog.setEnabled(enable)
# Disable move to buttons in LMFitDialog
self.ui.findChild(LMFitDialog).set_enable_move_to_buttons(enable)
def _add_action_buttons(self) -> None:
"""Add action buttons for the Action Control"""
fit_dialog = self.ui.findChild(LMFitDialog)
fit_dialog.update_activated_button_list(["center", "center1", "center2"])
fit_dialog.move_to_position.connect(self.move_to_center)
def _customise_bec_waveform_widget(self) -> None:
"""Customise the BEC Waveform Widget, i.e. hide toolbar buttons except roi selection"""
for button in self.waveform.toolbar.widgets.values():
if getattr(button, "action", None) is not None:
button.action.setVisible(False)
self.waveform.toolbar.widgets["roi_select"].action.setVisible(True)
toggle_switch = self.ui.findChild(ToggleSwitch, "toggle_switch")
scan_control = self.ui.scan_control
self.waveform.toolbar.populate_toolbar(
{
"label": WidgetAction(label="Enable DAP ROI", widget=toggle_switch),
"scan_control": WidgetAction(widget=scan_control),
},
self.waveform,
)
def _setup_arrow_item(self) -> None:
"""Setup the arrow item"""
self.waveform.waveform.motor_pos_tick.add_to_plot()
positioner_line = self.ui.findChild(PositionerControlLine)
positioner_line.position_update.connect(self.waveform.waveform.motor_pos_tick.set_position)
try:
pos = float(positioner_line.ui.readback.text())
except ValueError:
pos = 0
self.waveform.waveform.motor_pos_tick.set_position(pos)
def _setup_motor_combobox(self) -> None:
"""Setup motor selection"""
# FIXME after changing the filtering in the combobox
motors = [name for name in self.dev if isinstance(self.dev.get(name), Positioner)]
self.ui.device_combobox.setCurrentText(motors[0])
self.ui.device_combobox.set_device_filter("Positioner")
def _setup_signal_combobox(self) -> None:
"""Setup signal selection"""
# FIXME after changing the filtering in the combobox
signals = [name for name in self.dev if isinstance(self.dev.get(name), Signal)]
self.ui.device_combobox_2.setCurrentText(signals[0])
self.ui.device_combobox_2.set_device_filter("Signal")
def _setup_scan_control(self) -> None:
"""Setup scan control, connect spin and check boxes to the scan_control widget"""
device_line_edit = self.ui.scan_control.arg_box.findChild(DeviceLineEdit)
self.ui.device_combobox.currentTextChanged.connect(device_line_edit.setText)
device_line_edit.setText(self.ui.device_combobox.currentText())
spin_boxes = self.ui.scan_control.arg_box.findChildren(QDoubleSpinBox)
start = self.ui.findChild(QDoubleSpinBox, "linescan_start")
start.valueChanged.connect(spin_boxes[0].setValue)
stop = self.ui.findChild(QDoubleSpinBox, "linescan_stop")
stop.valueChanged.connect(spin_boxes[1].setValue)
step = self.ui.findChild(QSpinBox, "linescan_step")
step.valueChanged.connect(
self.ui.scan_control.kwarg_boxes[0].findChildren(QSpinBox)[0].setValue
)
exp_time = self.ui.findChild(QDoubleSpinBox, "linescan_exp_time")
exp_time.valueChanged.connect(
self.ui.scan_control.kwarg_boxes[1].findChildren(QDoubleSpinBox)[0].setValue
)
relative = self.ui.findChild(QCheckBox, "linescan_relative")
relative.toggled.connect(
self.ui.scan_control.kwarg_boxes[0].findChildren(QCheckBox)[0].setChecked
)
def _setup_progress_bar(self) -> None:
"""Setup progress bar"""
# FIXME once the BECScanProgressBar is implemented
self.progress_bar = self.ui.findChild(BECProgressBar, "bec_progress_bar")
self.progress_bar.set_value(0)
self.ui.bec_waveform_widget.new_scan.connect(self._reset_progress_bar)
self.bec_dispatcher.connect_slot(
self._update_progress_bar, MessageEndpoints.scan_progress()
)
if __name__ == "__main__":
import sys
from qtpy.QtWidgets import QApplication
app = QApplication(sys.argv)
window = Alignment1D()
window.show()
sys.exit(app.exec_())

View File

@ -0,0 +1,779 @@
<?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>1578</width>
<height>962</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>12</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>12</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout" stretch="1">
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tab">
<attribute name="title">
<string>Alignment Control</string>
</attribute>
<layout class="QHBoxLayout" name="horizontalLayout_6" stretch="0,1,2">
<property name="leftMargin">
<number>4</number>
</property>
<property name="topMargin">
<number>4</number>
</property>
<property name="rightMargin">
<number>4</number>
</property>
<property name="bottomMargin">
<number>4</number>
</property>
<item>
<widget class="ScanControl" name="scan_control">
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="current_scan" stdset="0">
<string>line_scan</string>
</property>
<property name="hide_arg_box" stdset="0">
<bool>true</bool>
</property>
<property name="hide_kwarg_boxes" stdset="0">
<bool>true</bool>
</property>
<property name="hide_scan_remember_toggle" stdset="0">
<bool>true</bool>
</property>
<property name="hide_scan_control_buttons" stdset="0">
<bool>true</bool>
</property>
<property name="hide_bundle_buttons" stdset="0">
<bool>true</bool>
</property>
<property name="hide_args_group" stdset="0">
<bool>true</bool>
</property>
<property name="hide_kwargs_group" stdset="0">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="control_layout" stretch="0,0,0,0">
<item>
<widget class="QGroupBox" name="device_box">
<property name="minimumSize">
<size>
<width>600</width>
<height>95</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>600</width>
<height>95</height>
</size>
</property>
<property name="font">
<font>
<pointsize>15</pointsize>
<bold>true</bold>
</font>
</property>
<property name="autoFillBackground">
<bool>false</bool>
</property>
<property name="title">
<string>Devices</string>
</property>
<layout class="QGridLayout" name="gridLayout" rowstretch="0,0" columnstretch="0,0,0">
<property name="leftMargin">
<number>8</number>
</property>
<property name="topMargin">
<number>8</number>
</property>
<property name="rightMargin">
<number>8</number>
</property>
<property name="bottomMargin">
<number>8</number>
</property>
<item row="0" column="2">
<widget class="QLabel" name="label_3">
<property name="font">
<font/>
</property>
<property name="text">
<string>LMFit Model</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="DeviceComboBox" name="device_combobox_2"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="font">
<font/>
</property>
<property name="text">
<string>Motor</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="DeviceComboBox" name="device_combobox"/>
</item>
<item row="0" column="1">
<widget class="QLabel" name="label_2">
<property name="font">
<font/>
</property>
<property name="text">
<string>Monitor</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="DapComboBox" name="dap_combo_box"/>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="scan_box">
<property name="minimumSize">
<size>
<width>600</width>
<height>191</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>600</width>
<height>191</height>
</size>
</property>
<property name="font">
<font>
<pointsize>15</pointsize>
<bold>true</bold>
</font>
</property>
<property name="autoFillBackground">
<bool>false</bool>
</property>
<property name="title">
<string>LineScan</string>
</property>
<layout class="QGridLayout" name="gridLayout_3" rowstretch="0,0,0,0,0" columnstretch="0,0,0">
<property name="leftMargin">
<number>8</number>
</property>
<property name="topMargin">
<number>8</number>
</property>
<property name="rightMargin">
<number>8</number>
</property>
<property name="bottomMargin">
<number>8</number>
</property>
<item row="2" column="0">
<widget class="QLabel" name="label_10">
<property name="font">
<font>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>Relative</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QDoubleSpinBox" name="linescan_exp_time">
<property name="decimals">
<number>3</number>
</property>
<property name="minimum">
<double>-10000000.000000000000000</double>
</property>
<property name="maximum">
<double>10000000.000000000000000</double>
</property>
<property name="value">
<double>0.000000000000000</double>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QSpinBox" name="linescan_step_2">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>10000000</number>
</property>
<property name="value">
<number>1</number>
</property>
</widget>
</item>
<item row="4" column="0" colspan="3">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QPushButton" name="scan_button">
<property name="text">
<string>Run Scan</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
<property name="default">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="AbortButton" name="abort_button"/>
</item>
</layout>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_7">
<property name="font">
<font>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>Start</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLabel" name="label_11">
<property name="font">
<font>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>Exposure Time</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QLabel" name="label_9">
<property name="font">
<font>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>Steps</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="label_8">
<property name="font">
<font>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>Stop</string>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QLabel" name="label_12">
<property name="font">
<font>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>Burst at each point</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QDoubleSpinBox" name="linescan_stop">
<property name="decimals">
<number>3</number>
</property>
<property name="minimum">
<double>-10000000.000000000000000</double>
</property>
<property name="maximum">
<double>10000000.000000000000000</double>
</property>
<property name="value">
<double>0.000000000000000</double>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QDoubleSpinBox" name="linescan_start">
<property name="decimals">
<number>3</number>
</property>
<property name="minimum">
<double>-10000000.000000000000000</double>
</property>
<property name="maximum">
<double>10000000.000000000000000</double>
</property>
<property name="value">
<double>0.000000000000000</double>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QSpinBox" name="linescan_step">
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>10000000</number>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="linescan_relative">
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="motor_tweak_box">
<property name="minimumSize">
<size>
<width>600</width>
<height>201</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>600</width>
<height>201</height>
</size>
</property>
<property name="font">
<font>
<pointsize>15</pointsize>
<bold>true</bold>
</font>
</property>
<property name="title">
<string>Motor Tweak</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>12</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>12</number>
</property>
<item>
<widget class="PositionerControlLine" name="positioner_control_line">
<property name="hide_device_selection" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="action_box">
<property name="minimumSize">
<size>
<width>600</width>
<height>85</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>600</width>
<height>85</height>
</size>
</property>
<property name="font">
<font>
<pointsize>15</pointsize>
<bold>true</bold>
</font>
</property>
<property name="title">
<string>Utils</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="QPushButton" name="button_send_summary_scilog">
<property name="text">
<string>Send summary to SciLog</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="plotting_layout" stretch="7,0,2">
<item>
<widget class="BECWaveformWidget" name="bec_waveform_widget">
<property name="minimumSize">
<size>
<width>600</width>
<height>0</height>
</size>
</property>
<property name="clear_curves_on_plot_update" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="ToggleSwitch" name="toggle_switch">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>3</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Activate linear region select for LMFit</string>
</property>
<property name="layoutDirection">
<enum>Qt::LayoutDirection::LeftToRight</enum>
</property>
<property name="checked" stdset="0">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="fit_dialog_layout" stretch="4">
<item>
<widget class="LMFitDialog" name="lm_fit_dialog">
<property name="always_show_latest" stdset="0">
<bool>true</bool>
</property>
<property name="hide_curve_selection" stdset="0">
<bool>true</bool>
</property>
<property name="hide_summary" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_2">
<attribute name="title">
<string>Logbook</string>
</attribute>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="WebsiteWidget" name="website_widget">
<property name="url" stdset="0">
<string>https://scilog.psi.ch/login</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3" stretch="1,4">
<item>
<widget class="StopButton" name="stop_button"/>
</item>
<item>
<widget class="BECProgressBar" name="bec_progress_bar"/>
</item>
</layout>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>DapComboBox</class>
<extends>QWidget</extends>
<header>dap_combo_box</header>
</customwidget>
<customwidget>
<class>StopButton</class>
<extends>QWidget</extends>
<header>stop_button</header>
</customwidget>
<customwidget>
<class>WebsiteWidget</class>
<extends>QWidget</extends>
<header>website_widget</header>
</customwidget>
<customwidget>
<class>ScanControl</class>
<extends>QWidget</extends>
<header>scan_control</header>
</customwidget>
<customwidget>
<class>ToggleSwitch</class>
<extends>QWidget</extends>
<header>toggle_switch</header>
</customwidget>
<customwidget>
<class>PositionerBox</class>
<extends>QWidget</extends>
<header>positioner_box</header>
</customwidget>
<customwidget>
<class>PositionerControlLine</class>
<extends>PositionerBox</extends>
<header>positioner_control_line</header>
</customwidget>
<customwidget>
<class>BECProgressBar</class>
<extends>QWidget</extends>
<header>bec_progress_bar</header>
</customwidget>
<customwidget>
<class>BECWaveformWidget</class>
<extends>QWidget</extends>
<header>bec_waveform_widget</header>
</customwidget>
<customwidget>
<class>DeviceComboBox</class>
<extends>QComboBox</extends>
<header>device_combobox</header>
</customwidget>
<customwidget>
<class>LMFitDialog</class>
<extends>QWidget</extends>
<header>lm_fit_dialog</header>
</customwidget>
<customwidget>
<class>AbortButton</class>
<extends>QWidget</extends>
<header>abort_button</header>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>tabWidget</tabstop>
<tabstop>device_combobox</tabstop>
<tabstop>device_combobox_2</tabstop>
<tabstop>linescan_start</tabstop>
<tabstop>linescan_stop</tabstop>
<tabstop>linescan_step</tabstop>
<tabstop>linescan_relative</tabstop>
<tabstop>linescan_exp_time</tabstop>
<tabstop>linescan_step_2</tabstop>
<tabstop>scan_button</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>device_combobox</sender>
<signal>currentTextChanged(QString)</signal>
<receiver>dap_combo_box</receiver>
<slot>select_x_axis(QString)</slot>
<hints>
<hint type="sourcelabel">
<x>214</x>
<y>130</y>
</hint>
<hint type="destinationlabel">
<x>629</x>
<y>130</y>
</hint>
</hints>
</connection>
<connection>
<sender>device_combobox_2</sender>
<signal>currentTextChanged(QString)</signal>
<receiver>dap_combo_box</receiver>
<slot>select_y_axis(QString)</slot>
<hints>
<hint type="sourcelabel">
<x>388</x>
<y>130</y>
</hint>
<hint type="destinationlabel">
<x>629</x>
<y>130</y>
</hint>
</hints>
</connection>
<connection>
<sender>dap_combo_box</sender>
<signal>new_dap_config(QString,QString,QString)</signal>
<receiver>bec_waveform_widget</receiver>
<slot>add_dap(QString,QString,QString)</slot>
<hints>
<hint type="sourcelabel">
<x>629</x>
<y>130</y>
</hint>
<hint type="destinationlabel">
<x>1099</x>
<y>221</y>
</hint>
</hints>
</connection>
<connection>
<sender>device_combobox</sender>
<signal>currentTextChanged(QString)</signal>
<receiver>positioner_control_line</receiver>
<slot>set_positioner(QString)</slot>
<hints>
<hint type="sourcelabel">
<x>214</x>
<y>130</y>
</hint>
<hint type="destinationlabel">
<x>174</x>
<y>550</y>
</hint>
</hints>
</connection>
<connection>
<sender>scan_button</sender>
<signal>clicked()</signal>
<receiver>scan_control</receiver>
<slot>run_scan()</slot>
<hints>
<hint type="sourcelabel">
<x>332</x>
<y>336</y>
</hint>
<hint type="destinationlabel">
<x>28</x>
<y>319</y>
</hint>
</hints>
</connection>
<connection>
<sender>device_combobox_2</sender>
<signal>currentTextChanged(QString)</signal>
<receiver>bec_waveform_widget</receiver>
<slot>plot(QString)</slot>
<hints>
<hint type="sourcelabel">
<x>388</x>
<y>130</y>
</hint>
<hint type="destinationlabel">
<x>1099</x>
<y>201</y>
</hint>
</hints>
</connection>
<connection>
<sender>device_combobox</sender>
<signal>currentTextChanged(QString)</signal>
<receiver>bec_waveform_widget</receiver>
<slot>set_x(QString)</slot>
<hints>
<hint type="sourcelabel">
<x>214</x>
<y>130</y>
</hint>
<hint type="destinationlabel">
<x>1099</x>
<y>258</y>
</hint>
</hints>
</connection>
<connection>
<sender>toggle_switch</sender>
<signal>enabled(bool)</signal>
<receiver>bec_waveform_widget</receiver>
<slot>toogle_roi_select(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>691</x>
<y>536</y>
</hint>
<hint type="destinationlabel">
<x>1099</x>
<y>96</y>
</hint>
</hints>
</connection>
<connection>
<sender>bec_waveform_widget</sender>
<signal>dap_summary_update(QVariantMap,QVariantMap)</signal>
<receiver>lm_fit_dialog</receiver>
<slot>update_summary_tree(QVariantMap,QVariantMap)</slot>
<hints>
<hint type="sourcelabel">
<x>1099</x>
<y>258</y>
</hint>
<hint type="destinationlabel">
<x>1098</x>
<y>668</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -0,0 +1,151 @@
"""Module to create an arrow item for a pyqtgraph plot"""
import pyqtgraph as pg
from qtpy.QtCore import QObject, Qt, Signal, Slot
from bec_widgets.utils.colors import get_accent_colors
class BECIndicatorItem(QObject):
def __init__(self, plot_item: pg.PlotItem = None, parent=None):
super().__init__(parent=parent)
self.accent_colors = get_accent_colors()
self.plot_item = plot_item
def add_to_plot(self) -> None:
"""Add the item to the plot"""
raise NotImplementedError("Method add_to_plot not implemented")
def remove_from_plot(self) -> None:
"""Remove the item from the plot"""
raise NotImplementedError("Method remove_from_plot not implemented")
def set_position(self, pos: tuple[float, float] | float) -> None:
"""Set the position of the item"""
raise NotImplementedError("Method set_position not implemented")
def cleanup(self) -> None:
"""Cleanup the item"""
self.remove_from_plot()
self.deleteLater()
class BECTickItem(BECIndicatorItem):
"""Class to create a tick item which can be added to a pyqtgraph plot.
The tick item will be added to the layout of the plot item and can be used to indicate
a position"""
position_changed = Signal(float)
position_changed_str = Signal(str)
def __init__(self, plot_item: pg.PlotItem = None, parent=None):
super().__init__(plot_item=plot_item, parent=parent)
self.tick_item = pg.TickSliderItem(allowAdd=False, allowRemove=False)
self.tick = None
self._tick_pos = 0
self._range = [0, 1]
@Slot(float)
def set_position(self, pos: float) -> None:
"""Set the position of the tick item
Args:
pos (float): The position of the tick item.
"""
self._tick_pos = pos
self.update_range(self.plot_item.vb, self._range)
def update_range(self, vb, viewRange) -> None:
"""Update the range of the tick item
Args:
vb (pg.ViewBox): The view box.
viewRange (tuple): The view range.
"""
origin = self.tick_item.tickSize / 2.0
length = self.tick_item.length
lengthIncludingPadding = length + self.tick_item.tickSize + 2
self._range = viewRange
tickValueIncludingPadding = (self._tick_pos - viewRange[0]) / (viewRange[1] - viewRange[0])
tickValue = (tickValueIncludingPadding * lengthIncludingPadding - origin) / length
self.tick_item.setTickValue(self.tick, tickValue)
self.position_changed.emit(self._tick_pos)
self.position_changed_str.emit(str(self._tick_pos))
def add_to_plot(self):
"""Add the tick item to the view box or plot item."""
if self.plot_item is not None:
self.plot_item.layout.addItem(self.tick_item, 4, 1)
self.tick = self.tick_item.addTick(0, movable=False, color=self.accent_colors.highlight)
self.plot_item.vb.sigXRangeChanged.connect(self.update_range)
def remove_from_plot(self):
"""Remove the tick item from the view box or plot item."""
if self.plot_item is not None:
self.plot_item.layout.removeItem(self.tick_item)
class BECArrowItem(BECIndicatorItem):
"""Class to create an arrow item which can be added to a pyqtgraph plot.
It can be either added directly to a view box or a plot item.
To add the arrow item to a view box or plot item, use the add_to_plot method.
Args:
view_box (pg.ViewBox | pg.PlotItem): The view box or plot item to which the arrow item should be added.
parent (QObject): The parent object.
Signals:
position_changed (tuple[float, float]): Signal emitted when the position of the arrow item has changed.
position_changed_str (tuple[str, str]): Signal emitted when the position of the arrow item has changed.
"""
# Signal to emit if the position of the arrow item has changed
position_changed = Signal(tuple)
position_changed_str = Signal(tuple)
def __init__(self, plot_item: pg.PlotItem = None, parent=None):
super().__init__(plot_item=plot_item, parent=parent)
self.arrow_item = pg.ArrowItem()
@Slot(dict)
def set_style(self, style: dict) -> None:
"""Set the style of the arrow item
Args:
style (dict): The style of the arrow item. Dictionary with key,
value pairs which are accepted from the pg.ArrowItem.setStyle method.
"""
self.arrow_item.setStyle(**style)
@Slot(tuple)
def set_position(self, pos: tuple[float, float]) -> None:
"""Set the position of the arrow item
Args:
pos (tuple): The position of the arrow item as a tuple (x, y).
"""
pos_x = pos[0]
pos_y = pos[1]
self.arrow_item.setPos(pos_x, pos_y)
self.position_changed.emit((pos_x, pos_y))
self.position_changed_str.emit((str(pos_x), str(pos_y)))
def add_to_plot(self):
"""Add the arrow item to the view box or plot item."""
self.arrow_item.setStyle(
angle=-90,
pen=pg.mkPen(self.accent_colors.emergency, width=1),
brush=pg.mkBrush(self.accent_colors.highlight),
headLen=20,
)
if self.plot_item is not None:
self.plot_item.addItem(self.arrow_item)
def remove_from_plot(self):
"""Remove the arrow item from the view box or plot item."""
if self.plot_item is not None:
self.plot_item.removeItem(self.arrow_item)

View File

@ -12,6 +12,7 @@ from qtpy.QtWidgets import QApplication, QWidget
from bec_widgets.utils import BECConnector, ConnectionConfig
from bec_widgets.utils.crosshair import Crosshair
from bec_widgets.utils.plot_indicator_items import BECArrowItem, BECTickItem
logger = bec_logger.logger
@ -105,6 +106,8 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout):
self.add_legend()
self.crosshair = None
self.motor_pos_tick = BECTickItem(parent=self, plot_item=self.plot_item)
self.arrow_item = BECArrowItem(parent=self, plot_item=self.plot_item)
self._connect_to_theme_change()
def _connect_to_theme_change(self):
@ -428,6 +431,8 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout):
def cleanup_pyqtgraph(self):
"""Cleanup pyqtgraph items."""
self.unhook_crosshair()
self.motor_pos_tick.cleanup()
self.arrow_item.cleanup()
item = self.plot_item
item.vb.menu.close()
item.vb.menu.deleteLater()

View File

@ -33,6 +33,8 @@ class PositionerBox(BECWidget, QWidget):
ICON_NAME = "switch_right"
USER_ACCESS = ["set_positioner"]
device_changed = Signal(str, str)
# Signal emitted to inform listeners about a position update
position_update = Signal(float)
def __init__(self, parent=None, device: Positioner = None, **kwargs):
"""Initialize the PositionerBox widget.
@ -245,6 +247,7 @@ class PositionerBox(BECWidget, QWidget):
if readback_val is not None:
self.ui.readback.setText(f"{readback_val:.{precision}f}")
self.position_update.emit(readback_val)
if setpoint_val is not None:
self.ui.setpoint.setText(f"{setpoint_val:.{precision}f}")

View File

@ -0,0 +1,90 @@
import pyqtgraph as pg
import pytest
from bec_qthemes._main import AccentColors
from bec_widgets.utils.plot_indicator_items import BECArrowItem, BECTickItem
@pytest.fixture(scope="function")
def arrow_item():
"""Fixture for the BECArrowItem class"""
item = BECArrowItem(plot_item=pg.PlotItem())
yield item
def test_arrow_item_add_to_plot(arrow_item):
"""Test the add_to_plot method"""
assert arrow_item.plot_item is not None
assert arrow_item.plot_item.items == []
arrow_item.accent_colors = AccentColors(theme="dark")
arrow_item.add_to_plot()
assert arrow_item.plot_item.items == [arrow_item.arrow_item]
def test_arrow_item_remove_to_plot(arrow_item):
"""Test the remove_from_plot method"""
arrow_item.accent_colors = AccentColors(theme="dark")
arrow_item.add_to_plot()
assert arrow_item.plot_item.items == [arrow_item.arrow_item]
arrow_item.remove_from_plot()
assert arrow_item.plot_item.items == []
def test_arrow_item_set_position(arrow_item):
"""Test the set_position method"""
container = []
def signal_callback(tup: tuple):
container.append(tup)
arrow_item.accent_colors = AccentColors(theme="dark")
arrow_item.add_to_plot()
arrow_item.position_changed.connect(signal_callback)
arrow_item.set_position(pos=(1, 1))
assert arrow_item.arrow_item.pos().toTuple() == (1, 1)
arrow_item.set_position(pos=(2, 2))
assert arrow_item.arrow_item.pos().toTuple() == (2, 2)
assert container == [(1, 1), (2, 2)]
@pytest.fixture(scope="function")
def tick_item():
"""Fixture for the BECArrowItem class"""
item = BECTickItem(plot_item=pg.PlotItem())
yield item
def test_tick_item_add_to_plot(tick_item):
"""Test the add_to_plot method"""
assert tick_item.plot_item is not None
assert tick_item.plot_item.items == []
tick_item.accent_colors = AccentColors(theme="dark")
tick_item.add_to_plot()
assert tick_item.plot_item.layout.itemAt(4, 1) == tick_item.tick_item
def test_tick_item_remove_to_plot(tick_item):
"""Test the remove_from_plot method"""
tick_item.accent_colors = AccentColors(theme="dark")
tick_item.add_to_plot()
assert tick_item.plot_item.layout.itemAt(4, 1) == tick_item.tick_item
tick_item.remove_from_plot()
assert tick_item.plot_item.layout.itemAt(4, 1) is None
def test_tick_item_set_position(tick_item):
"""Test the set_position method"""
container = []
def signal_callback(val: float):
container.append(val)
tick_item.accent_colors = AccentColors(theme="dark")
tick_item.add_to_plot()
tick_item.position_changed.connect(signal_callback)
tick_item.set_position(pos=1)
assert tick_item._tick_pos == 1
tick_item.set_position(pos=2)
assert tick_item._tick_pos == 2
assert container == [1.0, 2.0]