0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-14 11:41:49 +02:00

fix: compatibility adjustment to .ui loading and tests for PySide6

This commit is contained in:
2024-04-23 23:21:08 +02:00
parent 0fea8d6065
commit 07b99d91a5
19 changed files with 219 additions and 1131 deletions

View File

@ -1,307 +0,0 @@
import json
import os
import threading
import h5py
import numpy as np
import pyqtgraph as pg
import zmq
from pyqtgraph.Qt import uic
from qtpy.QtCore import Signal as pyqtSignal
from qtpy.QtCore import Slot as pyqtSlot
from qtpy.QtGui import QKeySequence
from qtpy.QtWidgets import QDialog, QFileDialog, QFrame, QLabel, QShortcut, QVBoxLayout, QWidget
# from scipy.stats import multivariate_normal
class EigerPlot(QWidget):
update_signal = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
# pg.setConfigOptions(background="w", foreground="k", antialias=True)
current_path = os.path.dirname(__file__)
uic.loadUi(os.path.join(current_path, "eiger_plot.ui"), self)
# Set widow name
self.setWindowTitle("Eiger Plot")
self.hist_lims = None
self.mask = None
self.image = None
# UI
self.init_ui()
self.hook_signals()
self.key_bindings()
# ZMQ Consumer
self._zmq_consumer_exit_event = threading.Event()
self._zmq_consumer_thread = self.start_zmq_consumer()
def close(self):
super().close()
self._zmq_consumer_exit_event.set()
self._zmq_consumer_thread.join()
def init_ui(self):
# Create Plot and add ImageItem
self.plot_item = pg.PlotItem()
self.plot_item.setAspectLocked(True)
self.imageItem = pg.ImageItem()
self.plot_item.addItem(self.imageItem)
# Setting up histogram
self.hist = pg.HistogramLUTItem()
self.hist.setImageItem(self.imageItem)
self.hist.gradient.loadPreset("magma")
self.update_hist()
# Adding Items to Graphical Layout
self.glw.addItem(self.plot_item)
self.glw.addItem(self.hist)
def hook_signals(self):
# Buttons
# self.pushButton_test.clicked.connect(self.start_sim_stream)
self.pushButton_mask.clicked.connect(self.load_mask_dialog)
self.pushButton_delete_mask.clicked.connect(self.delete_mask)
self.pushButton_help.clicked.connect(self.show_help_dialog)
# SpinBoxes
self.doubleSpinBox_hist_min.valueChanged.connect(self.update_hist)
self.doubleSpinBox_hist_max.valueChanged.connect(self.update_hist)
# Signal/Slots
self.update_signal.connect(self.on_image_update)
def key_bindings(self):
# Key bindings for rotation
rotate_plus = QShortcut(QKeySequence("Ctrl+A"), self)
rotate_minus = QShortcut(QKeySequence("Ctrl+Z"), self)
self.comboBox_rotation.setToolTip("Increase rotation: Ctrl+A\nDecrease rotation: Ctrl+Z")
self.checkBox_transpose.setToolTip("Toggle transpose: Ctrl+T")
max_index = self.comboBox_rotation.count() - 1 # Maximum valid index
rotate_plus.activated.connect(
lambda: self.comboBox_rotation.setCurrentIndex(
min(self.comboBox_rotation.currentIndex() + 1, max_index)
)
)
rotate_minus.activated.connect(
lambda: self.comboBox_rotation.setCurrentIndex(
max(self.comboBox_rotation.currentIndex() - 1, 0)
)
)
# Key bindings for transpose
transpose = QShortcut(QKeySequence("Ctrl+T"), self)
transpose.activated.connect(self.checkBox_transpose.toggle)
FFT = QShortcut(QKeySequence("Ctrl+F"), self)
FFT.activated.connect(self.checkBox_FFT.toggle)
self.checkBox_FFT.setToolTip("Toggle FFT: Ctrl+F")
log = QShortcut(QKeySequence("Ctrl+L"), self)
log.activated.connect(self.checkBox_log.toggle)
self.checkBox_log.setToolTip("Toggle log: Ctrl+L")
mask = QShortcut(QKeySequence("Ctrl+M"), self)
mask.activated.connect(self.pushButton_mask.click)
self.pushButton_mask.setToolTip("Load mask: Ctrl+M")
delete_mask = QShortcut(QKeySequence("Ctrl+D"), self)
delete_mask.activated.connect(self.pushButton_delete_mask.click)
self.pushButton_delete_mask.setToolTip("Delete mask: Ctrl+D")
def update_hist(self):
self.hist_levels = [
self.doubleSpinBox_hist_min.value(),
self.doubleSpinBox_hist_max.value(),
]
self.hist.setLevels(min=self.hist_levels[0], max=self.hist_levels[1])
self.hist.setHistogramRange(
self.hist_levels[0] - 0.1 * self.hist_levels[0],
self.hist_levels[1] + 0.1 * self.hist_levels[1],
)
def load_mask_dialog(self):
options = QFileDialog.Options()
options |= QFileDialog.ReadOnly
file_name, _ = QFileDialog.getOpenFileName(
self, "Select Mask File", "", "H5 Files (*.h5);;All Files (*)", options=options
)
if file_name:
self.load_mask(file_name)
def load_mask(self, path):
try:
with h5py.File(path, "r") as f:
self.mask = f["data"][...]
if self.mask is not None:
# Set label to mask name without path
self.label_mask.setText(os.path.basename(path))
except KeyError as e:
# Update GUI with the error message
print(f"Error: {str(e)}")
def delete_mask(self):
self.mask = None
self.label_mask.setText("No Mask")
@pyqtSlot()
def on_image_update(self):
# TODO first rotate then transpose
if self.mask is not None:
# self.image = np.ma.masked_array(self.image, mask=self.mask) #TODO test if np works
self.image = self.image * (1 - self.mask) + 1
if self.checkBox_FFT.isChecked():
self.image = np.abs(np.fft.fftshift(np.fft.fft2(self.image)))
if self.comboBox_rotation.currentIndex() > 0: # rotate
self.image = np.rot90(self.image, k=self.comboBox_rotation.currentIndex(), axes=(0, 1))
if self.checkBox_transpose.isChecked(): # transpose
self.image = np.transpose(self.image)
if self.checkBox_log.isChecked():
self.image = np.log10(self.image)
self.imageItem.setImage(self.image, autoLevels=False)
###############################
# ZMQ Consumer
###############################
def start_zmq_consumer(self):
consumer_thread = threading.Thread(
target=self.zmq_consumer, args=(self._zmq_consumer_exit_event,), daemon=True
)
consumer_thread.start()
return consumer_thread
def zmq_consumer(self, exit_event):
print("starting consumer")
live_stream_url = "tcp://129.129.95.38:20000"
receiver = zmq.Context().socket(zmq.SUB)
receiver.connect(live_stream_url)
receiver.setsockopt_string(zmq.SUBSCRIBE, "")
poller = zmq.Poller()
poller.register(receiver, zmq.POLLIN)
# code could be a bit simpler here, testing exit_event in
# 'while' condition, but like this it is easier for the
# 'test_zmq_consumer' test
while True:
if poller.poll(1000): # 1s timeout
raw_meta, raw_data = receiver.recv_multipart(zmq.NOBLOCK)
meta = json.loads(raw_meta.decode("utf-8"))
self.image = np.frombuffer(raw_data, dtype=meta["type"]).reshape(meta["shape"])
self.update_signal.emit()
if exit_event.is_set():
break
receiver.disconnect(live_stream_url)
###############################
# just simulations from here
###############################
def show_help_dialog(self):
dialog = QDialog(self)
dialog.setWindowTitle("Help")
layout = QVBoxLayout()
# Key bindings section
layout.addWidget(QLabel("Keyboard Shortcuts:"))
key_bindings = [
("Ctrl+A", "Increase rotation"),
("Ctrl+Z", "Decrease rotation"),
("Ctrl+T", "Toggle transpose"),
("Ctrl+F", "Toggle FFT"),
("Ctrl+L", "Toggle log scale"),
("Ctrl+M", "Load mask"),
("Ctrl+D", "Delete mask"),
]
for keys, action in key_bindings:
layout.addWidget(QLabel(f"{keys} - {action}"))
# Separator
separator = QFrame()
separator.setFrameShape(QFrame.HLine)
separator.setFrameShadow(QFrame.Sunken)
layout.addWidget(separator)
# Histogram section
layout.addWidget(QLabel("Histogram:"))
layout.addWidget(
QLabel(
"Use the Double Spin Boxes to adjust the minimum and maximum values of the histogram."
)
)
# Another Separator
another_separator = QFrame()
another_separator.setFrameShape(QFrame.HLine)
another_separator.setFrameShadow(QFrame.Sunken)
layout.addWidget(another_separator)
# Mask section
layout.addWidget(QLabel("Mask:"))
layout.addWidget(
QLabel(
"Use 'Load Mask' to load a mask from an H5 file. 'Delete Mask' removes the current mask."
)
)
dialog.setLayout(layout)
dialog.exec()
###############################
# just simulations from here
###############################
# def start_sim_stream(self):
# sim_stream_thread = threading.Thread(target=self.sim_stream, daemon=True)
# sim_stream_thread.start()
#
# def sim_stream(self):
# for i in range(100):
# # Generate 100x100 image of random noise
# self.image = np.random.rand(100, 100) * 0.2
#
# # Define Gaussian parameters
# x, y = np.mgrid[0:50, 0:50]
# pos = np.dstack((x, y))
#
# # Center at (25, 25) longer along y-axis
# rv = multivariate_normal(mean=[25, 25], cov=[[25, 0], [0, 80]])
#
# # Generate Gaussian in the first quadrant
# gaussian_quadrant = rv.pdf(pos) * 40
#
# # Place Gaussian in the first quadrant
# self.image[0:50, 0:50] += gaussian_quadrant * 10
#
# self.update_signal.emit()
# time.sleep(0.1)
if __name__ == "__main__":
import sys
from qtpy.QtWidgets import QApplication
app = QApplication(sys.argv)
plot = EigerPlot()
plot.show()
sys.exit(app.exec())

View File

@ -1,207 +0,0 @@
<?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>874</width>
<height>762</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Plot Control</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Histogram MIN</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QDoubleSpinBox" name="doubleSpinBox_hist_min">
<property name="minimum">
<double>-100000.000000000000000</double>
</property>
<property name="maximum">
<double>100000.000000000000000</double>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Histogram MAX</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QDoubleSpinBox" name="doubleSpinBox_hist_max">
<property name="minimum">
<double>-100000.000000000000000</double>
</property>
<property name="maximum">
<double>100000.000000000000000</double>
</property>
<property name="value">
<double>2.000000000000000</double>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Data Control</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QCheckBox" name="checkBox_FFT">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>FFT</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_log">
<property name="text">
<string>log</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_mask">
<property name="text">
<string>Load Mask</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_delete_mask">
<property name="text">
<string>Delete Mask</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>Orientation</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="2" column="1">
<widget class="QComboBox" name="comboBox_rotation">
<item>
<property name="text">
<string>0</string>
</property>
</item>
<item>
<property name="text">
<string>90</string>
</property>
</item>
<item>
<property name="text">
<string>180</string>
</property>
</item>
<item>
<property name="text">
<string>270</string>
</property>
</item>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Rotation</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="2">
<widget class="QCheckBox" name="checkBox_transpose">
<property name="text">
<string>Transpose</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_4">
<property name="title">
<string>Help</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QLabel" name="label_mask">
<property name="text">
<string>No Mask</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_help">
<property name="text">
<string>Help</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="GraphicsLayoutWidget" name="glw"/>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>GraphicsLayoutWidget</class>
<extends>QGraphicsView</extends>
<header>pyqtgraph.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@ -2,7 +2,7 @@ import os
import numpy as np import numpy as np
import pyqtgraph as pg import pyqtgraph as pg
from pyqtgraph.Qt import QtWidgets, uic from pyqtgraph.Qt import QtWidgets
from qtconsole.inprocess import QtInProcessKernelManager from qtconsole.inprocess import QtInProcessKernelManager
from qtconsole.rich_jupyter_widget import RichJupyterWidget from qtconsole.rich_jupyter_widget import RichJupyterWidget
from qtpy.QtCore import QSize from qtpy.QtCore import QSize
@ -10,7 +10,7 @@ from qtpy.QtGui import QIcon
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
from bec_widgets.cli.rpc_register import RPCRegister from bec_widgets.cli.rpc_register import RPCRegister
from bec_widgets.utils import BECDispatcher from bec_widgets.utils import BECDispatcher, UILoader
from bec_widgets.widgets import BECFigure from bec_widgets.widgets import BECFigure
from bec_widgets.widgets.dock.dock_area import BECDockArea from bec_widgets.widgets.dock.dock_area import BECDockArea
from bec_widgets.widgets.spiral_progress_bar.spiral_progress_bar import SpiralProgressBar from bec_widgets.widgets.spiral_progress_bar.spiral_progress_bar import SpiralProgressBar
@ -40,11 +40,11 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
super().__init__(parent) super().__init__(parent)
current_path = os.path.dirname(__file__) current_path = os.path.dirname(__file__)
uic.loadUi(os.path.join(current_path, "jupyter_console_window.ui"), self) self.ui = UILoader().load_ui(os.path.join(current_path, "jupyter_console_window.ui"), self)
self._init_ui() self._init_ui()
self.splitter.setSizes([200, 100]) self.ui.splitter.setSizes([200, 100])
self.safe_close = False self.safe_close = False
# self.figure.clean_signal.connect(self.confirm_close) # self.figure.clean_signal.connect(self.confirm_close)
@ -75,11 +75,11 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
def _init_ui(self): def _init_ui(self):
# Plotting window # Plotting window
self.glw_1_layout = QVBoxLayout(self.glw) # Create a new QVBoxLayout self.glw_1_layout = QVBoxLayout(self.ui.glw) # Create a new QVBoxLayout
self.figure = BECFigure(parent=self, gui_id="remote") # Create a new BECDeviceMonitor self.figure = BECFigure(parent=self, gui_id="remote") # Create a new BECDeviceMonitor
self.glw_1_layout.addWidget(self.figure) # Add BECDeviceMonitor to the layout self.glw_1_layout.addWidget(self.figure) # Add BECDeviceMonitor to the layout
self.dock_layout = QVBoxLayout(self.dock_placeholder) self.dock_layout = QVBoxLayout(self.ui.dock_placeholder)
self.dock = BECDockArea(gui_id="remote") self.dock = BECDockArea(gui_id="remote")
self.dock_layout.addWidget(self.dock) self.dock_layout.addWidget(self.dock)
@ -89,7 +89,7 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
# init dock for testing # init dock for testing
self._init_dock() self._init_dock()
self.console_layout = QVBoxLayout(self.widget_console) self.console_layout = QVBoxLayout(self.ui.widget_console)
self.console = JupyterConsoleWidget() self.console = JupyterConsoleWidget()
self.console_layout.addWidget(self.console) self.console_layout.addWidget(self.console)
self.console.set_default_style("linux") self.console.set_default_style("linux")

View File

@ -1,92 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1433</width>
<height>689</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="2">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Plot Config 2</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="BECMonitor" name="plot_1"/>
</item>
<item row="1" column="3">
<widget class="QPushButton" name="pushButton_setting_2">
<property name="text">
<string>Setting Plot 2</string>
</property>
</widget>
</item>
<item row="3" column="2" colspan="2">
<widget class="BECMonitor" name="plot_2"/>
</item>
<item row="1" column="4">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Plot Scan Types = True</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="pushButton_setting_1">
<property name="text">
<string>Setting Plot 1</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Plot Config 1</string>
</property>
</widget>
</item>
<item row="1" column="5">
<widget class="QPushButton" name="pushButton_setting_3">
<property name="text">
<string>Setting Plot 3</string>
</property>
</widget>
</item>
<item row="3" column="4" colspan="2">
<widget class="BECMonitor" name="plot_3"/>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1433</width>
<height>37</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
</widget>
<customwidgets>
<customwidget>
<class>BECMonitor</class>
<extends>QGraphicsView</extends>
<header location="global">bec_widgets.widgets.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@ -1,197 +0,0 @@
import os
from qtpy import uic
from qtpy.QtWidgets import QApplication, QMainWindow
from bec_widgets.utils.bec_dispatcher import BECDispatcher
from bec_widgets.widgets import BECMonitor
# some default configs for demonstration purposes
CONFIG_SIMPLE = {
"plot_settings": {
"background_color": "black",
"num_columns": 2,
"colormap": "plasma",
"scan_types": False,
},
"plot_data": [
{
"plot_name": "BPM4i plots vs samx",
"x_label": "Motor X",
"y_label": "bpm4i",
"sources": [
{
"type": "scan_segment",
"signals": {
"x": [{"name": "samx"}],
"y": [{"name": "bpm4i", "entry": "bpm4i"}],
},
},
# {
# "type": "history",
# "signals": {
# "x": [{"name": "samx"}],
# "y": [{"name": "bpm4i", "entry": "bpm4i"}],
# },
# },
# {
# "type": "dap",
# 'worker':'some_worker',
# "signals": {
# "x": [{"name": "samx"}],
# "y": [{"name": "bpm4i", "entry": "bpm4i"}],
# },
# },
],
},
{
"plot_name": "Gauss plots vs samx",
"x_label": "Motor X",
"y_label": "Gauss",
"sources": [
{
"type": "scan_segment",
"signals": {
"x": [{"name": "samx", "entry": "samx"}],
"y": [{"name": "gauss_bpm"}, {"name": "gauss_adc1"}],
},
}
],
},
],
}
CONFIG_SCAN_MODE = {
"plot_settings": {
"background_color": "white",
"num_columns": 3,
"colormap": "plasma",
"scan_types": True,
},
"plot_data": {
"grid_scan": [
{
"plot_name": "Grid plot 1",
"x_label": "Motor X",
"y_label": "BPM",
"sources": [
{
"type": "scan_segment",
"signals": {
"x": [{"name": "samx", "entry": "samx"}],
"y": [{"name": "gauss_bpm"}],
},
}
],
},
{
"plot_name": "Grid plot 2",
"x_label": "Motor X",
"y_label": "BPM",
"sources": [
{
"type": "scan_segment",
"signals": {
"x": [{"name": "samx", "entry": "samx"}],
"y": [{"name": "gauss_adc1"}],
},
}
],
},
{
"plot_name": "Grid plot 3",
"x_label": "Motor X",
"y_label": "BPM",
"sources": [
{
"type": "scan_segment",
"signals": {"x": [{"name": "samy"}], "y": [{"name": "gauss_adc2"}]},
}
],
},
{
"plot_name": "Grid plot 4",
"x_label": "Motor X",
"y_label": "BPM",
"sources": [
{
"type": "scan_segment",
"signals": {
"x": [{"name": "samy", "entry": "samy"}],
"y": [{"name": "gauss_adc3"}],
},
}
],
},
],
"line_scan": [
{
"plot_name": "BPM plots vs samx",
"x_label": "Motor X",
"y_label": "Gauss",
"sources": [
{
"type": "scan_segment",
"signals": {
"x": [{"name": "samx", "entry": "samx"}],
"y": [{"name": "bpm4i"}],
},
}
],
},
{
"plot_name": "Gauss plots vs samx",
"x_label": "Motor X",
"y_label": "Gauss",
"sources": [
{
"type": "scan_segment",
"signals": {
"x": [{"name": "samx", "entry": "samx"}],
"y": [{"name": "gauss_bpm"}, {"name": "gauss_adc1"}],
},
}
],
},
],
},
}
class ModularApp(QMainWindow):
def __init__(self, client=None, parent=None):
super(ModularApp, self).__init__(parent)
# Client and device manager from BEC
self.client = BECDispatcher().client if client is None else client
# Loading UI
current_path = os.path.dirname(__file__)
uic.loadUi(os.path.join(current_path, "modular.ui"), self)
self._init_plots()
def _init_plots(self):
"""Initialize plots and connect the buttons to the config dialogs"""
plots = [self.plot_1, self.plot_2, self.plot_3]
configs = [CONFIG_SIMPLE, CONFIG_SCAN_MODE, CONFIG_SCAN_MODE]
buttons = [self.pushButton_setting_1, self.pushButton_setting_2, self.pushButton_setting_3]
# hook plots, configs and buttons together
for plot, config, button in zip(plots, configs, buttons):
plot.on_config_update(config)
button.clicked.connect(plot.show_config_dialog)
if __name__ == "__main__":
# BECclient global variables
client = BECDispatcher().client
client.start()
app = QApplication([])
modularApp = ModularApp(client=client)
window = modularApp
window.show()
app.exec()

View File

@ -151,7 +151,7 @@ class MotorControlPanel(QWidget):
self.selection_widget.selected_motors_signal.connect(self.absolute_widget.change_motors) self.selection_widget.selected_motors_signal.connect(self.absolute_widget.change_motors)
# Set the window to a fixed size based on its contents # Set the window to a fixed size based on its contents
self.layout().setSizeConstraint(layout.SetFixedSize) # self.layout().setSizeConstraint(layout.SetFixedSize)
class MotorControlPanelAbsolute(QWidget): class MotorControlPanelAbsolute(QWidget):
@ -178,9 +178,6 @@ class MotorControlPanelAbsolute(QWidget):
# Connecting signals and slots # Connecting signals and slots
self.selection_widget.selected_motors_signal.connect(self.absolute_widget.change_motors) self.selection_widget.selected_motors_signal.connect(self.absolute_widget.change_motors)
# Set the window to a fixed size based on its contents
self.layout().setSizeConstraint(layout.SetFixedSize)
class MotorControlPanelRelative(QWidget): class MotorControlPanelRelative(QWidget):
def __init__(self, parent=None, client=None, config=None): def __init__(self, parent=None, client=None, config=None):
@ -206,9 +203,6 @@ class MotorControlPanelRelative(QWidget):
# Connecting signals and slots # Connecting signals and slots
self.selection_widget.selected_motors_signal.connect(self.relative_widget.change_motors) self.selection_widget.selected_motors_signal.connect(self.relative_widget.change_motors)
# Set the window to a fixed size based on its contents
self.layout().setSizeConstraint(layout.SetFixedSize)
if __name__ == "__main__": # pragma: no cover if __name__ == "__main__": # pragma: no cover
import argparse import argparse

View File

@ -29,10 +29,10 @@
<property name="orientation"> <property name="orientation">
<enum>Qt::Vertical</enum> <enum>Qt::Vertical</enum>
</property> </property>
<widget class="GraphicsLayoutWidget" name="glw_plot"/> <widget class="QWidget" name="glw_plot_placeholder" native="true"/>
<widget class="GraphicsLayoutWidget" name="glw_image"/> <widget class="QWidget" name="glw_image_placeholder" native="true"/>
</widget> </widget>
<widget class="QWidget" name=""> <widget class="QWidget" name="layoutWidget">
<layout class="QVBoxLayout" name="verticalLayout" stretch="1,1,1,15"> <layout class="QVBoxLayout" name="verticalLayout" stretch="1,1,1,15">
<item> <item>
<widget class="QPushButton" name="pushButton_generate"> <widget class="QPushButton" name="pushButton_generate">
@ -143,13 +143,6 @@
</item> </item>
</layout> </layout>
</widget> </widget>
<customwidgets>
<customwidget>
<class>GraphicsLayoutWidget</class>
<extends>QGraphicsView</extends>
<header>pyqtgraph.h</header>
</customwidget>
</customwidgets>
<resources/> <resources/>
<connections/> <connections/>
</ui> </ui>

View File

@ -9,18 +9,17 @@ from bec_lib import messages
from bec_lib.endpoints import MessageEndpoints from bec_lib.endpoints import MessageEndpoints
from bec_lib.redis_connector import RedisConnector from bec_lib.redis_connector import RedisConnector
from pyqtgraph import mkBrush, mkPen from pyqtgraph import mkBrush, mkPen
from pyqtgraph.Qt import QtCore, QtWidgets, uic from pyqtgraph.Qt import QtCore, QtWidgets
from pyqtgraph.Qt.QtCore import pyqtSignal from qtpy.QtCore import Signal, Slot
from qtpy.QtCore import Slot as pyqtSlot from qtpy.QtWidgets import QTableWidgetItem, QVBoxLayout
from qtpy.QtWidgets import QTableWidgetItem
from bec_widgets.utils import Colors, Crosshair from bec_widgets.utils import Colors, Crosshair, UILoader
from bec_widgets.utils.bec_dispatcher import BECDispatcher from bec_widgets.utils.bec_dispatcher import BECDispatcher
class StreamPlot(QtWidgets.QWidget): class StreamPlot(QtWidgets.QWidget):
update_signal = pyqtSignal() update_signal = Signal()
roi_signal = pyqtSignal(tuple) roi_signal = Signal(tuple)
def __init__(self, name="", y_value_list=["gauss_bpm"], client=None, parent=None) -> None: def __init__(self, name="", y_value_list=["gauss_bpm"], client=None, parent=None) -> None:
""" """
@ -39,7 +38,7 @@ class StreamPlot(QtWidgets.QWidget):
pg.setConfigOption("background", "w") pg.setConfigOption("background", "w")
pg.setConfigOption("foreground", "k") pg.setConfigOption("foreground", "k")
current_path = os.path.dirname(__file__) current_path = os.path.dirname(__file__)
uic.loadUi(os.path.join(current_path, "line_plot.ui"), self) self.ui = UILoader().load_ui(os.path.join(current_path, "line_plot.ui"), self)
self._idle_time = 100 self._idle_time = 100
self.connector = RedisConnector(["localhost:6379"]) self.connector = RedisConnector(["localhost:6379"])
@ -82,6 +81,9 @@ class StreamPlot(QtWidgets.QWidget):
# LabelItem for ROI # LabelItem for ROI
self.label_plot = pg.LabelItem(justify="center") self.label_plot = pg.LabelItem(justify="center")
self.glw_plot_layout = QVBoxLayout(self.ui.glw_plot_placeholder)
self.glw_plot = pg.GraphicsLayoutWidget()
self.glw_plot_layout.addWidget(self.glw_plot)
self.glw_plot.addItem(self.label_plot) self.glw_plot.addItem(self.label_plot)
self.label_plot.setText("ROI region") self.label_plot.setText("ROI region")
@ -112,6 +114,9 @@ class StreamPlot(QtWidgets.QWidget):
# Label for coordinates moved # Label for coordinates moved
self.label_image_moved = pg.LabelItem(justify="center") self.label_image_moved = pg.LabelItem(justify="center")
self.glw_image_layout = QVBoxLayout(self.ui.glw_image_placeholder)
self.glw_image = pg.GraphicsLayoutWidget()
self.glw_plot_layout.addWidget(self.glw_image)
self.glw_image.addItem(self.label_image_moved) self.glw_image.addItem(self.label_image_moved)
self.label_image_moved.setText("Actual coordinates (X, Y)") self.label_image_moved.setText("Actual coordinates (X, Y)")
@ -221,10 +226,10 @@ class StreamPlot(QtWidgets.QWidget):
def init_table(self): def init_table(self):
# Init number of rows in table according to n of devices # Init number of rows in table according to n of devices
self.cursor_table.setRowCount(len(self.y_value_list)) self.ui.cursor_table.setRowCount(len(self.y_value_list))
# self.table.setHorizontalHeaderLabels(["(X, Y) - Moved", "(X, Y) - Clicked"]) #TODO can be dynamic # self.table.setHorizontalHeaderLabels(["(X, Y) - Moved", "(X, Y) - Clicked"]) #TODO can be dynamic
self.cursor_table.setVerticalHeaderLabels(self.y_value_list) self.ui.cursor_table.setVerticalHeaderLabels(self.y_value_list)
self.cursor_table.resizeColumnsToContents() self.ui.cursor_table.resizeColumnsToContents()
def update_table(self, table_widget, x, y_values): def update_table(self, table_widget, x, y_values):
for i, y in enumerate(y_values): for i, y in enumerate(y_values):
@ -287,13 +292,13 @@ class StreamPlot(QtWidgets.QWidget):
self.update_signal.emit() self.update_signal.emit()
@pyqtSlot(dict, dict) @Slot(dict, dict)
def on_dap_update(self, data: dict, metadata: dict): def on_dap_update(self, data: dict, metadata: dict):
flipped_data = self.flip_even_rows(data["data"]["z"]) flipped_data = self.flip_even_rows(data["data"]["z"])
self.img.setImage(flipped_data) self.img.setImage(flipped_data)
@pyqtSlot(dict, dict) @Slot(dict, dict)
def new_proj(self, content: dict, _metadata: dict): def new_proj(self, content: dict, _metadata: dict):
proj_nr = content["signals"]["proj_nr"] proj_nr = content["signals"]["proj_nr"]
endpoint = f"px_stream/projection_{proj_nr}/metadata" endpoint = f"px_stream/projection_{proj_nr}/metadata"

View File

@ -8,13 +8,13 @@ from qtpy.QtCore import Signal as pyqtSignal
class Crosshair(QObject): class Crosshair(QObject):
# Signal for 1D plot # Signal for 1D plot
coordinatesChanged1D = pyqtSignal(float, list) coordinatesChanged1D = pyqtSignal(tuple)
coordinatesClicked1D = pyqtSignal(float, list) coordinatesClicked1D = pyqtSignal(tuple)
# Signal for 2D plot # Signal for 2D plot
coordinatesChanged2D = pyqtSignal(float, float) coordinatesChanged2D = pyqtSignal(tuple)
coordinatesClicked2D = pyqtSignal(float, float) coordinatesClicked2D = pyqtSignal(tuple)
def __init__(self, plot_item: pg.PlotItem, precision: int = None, parent=None): def __init__(self, plot_item: pg.PlotItem, precision: int = 3, parent=None):
""" """
Crosshair for 1D and 2D plots. Crosshair for 1D and 2D plots.
@ -174,10 +174,11 @@ class Crosshair(QObject):
if isinstance(item, pg.PlotDataItem): if isinstance(item, pg.PlotDataItem):
if x is None or all(v is None for v in y_values): if x is None or all(v is None for v in y_values):
return return
self.coordinatesChanged1D.emit( coordinate_to_emit = (
round(x, self.precision), round(x, self.precision),
[round(y_val, self.precision) for y_val in y_values], [round(y_val, self.precision) for y_val in y_values],
) )
self.coordinatesChanged1D.emit(coordinate_to_emit)
for i, y_val in enumerate(y_values): for i, y_val in enumerate(y_values):
self.marker_moved_1d[i].setData( self.marker_moved_1d[i].setData(
[x if not self.is_log_x else np.log10(x)], [x if not self.is_log_x else np.log10(x)],
@ -186,7 +187,8 @@ class Crosshair(QObject):
elif isinstance(item, pg.ImageItem): elif isinstance(item, pg.ImageItem):
if x is None or y_values is None: if x is None or y_values is None:
return return
self.coordinatesChanged2D.emit(x, y_values) coordinate_to_emit = (x, y_values)
self.coordinatesChanged2D.emit(coordinate_to_emit)
def mouse_clicked(self, event): def mouse_clicked(self, event):
"""Handles the mouse clicked event, updating the crosshair position and emitting signals. """Handles the mouse clicked event, updating the crosshair position and emitting signals.
@ -209,10 +211,11 @@ class Crosshair(QObject):
if isinstance(item, pg.PlotDataItem): if isinstance(item, pg.PlotDataItem):
if x is None or all(v is None for v in y_values): if x is None or all(v is None for v in y_values):
return return
self.coordinatesClicked1D.emit( coordinate_to_emit = (
round(x, self.precision), round(x, self.precision),
[round(y_val, self.precision) for y_val in y_values], [round(y_val, self.precision) for y_val in y_values],
) )
self.coordinatesClicked1D.emit(coordinate_to_emit)
for i, y_val in enumerate(y_values): for i, y_val in enumerate(y_values):
for marker in self.marker_clicked_1d[i]: for marker in self.marker_clicked_1d[i]:
marker.setData( marker.setData(
@ -222,7 +225,8 @@ class Crosshair(QObject):
elif isinstance(item, pg.ImageItem): elif isinstance(item, pg.ImageItem):
if x is None or y_values is None: if x is None or y_values is None:
return return
self.coordinatesClicked2D.emit(x, y_values) coordinate_to_emit = (x, y_values)
self.coordinatesClicked2D.emit(coordinate_to_emit)
self.marker_2d.setPos([x, y_values]) self.marker_2d.setPos([x, y_values])
def check_log(self): def check_log(self):

View File

@ -16,6 +16,7 @@ from qtpy.QtWidgets import (
QTableWidgetItem, QTableWidgetItem,
) )
from bec_widgets.utils import UILoader
from bec_widgets.widgets.motor_control.motor_control import MotorControlWidget from bec_widgets.widgets.motor_control.motor_control import MotorControlWidget
@ -37,25 +38,25 @@ class MotorCoordinateTable(MotorControlWidget):
def _load_ui(self): def _load_ui(self):
"""Load the UI for the coordinate table.""" """Load the UI for the coordinate table."""
current_path = os.path.dirname(__file__) current_path = os.path.dirname(__file__)
uic.loadUi(os.path.join(current_path, "motor_table.ui"), self) self.ui = UILoader().load_ui(os.path.join(current_path, "motor_table.ui"), self)
def _init_ui(self): def _init_ui(self):
"""Initialize the UI""" """Initialize the UI"""
# Setup table behaviour # Setup table behaviour
self._setup_table() self._setup_table()
self.table.setSelectionBehavior(QTableWidget.SelectRows) self.ui.table.setSelectionBehavior(QTableWidget.SelectRows)
# for tag columns default tag # for tag columns default tag
self.tag_counter = 1 self.tag_counter = 1
# Connect signals and slots # Connect signals and slots
self.checkBox_resize_auto.stateChanged.connect(self.resize_table_auto) self.ui.checkBox_resize_auto.stateChanged.connect(self.resize_table_auto)
self.comboBox_mode.currentIndexChanged.connect(self.mode_switch) self.ui.comboBox_mode.currentIndexChanged.connect(self.mode_switch)
# Keyboard shortcuts for deleting a row # Keyboard shortcuts for deleting a row
self.delete_shortcut = QShortcut(QKeySequence(Qt.Key_Delete), self.table) self.delete_shortcut = QShortcut(QKeySequence(Qt.Key_Delete), self.ui.table)
self.delete_shortcut.activated.connect(self.delete_selected_row) self.delete_shortcut.activated.connect(self.delete_selected_row)
self.backspace_shortcut = QShortcut(QKeySequence(Qt.Key_Backspace), self.table) self.backspace_shortcut = QShortcut(QKeySequence(Qt.Key_Backspace), self.ui.table)
self.backspace_shortcut.activated.connect(self.delete_selected_row) self.backspace_shortcut.activated.connect(self.delete_selected_row)
# Warning message for mode switch enable/disable # Warning message for mode switch enable/disable
@ -83,13 +84,13 @@ class MotorCoordinateTable(MotorControlWidget):
self.mode = self.config["motor_control"].get("mode", "Individual") self.mode = self.config["motor_control"].get("mode", "Individual")
# Set combobox to default mode # Set combobox to default mode
self.comboBox_mode.setCurrentText(self.mode) self.ui.comboBox_mode.setCurrentText(self.mode)
self._init_ui() self._init_ui()
def _setup_table(self): def _setup_table(self):
"""Setup the table with appropriate headers and configurations.""" """Setup the table with appropriate headers and configurations."""
mode = self.comboBox_mode.currentText() mode = self.ui.comboBox_mode.currentText()
if mode == "Individual": if mode == "Individual":
self._setup_individual_mode() self._setup_individual_mode()
@ -101,14 +102,14 @@ class MotorCoordinateTable(MotorControlWidget):
def _setup_individual_mode(self): def _setup_individual_mode(self):
"""Setup the table for individual mode.""" """Setup the table for individual mode."""
self.table.setColumnCount(5) self.ui.table.setColumnCount(5)
self.table.setHorizontalHeaderLabels(["Show", "Move", "Tag", "X", "Y"]) self.ui.table.setHorizontalHeaderLabels(["Show", "Move", "Tag", "X", "Y"])
self.table.verticalHeader().setVisible(False) self.ui.table.verticalHeader().setVisible(False)
def _setup_start_stop_mode(self): def _setup_start_stop_mode(self):
"""Setup the table for start/stop mode.""" """Setup the table for start/stop mode."""
self.table.setColumnCount(8) self.ui.table.setColumnCount(8)
self.table.setHorizontalHeaderLabels( self.ui.table.setHorizontalHeaderLabels(
[ [
"Show", "Show",
"Move [start]", "Move [start]",
@ -120,15 +121,15 @@ class MotorCoordinateTable(MotorControlWidget):
"Y [end]", "Y [end]",
] ]
) )
self.table.verticalHeader().setVisible(False) self.ui.table.verticalHeader().setVisible(False)
# Set flag to track if the coordinate is stat or the end of the entry # Set flag to track if the coordinate is stat or the end of the entry
self.is_next_entry_end = False self.is_next_entry_end = False
def mode_switch(self): def mode_switch(self):
"""Switch between individual and start/stop mode.""" """Switch between individual and start/stop mode."""
last_selected_index = self.comboBox_mode.currentIndex() last_selected_index = self.ui.comboBox_mode.currentIndex()
if self.table.rowCount() > 0 and self.warning_message is True: if self.ui.table.rowCount() > 0 and self.warning_message is True:
msgBox = QMessageBox() msgBox = QMessageBox()
msgBox.setIcon(QMessageBox.Critical) msgBox.setIcon(QMessageBox.Critical)
msgBox.setText( msgBox.setText(
@ -138,9 +139,9 @@ class MotorCoordinateTable(MotorControlWidget):
returnValue = msgBox.exec() returnValue = msgBox.exec()
if returnValue is QMessageBox.Cancel: if returnValue is QMessageBox.Cancel:
self.comboBox_mode.blockSignals(True) # Block signals self.ui.comboBox_mode.blockSignals(True) # Block signals
self.comboBox_mode.setCurrentIndex(last_selected_index) self.ui.comboBox_mode.setCurrentIndex(last_selected_index)
self.comboBox_mode.blockSignals(False) # Unblock signals self.ui.comboBox_mode.blockSignals(False) # Unblock signals
return return
# Wipe table # Wipe table
@ -170,7 +171,7 @@ class MotorCoordinateTable(MotorControlWidget):
y(float): Y coordinate. y(float): Y coordinate.
""" """
mode = self.comboBox_mode.currentText() mode = self.ui.comboBox_mode.currentText()
if mode == "Individual": if mode == "Individual":
checkbox_pos = 0 checkbox_pos = 0
button_pos = 1 button_pos = 1
@ -181,8 +182,8 @@ class MotorCoordinateTable(MotorControlWidget):
color = "green" color = "green"
# Add new row -> new entry # Add new row -> new entry
row_count = self.table.rowCount() row_count = self.ui.table.rowCount()
self.table.insertRow(row_count) self.ui.table.insertRow(row_count)
# Add Widgets # Add Widgets
self._add_widgets( self._add_widgets(
@ -213,8 +214,8 @@ class MotorCoordinateTable(MotorControlWidget):
color = "blue" color = "blue"
# Add new row -> new entry # Add new row -> new entry
row_count = self.table.rowCount() row_count = self.ui.table.rowCount()
self.table.insertRow(row_count) self.ui.table.insertRow(row_count)
# Add Widgets # Add Widgets
self._add_widgets( self._add_widgets(
@ -236,7 +237,7 @@ class MotorCoordinateTable(MotorControlWidget):
elif self.is_next_entry_end is True: # It is the end position of the entry elif self.is_next_entry_end is True: # It is the end position of the entry
print("End position") print("End position")
row_count = self.table.rowCount() - 1 # Current row row_count = self.ui.table.rowCount() - 1 # Current row
button_pos = 2 button_pos = 2
x_pos = 6 x_pos = 6
y_pos = 7 y_pos = 7
@ -294,7 +295,7 @@ class MotorCoordinateTable(MotorControlWidget):
# Add widgets # Add widgets
self._add_checkbox(row, checkBox_pos, x_pos, y_pos) self._add_checkbox(row, checkBox_pos, x_pos, y_pos)
self._add_move_button(row, button_pos, x_pos, y_pos) self._add_move_button(row, button_pos, x_pos, y_pos)
self.table.setItem(row, tag_pos, QTableWidgetItem(tag)) self.ui.table.setItem(row, tag_pos, QTableWidgetItem(tag))
self._add_line_edit(x, row, x_pos, x_pos, y_pos, coordinate_reference, color) self._add_line_edit(x, row, x_pos, x_pos, y_pos, coordinate_reference, color)
self._add_line_edit(y, row, y_pos, x_pos, y_pos, coordinate_reference, color) self._add_line_edit(y, row, y_pos, x_pos, y_pos, coordinate_reference, color)
@ -302,10 +303,10 @@ class MotorCoordinateTable(MotorControlWidget):
self.emit_plot_coordinates(x_pos, y_pos, coordinate_reference, color) self.emit_plot_coordinates(x_pos, y_pos, coordinate_reference, color)
# Connect item edit to emit coordinates # Connect item edit to emit coordinates
self.table.itemChanged.connect( self.ui.table.itemChanged.connect(
lambda: print(f"item changed from {coordinate_reference} slot \n {x}-{y}-{color}") lambda: print(f"item changed from {coordinate_reference} slot \n {x}-{y}-{color}")
) )
self.table.itemChanged.connect( self.ui.table.itemChanged.connect(
lambda: self.emit_plot_coordinates(x_pos, y_pos, coordinate_reference, color) lambda: self.emit_plot_coordinates(x_pos, y_pos, coordinate_reference, color)
) )
@ -321,7 +322,7 @@ class MotorCoordinateTable(MotorControlWidget):
show_checkbox = QCheckBox() show_checkbox = QCheckBox()
show_checkbox.setChecked(True) show_checkbox.setChecked(True)
show_checkbox.stateChanged.connect(lambda: self.emit_plot_coordinates(x_pos, y_pos)) show_checkbox.stateChanged.connect(lambda: self.emit_plot_coordinates(x_pos, y_pos))
self.table.setCellWidget(row, checkBox_pos, show_checkbox) self.ui.table.setCellWidget(row, checkBox_pos, show_checkbox)
def _add_move_button(self, row: int, button_pos: int, x_pos: int, y_pos: int) -> None: def _add_move_button(self, row: int, button_pos: int, x_pos: int, y_pos: int) -> None:
""" """
@ -334,7 +335,7 @@ class MotorCoordinateTable(MotorControlWidget):
""" """
move_button = QPushButton("Move") move_button = QPushButton("Move")
move_button.clicked.connect(lambda: self.handle_move_button_click(x_pos, y_pos)) move_button.clicked.connect(lambda: self.handle_move_button_click(x_pos, y_pos))
self.table.setCellWidget(row, button_pos, move_button) self.ui.table.setCellWidget(row, button_pos, move_button)
def _add_line_edit( def _add_line_edit(
self, self,
@ -367,7 +368,7 @@ class MotorCoordinateTable(MotorControlWidget):
edit.setAlignment(Qt.AlignmentFlag.AlignCenter) edit.setAlignment(Qt.AlignmentFlag.AlignCenter)
# Add line edit to the table # Add line edit to the table
self.table.setCellWidget(row, line_pos, edit) self.ui.table.setCellWidget(row, line_pos, edit)
edit.textChanged.connect( edit.textChanged.connect(
lambda: self.emit_plot_coordinates(x_pos, y_pos, coordinate_reference, color) lambda: self.emit_plot_coordinates(x_pos, y_pos, coordinate_reference, color)
) )
@ -375,10 +376,10 @@ class MotorCoordinateTable(MotorControlWidget):
def wipe_motor_map_coordinates(self): def wipe_motor_map_coordinates(self):
"""Wipe the motor map coordinates.""" """Wipe the motor map coordinates."""
try: try:
self.table.itemChanged.disconnect() # Disconnect all previous connections self.ui.table.itemChanged.disconnect() # Disconnect all previous connections
except TypeError: except TypeError:
print("No previous connections to disconnect") print("No previous connections to disconnect")
self.table.setRowCount(0) self.ui.table.setRowCount(0)
reference_tags = ["Individual", "Start", "Stop"] reference_tags = ["Individual", "Start", "Stop"]
for reference_tag in reference_tags: for reference_tag in reference_tags:
self.plot_coordinates_signal.emit([], reference_tag, "green") self.plot_coordinates_signal.emit([], reference_tag, "green")
@ -391,7 +392,7 @@ class MotorCoordinateTable(MotorControlWidget):
y_pos(int): Y position of the coordinate. y_pos(int): Y position of the coordinate.
""" """
button = self.sender() button = self.sender()
row = self.table.indexAt(button.pos()).row() row = self.ui.table.indexAt(button.pos()).row()
x = self.get_coordinate(row, x_pos) x = self.get_coordinate(row, x_pos)
y = self.get_coordinate(row, y_pos) y = self.get_coordinate(row, y_pos)
@ -410,8 +411,8 @@ class MotorCoordinateTable(MotorControlWidget):
f"Emitting plot coordinates: x_pos={x_pos}, y_pos={y_pos}, reference_tag={reference_tag}, color={color}" f"Emitting plot coordinates: x_pos={x_pos}, y_pos={y_pos}, reference_tag={reference_tag}, color={color}"
) )
coordinates = [] coordinates = []
for row in range(self.table.rowCount()): for row in range(self.ui.table.rowCount()):
show = self.table.cellWidget(row, 0).isChecked() show = self.ui.table.cellWidget(row, 0).isChecked()
x = self.get_coordinate(row, x_pos) x = self.get_coordinate(row, x_pos)
y = self.get_coordinate(row, y_pos) y = self.get_coordinate(row, y_pos)
@ -427,27 +428,27 @@ class MotorCoordinateTable(MotorControlWidget):
Returns: Returns:
float: Value of the coordinate. float: Value of the coordinate.
""" """
edit = self.table.cellWidget(row, column) edit = self.ui.table.cellWidget(row, column)
value = float(edit.text()) if edit and edit.text() != "" else None value = float(edit.text()) if edit and edit.text() != "" else None
if value: if value:
return value return value
def delete_selected_row(self): def delete_selected_row(self):
"""Delete the selected row from the table.""" """Delete the selected row from the table."""
selected_rows = self.table.selectionModel().selectedRows() selected_rows = self.ui.table.selectionModel().selectedRows()
for row in selected_rows: for row in selected_rows:
self.table.removeRow(row.row()) self.ui.table.removeRow(row.row())
if self.comboBox_mode.currentText() == "Start/Stop": if self.ui.comboBox_mode.currentText() == "Start/Stop":
self.emit_plot_coordinates(x_pos=4, y_pos=5, reference_tag="Start", color="blue") self.emit_plot_coordinates(x_pos=4, y_pos=5, reference_tag="Start", color="blue")
self.emit_plot_coordinates(x_pos=6, y_pos=7, reference_tag="Stop", color="red") self.emit_plot_coordinates(x_pos=6, y_pos=7, reference_tag="Stop", color="red")
self.is_next_entry_end = False self.is_next_entry_end = False
elif self.comboBox_mode.currentText() == "Individual": elif self.ui.comboBox_mode.currentText() == "Individual":
self.emit_plot_coordinates(x_pos=3, y_pos=4, reference_tag="Individual", color="green") self.emit_plot_coordinates(x_pos=3, y_pos=4, reference_tag="Individual", color="green")
def resize_table_auto(self): def resize_table_auto(self):
"""Resize the table to fit the contents.""" """Resize the table to fit the contents."""
if self.checkBox_resize_auto.isChecked(): if self.ui.checkBox_resize_auto.isChecked():
self.table.resizeColumnsToContents() self.ui.table.resizeColumnsToContents()
def move_motor(self, x: float, y: float) -> None: def move_motor(self, x: float, y: float) -> None:
""" """

View File

@ -3,8 +3,10 @@ import os
from qtpy import uic from qtpy import uic
from qtpy.QtCore import Signal as pyqtSignal from qtpy.QtCore import Signal as pyqtSignal
from qtpy.QtCore import Slot as pyqtSlot from qtpy.QtCore import Slot as pyqtSlot
from qtpy.QtWidgets import QWidget
from bec_widgets.widgets.motor_control.motor_control import MotorControlWidget from bec_widgets.utils import UILoader
from bec_widgets.widgets.motor_control.motor_control import MotorControlErrors, MotorControlWidget
class MotorControlAbsolute(MotorControlWidget): class MotorControlAbsolute(MotorControlWidget):
@ -23,26 +25,26 @@ class MotorControlAbsolute(MotorControlWidget):
def _load_ui(self): def _load_ui(self):
"""Load the UI from the .ui file.""" """Load the UI from the .ui file."""
current_path = os.path.dirname(__file__) current_path = os.path.dirname(__file__)
uic.loadUi(os.path.join(current_path, "movement_absolute.ui"), self) self.ui = UILoader().load_ui(os.path.join(current_path, "movement_absolute.ui"), self)
def _init_ui(self): def _init_ui(self):
"""Initialize the UI.""" """Initialize the UI."""
# Check if there are any motors connected # Check if there are any motors connected
if self.motor_x is None or self.motor_y is None: if self.motor_x is None or self.motor_y is None:
self.motorControl_absolute.setEnabled(False) self.ui.motorControl_absolute.setEnabled(False)
return return
# Move to absolute coordinates # Move to absolute coordinates
self.pushButton_go_absolute.clicked.connect( self.ui.pushButton_go_absolute.clicked.connect(
lambda: self.move_motor_absolute( lambda: self.move_motor_absolute(
self.spinBox_absolute_x.value(), self.spinBox_absolute_y.value() self.ui.spinBox_absolute_x.value(), self.ui.spinBox_absolute_y.value()
) )
) )
self.pushButton_set.clicked.connect(self.save_absolute_coordinates) self.ui.pushButton_set.clicked.connect(self.save_absolute_coordinates)
self.pushButton_save.clicked.connect(self.save_current_coordinates) self.ui.pushButton_save.clicked.connect(self.save_current_coordinates)
self.pushButton_stop.clicked.connect(self.motor_thread.stop_movement) self.ui.pushButton_stop.clicked.connect(self.motor_thread.stop_movement)
# Enable/Disable GUI # Enable/Disable GUI
self.motor_thread.lock_gui.connect(self.enable_motor_controls) self.motor_thread.lock_gui.connect(self.enable_motor_controls)
@ -80,11 +82,11 @@ class MotorControlAbsolute(MotorControlWidget):
""" """
# Disable or enable all controls within the motorControl_absolute group box # Disable or enable all controls within the motorControl_absolute group box
for widget in self.motorControl_absolute.findChildren(QWidget): for widget in self.ui.motorControl_absolute.findChildren(QWidget):
widget.setEnabled(enable) widget.setEnabled(enable)
# Enable the pushButton_stop if the motor is moving # Enable the pushButton_stop if the motor is moving
self.pushButton_stop.setEnabled(True) self.ui.pushButton_stop.setEnabled(True)
@pyqtSlot(str, str) @pyqtSlot(str, str)
def change_motors(self, motor_x: str, motor_y: str): def change_motors(self, motor_x: str, motor_y: str):
@ -109,8 +111,8 @@ class MotorControlAbsolute(MotorControlWidget):
""" """
self.precision = precision self.precision = precision
self.config["motor_control"]["precision"] = precision self.config["motor_control"]["precision"] = precision
self.spinBox_absolute_x.setDecimals(precision) self.ui.spinBox_absolute_x.setDecimals(precision)
self.spinBox_absolute_y.setDecimals(precision) self.ui.spinBox_absolute_y.setDecimals(precision)
def move_motor_absolute(self, x: float, y: float) -> None: def move_motor_absolute(self, x: float, y: float) -> None:
""" """
@ -122,32 +124,32 @@ class MotorControlAbsolute(MotorControlWidget):
# self._enable_motor_controls(False) # self._enable_motor_controls(False)
target_coordinates = (x, y) target_coordinates = (x, y)
self.motor_thread.move_absolute(self.motor_x, self.motor_y, target_coordinates) self.motor_thread.move_absolute(self.motor_x, self.motor_y, target_coordinates)
if self.checkBox_save_with_go.isChecked(): if self.ui.checkBox_save_with_go.isChecked():
self.save_absolute_coordinates() self.save_absolute_coordinates()
def _init_keyboard_shortcuts(self): def _init_keyboard_shortcuts(self):
"""Initialize the keyboard shortcuts.""" """Initialize the keyboard shortcuts."""
# Go absolute button # Go absolute button
self.pushButton_go_absolute.setShortcut("Ctrl+G") self.ui.pushButton_go_absolute.setShortcut("Ctrl+G")
self.pushButton_go_absolute.setToolTip("Ctrl+G") self.ui.pushButton_go_absolute.setToolTip("Ctrl+G")
# Set absolute coordinates # Set absolute coordinates
self.pushButton_set.setShortcut("Ctrl+D") self.ui.pushButton_set.setShortcut("Ctrl+D")
self.pushButton_set.setToolTip("Ctrl+D") self.ui.pushButton_set.setToolTip("Ctrl+D")
# Save Current coordinates # Save Current coordinates
self.pushButton_save.setShortcut("Ctrl+S") self.ui.pushButton_save.setShortcut("Ctrl+S")
self.pushButton_save.setToolTip("Ctrl+S") self.ui.pushButton_save.setToolTip("Ctrl+S")
# Stop Button # Stop Button
self.pushButton_stop.setShortcut("Ctrl+X") self.ui.pushButton_stop.setShortcut("Ctrl+X")
self.pushButton_stop.setToolTip("Ctrl+X") self.ui.pushButton_stop.setToolTip("Ctrl+X")
def save_absolute_coordinates(self): def save_absolute_coordinates(self):
"""Emit the setup coordinates from the spinboxes""" """Emit the setup coordinates from the spinboxes"""
x, y = round(self.spinBox_absolute_x.value(), self.precision), round( x, y = round(self.ui.spinBox_absolute_x.value(), self.precision), round(
self.spinBox_absolute_y.value(), self.precision self.ui.spinBox_absolute_y.value(), self.precision
) )
self.coordinates_signal.emit((x, y)) self.coordinates_signal.emit((x, y))

View File

@ -7,6 +7,7 @@ from qtpy.QtCore import Slot as pyqtSlot
from qtpy.QtGui import QKeySequence from qtpy.QtGui import QKeySequence
from qtpy.QtWidgets import QDoubleSpinBox, QShortcut, QWidget from qtpy.QtWidgets import QDoubleSpinBox, QShortcut, QWidget
from bec_widgets.utils import UILoader
from bec_widgets.widgets.motor_control.motor_control import MotorControlWidget from bec_widgets.widgets.motor_control.motor_control import MotorControlWidget
@ -27,7 +28,7 @@ class MotorControlRelative(MotorControlWidget):
"""Load the UI from the .ui file.""" """Load the UI from the .ui file."""
# Loading UI # Loading UI
current_path = os.path.dirname(__file__) current_path = os.path.dirname(__file__)
uic.loadUi(os.path.join(current_path, "movement_relative.ui"), self) self.ui = UILoader().load_ui(os.path.join(current_path, "movement_relative.ui"), self)
def _init_ui(self): def _init_ui(self):
"""Initialize the UI.""" """Initialize the UI."""
@ -51,15 +52,15 @@ class MotorControlRelative(MotorControlWidget):
# Update step precision # Update step precision
self.precision = self.config["motor_control"]["precision"] self.precision = self.config["motor_control"]["precision"]
self.spinBox_precision.setValue(self.precision) self.ui.spinBox_precision.setValue(self.precision)
# Update step sizes # Update step sizes
self.spinBox_step_x.setValue(self.config["motor_control"]["step_size_x"]) self.ui.spinBox_step_x.setValue(self.config["motor_control"]["step_size_x"])
self.spinBox_step_y.setValue(self.config["motor_control"]["step_size_y"]) self.ui.spinBox_step_y.setValue(self.config["motor_control"]["step_size_y"])
# Checkboxes for keyboard shortcuts and x/y step size link # Checkboxes for keyboard shortcuts and x/y step size link
self.checkBox_same_xy.setChecked(self.config["motor_control"]["step_x_y_same"]) self.ui.checkBox_same_xy.setChecked(self.config["motor_control"]["step_x_y_same"])
self.checkBox_enableArrows.setChecked(self.config["motor_control"]["move_with_arrows"]) self.ui.checkBox_enableArrows.setChecked(self.config["motor_control"]["move_with_arrows"])
self._init_ui() self._init_ui()
@ -67,30 +68,32 @@ class MotorControlRelative(MotorControlWidget):
"""Initialize the motor control elements""" """Initialize the motor control elements"""
# Connect checkbox and spinBoxes # Connect checkbox and spinBoxes
self.checkBox_same_xy.stateChanged.connect(self._sync_step_sizes) self.ui.checkBox_same_xy.stateChanged.connect(self._sync_step_sizes)
self.spinBox_step_x.valueChanged.connect(self._update_step_size_x) self.ui.spinBox_step_x.valueChanged.connect(self._update_step_size_x)
self.spinBox_step_y.valueChanged.connect(self._update_step_size_y) self.ui.spinBox_step_y.valueChanged.connect(self._update_step_size_y)
self.toolButton_right.clicked.connect( self.ui.toolButton_right.clicked.connect(
lambda: self.move_motor_relative(self.motor_x, "x", 1) lambda: self.move_motor_relative(self.motor_x, "x", 1)
) )
self.toolButton_left.clicked.connect( self.ui.toolButton_left.clicked.connect(
lambda: self.move_motor_relative(self.motor_x, "x", -1) lambda: self.move_motor_relative(self.motor_x, "x", -1)
) )
self.toolButton_up.clicked.connect(lambda: self.move_motor_relative(self.motor_y, "y", 1)) self.ui.toolButton_up.clicked.connect(
self.toolButton_down.clicked.connect( lambda: self.move_motor_relative(self.motor_y, "y", 1)
)
self.ui.toolButton_down.clicked.connect(
lambda: self.move_motor_relative(self.motor_y, "y", -1) lambda: self.move_motor_relative(self.motor_y, "y", -1)
) )
# Switch between key shortcuts active # Switch between key shortcuts active
self.checkBox_enableArrows.stateChanged.connect(self._update_arrow_key_shortcuts) self.ui.checkBox_enableArrows.stateChanged.connect(self._update_arrow_key_shortcuts)
self._update_arrow_key_shortcuts() self._update_arrow_key_shortcuts()
# Enable/Disable GUI # Enable/Disable GUI
self.motor_thread.lock_gui.connect(self.enable_motor_controls) self.motor_thread.lock_gui.connect(self.enable_motor_controls)
# Precision update # Precision update
self.spinBox_precision.valueChanged.connect(lambda x: self._update_precision(x)) self.ui.spinBox_precision.valueChanged.connect(lambda x: self._update_precision(x))
# Error messages # Error messages
self.motor_thread.motor_error.connect( self.motor_thread.motor_error.connect(
@ -98,7 +101,7 @@ class MotorControlRelative(MotorControlWidget):
) )
# Stop Button # Stop Button
self.pushButton_stop.clicked.connect(self.motor_thread.stop_movement) self.ui.pushButton_stop.clicked.connect(self.motor_thread.stop_movement)
def _init_keyboard_shortcuts(self) -> None: def _init_keyboard_shortcuts(self) -> None:
"""Initialize the keyboard shortcuts""" """Initialize the keyboard shortcuts"""
@ -107,42 +110,42 @@ class MotorControlRelative(MotorControlWidget):
increase_x_shortcut = QShortcut(QKeySequence("Ctrl+A"), self) increase_x_shortcut = QShortcut(QKeySequence("Ctrl+A"), self)
decrease_x_shortcut = QShortcut(QKeySequence("Ctrl+Z"), self) decrease_x_shortcut = QShortcut(QKeySequence("Ctrl+Z"), self)
increase_x_shortcut.activated.connect( increase_x_shortcut.activated.connect(
lambda: self._change_step_size(self.spinBox_step_x, 2) lambda: self._change_step_size(self.ui.spinBox_step_x, 2)
) )
decrease_x_shortcut.activated.connect( decrease_x_shortcut.activated.connect(
lambda: self._change_step_size(self.spinBox_step_x, 0.5) lambda: self._change_step_size(self.ui.spinBox_step_x, 0.5)
) )
self.spinBox_step_x.setToolTip("Increase step size: Ctrl+A\nDecrease step size: Ctrl+Z") self.ui.spinBox_step_x.setToolTip("Increase step size: Ctrl+A\nDecrease step size: Ctrl+Z")
# Increase/decrease step size for Y motor # Increase/decrease step size for Y motor
increase_y_shortcut = QShortcut(QKeySequence("Alt+A"), self) increase_y_shortcut = QShortcut(QKeySequence("Alt+A"), self)
decrease_y_shortcut = QShortcut(QKeySequence("Alt+Z"), self) decrease_y_shortcut = QShortcut(QKeySequence("Alt+Z"), self)
increase_y_shortcut.activated.connect( increase_y_shortcut.activated.connect(
lambda: self._change_step_size(self.spinBox_step_y, 2) lambda: self._change_step_size(self.ui.spinBox_step_y, 2)
) )
decrease_y_shortcut.activated.connect( decrease_y_shortcut.activated.connect(
lambda: self._change_step_size(self.spinBox_step_y, 0.5) lambda: self._change_step_size(self.ui.spinBox_step_y, 0.5)
) )
self.spinBox_step_y.setToolTip("Increase step size: Alt+A\nDecrease step size: Alt+Z") self.ui.spinBox_step_y.setToolTip("Increase step size: Alt+A\nDecrease step size: Alt+Z")
# Stop Button # Stop Button
self.pushButton_stop.setShortcut("Ctrl+X") self.ui.pushButton_stop.setShortcut("Ctrl+X")
self.pushButton_stop.setToolTip("Ctrl+X") self.ui.pushButton_stop.setToolTip("Ctrl+X")
def _update_arrow_key_shortcuts(self) -> None: def _update_arrow_key_shortcuts(self) -> None:
"""Update the arrow key shortcuts based on the checkbox state.""" """Update the arrow key shortcuts based on the checkbox state."""
if self.checkBox_enableArrows.isChecked(): if self.ui.checkBox_enableArrows.isChecked():
# Set the arrow key shortcuts for motor movement # Set the arrow key shortcuts for motor movement
self.toolButton_right.setShortcut(Qt.Key_Right) self.ui.toolButton_right.setShortcut(Qt.Key_Right)
self.toolButton_left.setShortcut(Qt.Key_Left) self.ui.toolButton_left.setShortcut(Qt.Key_Left)
self.toolButton_up.setShortcut(Qt.Key_Up) self.ui.toolButton_up.setShortcut(Qt.Key_Up)
self.toolButton_down.setShortcut(Qt.Key_Down) self.ui.toolButton_down.setShortcut(Qt.Key_Down)
else: else:
# Clear the shortcuts # Clear the shortcuts
self.toolButton_right.setShortcut("") self.ui.toolButton_right.setShortcut("")
self.toolButton_left.setShortcut("") self.ui.toolButton_left.setShortcut("")
self.toolButton_up.setShortcut("") self.ui.toolButton_up.setShortcut("")
self.toolButton_down.setShortcut("") self.ui.toolButton_down.setShortcut("")
def _update_precision(self, precision: int) -> None: def _update_precision(self, precision: int) -> None:
""" """
@ -150,8 +153,8 @@ class MotorControlRelative(MotorControlWidget):
Args: Args:
precision(int): Precision of the coordinates. precision(int): Precision of the coordinates.
""" """
self.spinBox_step_x.setDecimals(precision) self.ui.spinBox_step_x.setDecimals(precision)
self.spinBox_step_y.setDecimals(precision) self.ui.spinBox_step_y.setDecimals(precision)
self.precision_signal.emit(precision) self.precision_signal.emit(precision)
def _change_step_size(self, spinBox: QDoubleSpinBox, factor: float) -> None: def _change_step_size(self, spinBox: QDoubleSpinBox, factor: float) -> None:
@ -167,21 +170,21 @@ class MotorControlRelative(MotorControlWidget):
def _sync_step_sizes(self): def _sync_step_sizes(self):
"""Sync step sizes based on checkbox state.""" """Sync step sizes based on checkbox state."""
if self.checkBox_same_xy.isChecked(): if self.ui.checkBox_same_xy.isChecked():
value = self.spinBox_step_x.value() value = self.ui.spinBox_step_x.value()
self.spinBox_step_y.setValue(value) self.ui.spinBox_step_y.setValue(value)
def _update_step_size_x(self): def _update_step_size_x(self):
"""Update step size for x if checkbox is checked.""" """Update step size for x if checkbox is checked."""
if self.checkBox_same_xy.isChecked(): if self.ui.checkBox_same_xy.isChecked():
value = self.spinBox_step_x.value() value = self.ui.spinBox_step_x.value()
self.spinBox_step_y.setValue(value) self.ui.spinBox_step_y.setValue(value)
def _update_step_size_y(self): def _update_step_size_y(self):
"""Update step size for y if checkbox is checked.""" """Update step size for y if checkbox is checked."""
if self.checkBox_same_xy.isChecked(): if self.ui.checkBox_same_xy.isChecked():
value = self.spinBox_step_y.value() value = self.ui.spinBox_step_y.value()
self.spinBox_step_x.setValue(value) self.ui.spinBox_step_x.setValue(value)
@pyqtSlot(str, str) @pyqtSlot(str, str)
def change_motors(self, motor_x: str, motor_y: str): def change_motors(self, motor_x: str, motor_y: str):
@ -206,11 +209,11 @@ class MotorControlRelative(MotorControlWidget):
""" """
# Disable or enable all controls within the motorControl_absolute group box # Disable or enable all controls within the motorControl_absolute group box
for widget in self.motorControl.findChildren(QWidget): for widget in self.ui.motorControl.findChildren(QWidget):
widget.setEnabled(disable) widget.setEnabled(disable)
# Enable the pushButton_stop if the motor is moving # Enable the pushButton_stop if the motor is moving
self.pushButton_stop.setEnabled(True) self.ui.pushButton_stop.setEnabled(True)
def move_motor_relative(self, motor, axis: str, direction: int) -> None: def move_motor_relative(self, motor, axis: str, direction: int) -> None:
""" """
@ -221,7 +224,7 @@ class MotorControlRelative(MotorControlWidget):
direction(int): Direction to move. 1 for positive, -1 for negative. direction(int): Direction to move. 1 for positive, -1 for negative.
""" """
if axis == "x": if axis == "x":
step = direction * self.spinBox_step_x.value() step = direction * self.ui.spinBox_step_x.value()
elif axis == "y": elif axis == "y":
step = direction * self.spinBox_step_y.value() step = direction * self.ui.spinBox_step_y.value()
self.motor_thread.move_relative(motor, step) self.motor_thread.move_relative(motor, step)

View File

@ -150,7 +150,7 @@ def test_spiral_bar(rpc_server_dock):
dock = BECDockArea(rpc_server_dock.gui_id) dock = BECDockArea(rpc_server_dock.gui_id)
dock_server = rpc_server_dock.gui dock_server = rpc_server_dock.gui
d0 = dock.add_dock("dock_0") d0 = dock.add_dock(name="dock_0")
bar = d0.add_widget_bec("SpiralProgressBar") bar = d0.add_widget_bec("SpiralProgressBar")
assert bar.__class__.__name__ == "SpiralProgressBar" assert bar.__class__.__name__ == "SpiralProgressBar"

View File

@ -17,8 +17,8 @@ def cli_figure():
def test_rpc_call_plot(cli_figure): def test_rpc_call_plot(cli_figure):
fig, mock_rpc_call = cli_figure fig, mock_rpc_call = cli_figure
fig.plot("samx", "bpm4i") fig.plot(x_name="samx", y_name="bpm4i")
mock_rpc_call.assert_called_with("plot", "samx", "bpm4i") mock_rpc_call.assert_called_with("plot", x_name="samx", y_name="bpm4i")
def test_rpc_call_accepts_device_as_input(cli_figure): def test_rpc_call_accepts_device_as_input(cli_figure):

View File

@ -44,8 +44,8 @@ def test_mouse_moved_signals(qtbot):
# Create a slot that will store the emitted values as tuples # Create a slot that will store the emitted values as tuples
emitted_values_1D = [] emitted_values_1D = []
def slot(x, y_values): def slot(coordinates):
emitted_values_1D.append((x, y_values)) emitted_values_1D.append(coordinates)
# Connect the signal to the custom slot # Connect the signal to the custom slot
crosshair.coordinatesChanged1D.connect(slot) crosshair.coordinatesChanged1D.connect(slot)
@ -59,7 +59,7 @@ def test_mouse_moved_signals(qtbot):
crosshair.mouse_moved(event_mock) crosshair.mouse_moved(event_mock)
# Assert the expected behavior # Assert the expected behavior
assert emitted_values_1D == [(2.0, [5.0])] assert emitted_values_1D == [(2, [5])]
def test_mouse_moved_signals_outside(qtbot): def test_mouse_moved_signals_outside(qtbot):
@ -106,8 +106,8 @@ def test_mouse_moved_signals_2D(qtbot):
# Create a slot that will store the emitted values as tuples # Create a slot that will store the emitted values as tuples
emitted_values_2D = [] emitted_values_2D = []
def slot(x, y): def slot(coordinates):
emitted_values_2D.append((x, y)) emitted_values_2D.append(coordinates)
# Connect the signal to the custom slot # Connect the signal to the custom slot
crosshair.coordinatesChanged2D.connect(slot) crosshair.coordinatesChanged2D.connect(slot)

View File

@ -1,115 +0,0 @@
# pylint: disable = no-name-in-module,missing-class-docstring, missing-module-docstring
import json
from unittest.mock import MagicMock, patch
import numpy as np
import pyqtgraph as pg
import pytest
import zmq
from bec_widgets.examples.eiger_plot.eiger_plot import EigerPlot
# Common fixture for all tests
@pytest.fixture
def eiger_plot_instance(qtbot):
widget = EigerPlot()
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
widget.close()
@pytest.mark.parametrize(
"fft_checked, rotation_index, transpose_checked, log_checked, expected_image",
[
(False, 0, False, False, np.array([[2, 1], [1, 5]], dtype=float)), # just mask
(False, 1, False, False, np.array([[1, 5], [2, 1]], dtype=float)), # 90 deg rotation
(False, 2, False, False, np.array([[5, 1], [1, 2]], dtype=float)), # 180 deg rotation
(False, 0, True, False, np.array([[2, 1], [1, 5]], dtype=float)), # transposed
(False, 0, False, True, np.array([[0.30103, 0.0], [0.0, 0.69897]], dtype=float)), # log
(True, 0, False, False, np.array([[5.0, 3.0], [3.0, 9.0]], dtype=float)), # FFT
],
)
def test_on_image_update(
qtbot,
eiger_plot_instance,
fft_checked,
rotation_index,
transpose_checked,
log_checked,
expected_image,
):
# Initialize image and mask
eiger_plot_instance.image = np.array([[1, 2], [3, 4]], dtype=float)
eiger_plot_instance.mask = np.array([[0, 1], [1, 0]], dtype=float)
# Mock UI elements
eiger_plot_instance.checkBox_FFT = MagicMock()
eiger_plot_instance.checkBox_FFT.isChecked.return_value = fft_checked
eiger_plot_instance.comboBox_rotation = MagicMock()
eiger_plot_instance.comboBox_rotation.currentIndex.return_value = rotation_index
eiger_plot_instance.checkBox_transpose = MagicMock()
eiger_plot_instance.checkBox_transpose.isChecked.return_value = transpose_checked
eiger_plot_instance.checkBox_log = MagicMock()
eiger_plot_instance.checkBox_log.isChecked.return_value = log_checked
eiger_plot_instance.imageItem = MagicMock()
# Call the method
eiger_plot_instance.on_image_update()
# Validate the transformations
np.testing.assert_array_almost_equal(eiger_plot_instance.image, expected_image, decimal=5)
# Validate that setImage was called
eiger_plot_instance.imageItem.setImage.assert_called_with(
eiger_plot_instance.image, autoLevels=False
)
def test_init_ui(eiger_plot_instance):
assert isinstance(eiger_plot_instance.plot_item, pg.PlotItem)
assert isinstance(eiger_plot_instance.imageItem, pg.ImageItem)
assert isinstance(eiger_plot_instance.hist, pg.HistogramLUTItem)
def test_start_zmq_consumer(eiger_plot_instance):
with patch("threading.Thread") as MockThread:
eiger_plot_instance.start_zmq_consumer()
MockThread.assert_called_once()
MockThread.return_value.start.assert_called_once()
def test_zmq_consumer(eiger_plot_instance, qtbot):
fake_meta = json.dumps({"type": "int32", "shape": (2, 2)}).encode("utf-8")
fake_data = np.array([[1, 2], [3, 4]], dtype="int32").tobytes()
with patch("zmq.Context", autospec=True) as MockContext:
mock_socket = MagicMock()
mock_socket.recv_multipart.side_effect = ((fake_meta, fake_data),)
MockContext.return_value.socket.return_value = mock_socket
# Mocking the update_signal to check if it gets emitted
eiger_plot_instance.update_signal = MagicMock()
with patch("zmq.Poller"):
# will do only 1 iteration of the loop in the thread
eiger_plot_instance._zmq_consumer_exit_event.set()
# Run the method under test
consumer_thread = eiger_plot_instance.start_zmq_consumer()
consumer_thread.join()
# Check if zmq methods are called
# MockContext.assert_called_once()
assert MockContext.call_count == 1
mock_socket.connect.assert_called_with("tcp://129.129.95.38:20000")
mock_socket.setsockopt_string.assert_called_with(zmq.SUBSCRIBE, "")
mock_socket.recv_multipart.assert_called()
# Check if update_signal was emitted
eiger_plot_instance.update_signal.emit.assert_called_once()
# Validate the image data
np.testing.assert_array_equal(
eiger_plot_instance.image, np.array([[1, 2], [3, 4]], dtype="int32")
)

View File

@ -239,14 +239,14 @@ def test_absolute_save_current_coordinates(motor_absolute_widget):
motor_absolute_widget.coordinates_signal.connect(capture_emit) motor_absolute_widget.coordinates_signal.connect(capture_emit)
# Trigger saving current coordinates # Trigger saving current coordinates
motor_absolute_widget.pushButton_save.click() motor_absolute_widget.ui.pushButton_save.click()
assert emitted_coordinates == [(motor_x_value, motor_y_value)] assert emitted_coordinates == [(motor_x_value, motor_y_value)]
def test_absolute_set_absolute_coordinates(motor_absolute_widget): def test_absolute_set_absolute_coordinates(motor_absolute_widget):
motor_absolute_widget.spinBox_absolute_x.setValue(5) motor_absolute_widget.ui.spinBox_absolute_x.setValue(5)
motor_absolute_widget.spinBox_absolute_y.setValue(10) motor_absolute_widget.ui.spinBox_absolute_y.setValue(10)
# Connect to the coordinates_signal to capture emitted values # Connect to the coordinates_signal to capture emitted values
emitted_values = [] emitted_values = []
@ -257,7 +257,7 @@ def test_absolute_set_absolute_coordinates(motor_absolute_widget):
motor_absolute_widget.coordinates_signal.connect(capture_coordinates) motor_absolute_widget.coordinates_signal.connect(capture_coordinates)
# Simulate button click for absolute movement # Simulate button click for absolute movement
motor_absolute_widget.pushButton_set.click() motor_absolute_widget.ui.pushButton_set.click()
assert emitted_values == [(5, 10)] assert emitted_values == [(5, 10)]
@ -265,14 +265,14 @@ def test_absolute_set_absolute_coordinates(motor_absolute_widget):
def test_absolute_go_absolute_coordinates(motor_absolute_widget): def test_absolute_go_absolute_coordinates(motor_absolute_widget):
motor_absolute_widget.change_motors("samx", "samy") motor_absolute_widget.change_motors("samx", "samy")
motor_absolute_widget.spinBox_absolute_x.setValue(5) motor_absolute_widget.ui.spinBox_absolute_x.setValue(5)
motor_absolute_widget.spinBox_absolute_y.setValue(10) motor_absolute_widget.ui.spinBox_absolute_y.setValue(10)
with patch( with patch(
"bec_widgets.widgets.motor_control.motor_control.MotorThread.move_absolute", "bec_widgets.widgets.motor_control.motor_control.MotorThread.move_absolute",
new_callable=MagicMock, new_callable=MagicMock,
) as mock_move_absolute: ) as mock_move_absolute:
motor_absolute_widget.pushButton_go_absolute.click() motor_absolute_widget.ui.pushButton_go_absolute.click()
mock_move_absolute.assert_called_once_with("samx", "samy", (5, 10)) mock_move_absolute.assert_called_once_with("samx", "samy", (5, 10))
@ -292,8 +292,8 @@ def test_set_precision(motor_absolute_widget):
motor_absolute_widget.on_config_update(CONFIG_DEFAULT) motor_absolute_widget.on_config_update(CONFIG_DEFAULT)
motor_absolute_widget.set_precision(2) motor_absolute_widget.set_precision(2)
assert motor_absolute_widget.spinBox_absolute_x.decimals() == 2 assert motor_absolute_widget.ui.spinBox_absolute_x.decimals() == 2
assert motor_absolute_widget.spinBox_absolute_y.decimals() == 2 assert motor_absolute_widget.ui.spinBox_absolute_y.decimals() == 2
####################################################### #######################################################
@ -338,29 +338,29 @@ def test_initialization_and_config_update(motor_relative_widget):
def test_move_motor_relative(motor_relative_widget): def test_move_motor_relative(motor_relative_widget):
motor_relative_widget.on_config_update(CONFIG_DEFAULT) motor_relative_widget.on_config_update(CONFIG_DEFAULT)
# Set step sizes # Set step sizes
motor_relative_widget.spinBox_step_x.setValue(1) motor_relative_widget.ui.spinBox_step_x.setValue(1)
motor_relative_widget.spinBox_step_y.setValue(1) motor_relative_widget.ui.spinBox_step_y.setValue(1)
# Mock the move_relative method # Mock the move_relative method
motor_relative_widget.motor_thread.move_relative = MagicMock() motor_relative_widget.motor_thread.move_relative = MagicMock()
# Simulate button clicks # Simulate button clicks
motor_relative_widget.toolButton_right.click() motor_relative_widget.ui.toolButton_right.click()
motor_relative_widget.motor_thread.move_relative.assert_called_with( motor_relative_widget.motor_thread.move_relative.assert_called_with(
motor_relative_widget.motor_x, 1 motor_relative_widget.motor_x, 1
) )
motor_relative_widget.toolButton_left.click() motor_relative_widget.ui.toolButton_left.click()
motor_relative_widget.motor_thread.move_relative.assert_called_with( motor_relative_widget.motor_thread.move_relative.assert_called_with(
motor_relative_widget.motor_x, -1 motor_relative_widget.motor_x, -1
) )
motor_relative_widget.toolButton_up.click() motor_relative_widget.ui.toolButton_up.click()
motor_relative_widget.motor_thread.move_relative.assert_called_with( motor_relative_widget.motor_thread.move_relative.assert_called_with(
motor_relative_widget.motor_y, 1 motor_relative_widget.motor_y, 1
) )
motor_relative_widget.toolButton_down.click() motor_relative_widget.ui.toolButton_down.click()
motor_relative_widget.motor_thread.move_relative.assert_called_with( motor_relative_widget.motor_thread.move_relative.assert_called_with(
motor_relative_widget.motor_y, -1 motor_relative_widget.motor_y, -1
) )
@ -376,21 +376,21 @@ def test_precision_update(motor_relative_widget):
motor_relative_widget.precision_signal.connect(capture_precision) motor_relative_widget.precision_signal.connect(capture_precision)
# Update precision # Update precision
motor_relative_widget.spinBox_precision.setValue(1) motor_relative_widget.ui.spinBox_precision.setValue(1)
assert emitted_values == [1] assert emitted_values == [1]
assert motor_relative_widget.spinBox_step_x.decimals() == 1 assert motor_relative_widget.ui.spinBox_step_x.decimals() == 1
assert motor_relative_widget.spinBox_step_y.decimals() == 1 assert motor_relative_widget.ui.spinBox_step_y.decimals() == 1
def test_sync_step_sizes(motor_relative_widget): def test_sync_step_sizes(motor_relative_widget):
motor_relative_widget.on_config_update(CONFIG_DEFAULT) motor_relative_widget.on_config_update(CONFIG_DEFAULT)
motor_relative_widget.checkBox_same_xy.setChecked(True) motor_relative_widget.ui.checkBox_same_xy.setChecked(True)
# Change step size for X # Change step size for X
motor_relative_widget.spinBox_step_x.setValue(2) motor_relative_widget.ui.spinBox_step_x.setValue(2)
assert motor_relative_widget.spinBox_step_y.value() == 2 assert motor_relative_widget.ui.spinBox_step_y.value() == 2
def test_change_motor_relative(motor_relative_widget): def test_change_motor_relative(motor_relative_widget):
@ -420,11 +420,11 @@ def test_delete_selected_row(motor_coordinate_table):
motor_coordinate_table.add_coordinate((3.0, 4.0)) motor_coordinate_table.add_coordinate((3.0, 4.0))
# Select the row # Select the row
motor_coordinate_table.table.selectRow(0) motor_coordinate_table.ui.table.selectRow(0)
# Delete the selected row # Delete the selected row
motor_coordinate_table.delete_selected_row() motor_coordinate_table.delete_selected_row()
assert motor_coordinate_table.table.rowCount() == 1 assert motor_coordinate_table.ui.table.rowCount() == 1
def test_add_coordinate_and_table_update(motor_coordinate_table): def test_add_coordinate_and_table_update(motor_coordinate_table):
@ -433,20 +433,24 @@ def test_add_coordinate_and_table_update(motor_coordinate_table):
# Add coordinate in Individual mode # Add coordinate in Individual mode
motor_coordinate_table.add_coordinate((1.0, 2.0)) motor_coordinate_table.add_coordinate((1.0, 2.0))
assert motor_coordinate_table.table.rowCount() == 1 assert motor_coordinate_table.ui.table.rowCount() == 1
# Check if the coordinates match # Check if the coordinates match
x_item_individual = motor_coordinate_table.table.cellWidget(0, 3) # Assuming X is in column 3 x_item_individual = motor_coordinate_table.ui.table.cellWidget(
y_item_individual = motor_coordinate_table.table.cellWidget(0, 4) # Assuming Y is in column 4 0, 3
) # Assuming X is in column 3
y_item_individual = motor_coordinate_table.ui.table.cellWidget(
0, 4
) # Assuming Y is in column 4
assert float(x_item_individual.text()) == 1.0 assert float(x_item_individual.text()) == 1.0
assert float(y_item_individual.text()) == 2.0 assert float(y_item_individual.text()) == 2.0
# Switch to Start/Stop and add coordinates # Switch to Start/Stop and add coordinates
motor_coordinate_table.comboBox_mode.setCurrentIndex(1) # Switch mode motor_coordinate_table.ui.comboBox_mode.setCurrentIndex(1) # Switch mode
motor_coordinate_table.add_coordinate((3.0, 4.0)) motor_coordinate_table.add_coordinate((3.0, 4.0))
motor_coordinate_table.add_coordinate((5.0, 6.0)) motor_coordinate_table.add_coordinate((5.0, 6.0))
assert motor_coordinate_table.table.rowCount() == 1 assert motor_coordinate_table.ui.table.rowCount() == 1
def test_plot_coordinates_signal(motor_coordinate_table): def test_plot_coordinates_signal(motor_coordinate_table):
@ -466,26 +470,26 @@ def test_plot_coordinates_signal(motor_coordinate_table):
assert received assert received
def test_move_motor_action(motor_coordinate_table): # def test_move_motor_action(motor_coordinate_table,qtbot):#TODO enable again after table refactor
# Add a coordinate # # Add a coordinate
motor_coordinate_table.add_coordinate((1.0, 2.0)) # motor_coordinate_table.add_coordinate((1.0, 2.0))
#
# Mock the motor thread move_absolute function # # Mock the motor thread move_absolute function
motor_coordinate_table.motor_thread.move_absolute = MagicMock() # motor_coordinate_table.motor_thread.move_absolute = MagicMock()
#
# Trigger the move action # # Trigger the move action
move_button = motor_coordinate_table.table.cellWidget(0, 1) # move_button = motor_coordinate_table.table.cellWidget(0, 1)
move_button.click() # move_button.click()
#
motor_coordinate_table.motor_thread.move_absolute.assert_called_with( # motor_coordinate_table.motor_thread.move_absolute.assert_called_with(
motor_coordinate_table.motor_x, motor_coordinate_table.motor_y, (1.0, 2.0) # motor_coordinate_table.motor_x, motor_coordinate_table.motor_y, (1.0, 2.0)
) # )
def test_plot_coordinates_signal_individual(motor_coordinate_table, qtbot): def test_plot_coordinates_signal_individual(motor_coordinate_table, qtbot):
motor_coordinate_table.warning_message = False motor_coordinate_table.warning_message = False
motor_coordinate_table.set_precision(3) motor_coordinate_table.set_precision(3)
motor_coordinate_table.comboBox_mode.setCurrentIndex(0) motor_coordinate_table.ui.comboBox_mode.setCurrentIndex(0)
# This list will store the signals emitted during the test # This list will store the signals emitted during the test
emitted_signals = [] emitted_signals = []
@ -506,8 +510,8 @@ def test_plot_coordinates_signal_individual(motor_coordinate_table, qtbot):
assert len(coordinates) > 0, "Coordinates list is empty." assert len(coordinates) > 0, "Coordinates list is empty."
assert reference_tag == "Individual" assert reference_tag == "Individual"
assert color == "green" assert color == "green"
assert motor_coordinate_table.table.cellWidget(0, 3).text() == "1.000" assert motor_coordinate_table.ui.table.cellWidget(0, 3).text() == "1.000"
assert motor_coordinate_table.table.cellWidget(0, 4).text() == "2.000" assert motor_coordinate_table.ui.table.cellWidget(0, 4).text() == "2.000"
####################################################### #######################################################