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

refactor: various minor improvements for the alignment gui

This commit is contained in:
2024-09-20 22:34:38 +02:00
parent 0f9953e8fd
commit f554f3c167
16 changed files with 797 additions and 565 deletions

View File

View File

@ -1,6 +1,5 @@
""" 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.
It is a preliminary version of the GUI, which will be added to the main branch and steadily updated to be improved.
"""
import os
@ -8,24 +7,32 @@ from typing import Optional
from bec_lib.device import Positioner, Signal
from bec_lib.endpoints import MessageEndpoints
from qtpy.QtCore import QSize
from qtpy.QtCore import Signal as pyqtSignal
from qtpy.QtWidgets import QCheckBox, QDoubleSpinBox, QSpinBox, QVBoxLayout, QWidget
from qtpy.QtGui import QIcon
from qtpy.QtWidgets import QCheckBox, QDoubleSpinBox, QPushButton, QSpinBox, QVBoxLayout, QWidget
import bec_widgets
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.utils.colors import get_accent_colors
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.positioner_box.positioner_box import PositionerBox
from bec_widgets.widgets.stop_button.stop_button import StopButton
from bec_widgets.widgets.toggle.toggle import ToggleSwitch
from bec_widgets.widgets.waveform.waveform_widget import BECWaveformWidget
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
class Alignment1D(BECWidget, QWidget):
"""GUI for 1D alignment"""
"""Alignment GUI to perform 1D scans"""
# Emit a signal when a motion is ongoing
motion_is_active = pyqtSignal(bool)
def __init__(
@ -42,6 +49,7 @@ class Alignment1D(BECWidget, QWidget):
super().__init__(client=client, gui_id=gui_id)
QWidget.__init__(self, parent)
self.get_bec_shortcuts()
self._accent_colors = get_accent_colors()
self.ui_file = "alignment_1d.ui"
self.ui = None
self.progress_bar = None
@ -50,57 +58,61 @@ class Alignment1D(BECWidget, QWidget):
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.waveform = self.ui.findChild(BECWaveformWidget, "bec_waveform_widget")
self._customise_bec_waveform_widget()
# Setup filters for comboboxes
# Setup comboboxes for motor and signal selection
# FIXME after changing the filtering in the combobox
self._setup_motor_combobox()
self._setup_signal_combobox()
# Setup arrow item
self._setup_arrow_item()
# Setup Scan Control
# Setup motor indicator
self._setup_motor_indicator()
# Connect spinboxes to scan Control
self._setup_scan_control()
# Setup progress bar
self._setup_progress_bar()
# Add actions buttons
self._add_action_buttons()
self._customise_buttons()
# Customize the positioner box
self._customize_positioner_box()
# Hook scaninfo updates
self.bec_dispatcher.connect_slot(self._scan_status_callback, MessageEndpoints.scan_status())
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:
def scan_status_callback(self, content: 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)
self.enable_ui(False)
elif content["status"] in ["aborted", "halted", "closed"]:
self.motion_is_active.emit(False)
self._enable_ui(True)
self.enable_ui(True)
@Slot(float)
def move_to_center(self, pos: float) -> None:
@Slot(tuple)
def move_to_center(self, move_request: tuple) -> None:
"""Move the selected motor to the center"""
motor = self.ui.device_combobox.currentText()
self.dev[motor].move(float(pos), relative=False)
if move_request[0] in ["center", "center1", "center2"]:
pos = move_request[1]
self.dev.get(motor).move(float(pos), relative=False)
@Slot()
def _reset_progress_bar(self) -> None:
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:
def update_progress_bar(self, content: dict, _) -> None:
"""Hook to update the progress bar
Args:
@ -113,58 +125,75 @@ class Alignment1D(BECWidget, QWidget):
self.progress_bar.set_maximum(content["max_value"])
self.progress_bar.set_value(content["value"])
@Slot()
def clear_queue(self) -> None:
"""Clear the scan queue"""
self.queue.request_queue_reset()
##############################
######## END OF SLOTS ########
##############################
def _enable_ui(self, enable: bool) -> None:
def enable_ui(self, enable: bool) -> None:
"""Enable or disable the UI components"""
# Device selection comboboxes
# Enable/disable motor and signal selection
self.ui.device_combobox.setEnabled(enable)
self.ui.device_combobox_2.setEnabled(enable)
# Enable/disable DAP selection
self.ui.dap_combo_box.setEnabled(enable)
# Scan button
# Enable/disable 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)
self.ui.positioner_box._toogle_enable_buttons(enable)
# Disable move to buttons in LMFitDialog
self.ui.findChild(LMFitDialog).set_enable_move_to_buttons(enable)
self.ui.findChild(LMFitDialog).set_actions_enabled(enable)
def _add_action_buttons(self) -> None:
"""Add action buttons for the Action Control"""
def _customise_buttons(self) -> None:
"""Add action buttons for the Action Control.
In addition, we are adding a callback to also clear the queue to the stop button
to ensure that upon clicking the button, no scans from another client may be queued
which would be confusing without the queue widget.
"""
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)
fit_dialog.active_action_list = ["center", "center1", "center2"]
fit_dialog.move_action.connect(self.move_to_center)
scan_button = self.ui.findChild(QPushButton, "scan_button")
scan_button.setStyleSheet(
f"""
QPushButton:enabled {{ background-color: {self._accent_colors.success.name()};color: white; }}
QPushButton:disabled {{ background-color: grey;color: white; }}
"""
)
stop_button = self.ui.findChild(StopButton)
stop_button.button.setText("Stop and Clear Queue")
stop_button.button.clicked.connect(self.clear_queue)
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)
"""Customise the BEC Waveform Widget, i.e. clear the toolbar, add the DAP ROI selection to the toolbar.
We also move the scan_control widget which is fully hidden and solely used for setting up the scan parameters to the toolbar.
"""
self.waveform.toolbar.clear()
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),
"label": WidgetAction(label="ENABLE DAP ROI", widget=toggle_switch),
"scan_control": WidgetAction(widget=scan_control),
},
self.waveform,
)
def _setup_arrow_item(self) -> None:
def _setup_motor_indicator(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)
self.waveform.waveform.tick_item.add_to_plot()
positioner_box = self.ui.findChild(PositionerBox)
positioner_box.position_update.connect(self.waveform.waveform.tick_item.set_position)
try:
pos = float(positioner_line.ui.readback.text())
pos = float(positioner_box.ui.readback.text())
except ValueError:
pos = 0
self.waveform.waveform.motor_pos_tick.set_position(pos)
self.waveform.waveform.tick_item.set_position(pos)
def _setup_motor_combobox(self) -> None:
"""Setup motor selection"""
@ -182,9 +211,11 @@ class Alignment1D(BECWidget, QWidget):
def _setup_scan_control(self) -> None:
"""Setup scan control, connect spin and check boxes to the scan_control widget"""
# Connect motor
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())
# Connect start, stop, step, exp_time and relative check box
spin_boxes = self.ui.scan_control.arg_box.findChildren(QDoubleSpinBox)
start = self.ui.findChild(QDoubleSpinBox, "linescan_start")
start.valueChanged.connect(spin_boxes[0].setValue)
@ -208,18 +239,27 @@ class Alignment1D(BECWidget, QWidget):
# 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()
)
self.ui.bec_waveform_widget.new_scan.connect(self.reset_progress_bar)
self.bec_dispatcher.connect_slot(self.update_progress_bar, MessageEndpoints.scan_progress())
def _customize_positioner_box(self) -> None:
"""Customize the positioner Box, i.e. remove the stop button"""
box = self.ui.findChild(PositionerBox)
box.ui.stop.setVisible(False)
box.ui.position_indicator.setFixedHeight(20)
if __name__ == "__main__":
if __name__ == "__main__": # pragma: no cover
import sys
from qtpy.QtWidgets import QApplication
from qtpy.QtWidgets import QApplication # pylint: disable=ungrouped-imports
app = QApplication(sys.argv)
icon = QIcon()
icon.addFile(
os.path.join(MODULE_PATH, "assets", "app_icons", "alignment_1d.png"), size=QSize(48, 48)
)
app.setWindowIcon(icon)
window = Alignment1D()
window.show()
sys.exit(app.exec_())

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

View File

@ -9,7 +9,7 @@ from typing import Literal
from bec_qthemes._icon.material_icons import material_icon
from qtpy.QtCore import QSize
from qtpy.QtGui import QAction, QColor, QIcon
from qtpy.QtWidgets import QHBoxLayout, QLabel, QMenu, QToolBar, QToolButton, QWidget
from qtpy.QtWidgets import QHBoxLayout, QLabel, QMenu, QSizePolicy, QToolBar, QToolButton, QWidget
import bec_widgets
@ -165,7 +165,9 @@ class WidgetAction(ToolBarAction):
layout.setContentsMargins(0, 0, 0, 0)
if self.label is not None:
label = QLabel(f"{self.label}")
label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
layout.addWidget(label)
self.widget.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
layout.addWidget(self.widget)
toolbar.addWidget(widget)

View File

@ -30,7 +30,7 @@ class LinearRegionWrapper(QObject):
self.proxy = None
self.change_roi_color((color, hover_color))
self.plot_item.ctrl.logXCheck.checkStateChanged.connect(self.check_log)
self.plot_item.ctrl.logXCheck.checkStateChanged.connect(self.check_log)
self.plot_item.ctrl.logYCheck.checkStateChanged.connect(self.check_log)
# Slot for changing the color of the region selector (edge and fill)
@Slot(tuple)

View File

@ -2,10 +2,13 @@
import numpy as np
import pyqtgraph as pg
from qtpy.QtCore import QObject, Qt, Signal, Slot
from bec_lib.logger import bec_logger
from qtpy.QtCore import QObject, QPointF, Signal, Slot
from bec_widgets.utils.colors import get_accent_colors
logger = bec_logger.logger
class BECIndicatorItem(QObject):
@ -13,10 +16,20 @@ class BECIndicatorItem(QObject):
super().__init__(parent=parent)
self.accent_colors = get_accent_colors()
self.plot_item = plot_item
self._item_on_plot = False
self._pos = None
self.is_log_x = False
self.is_log_y = False
@property
def item_on_plot(self) -> bool:
"""Returns if the item is on the plot"""
return self._item_on_plot
@item_on_plot.setter
def item_on_plot(self, value: bool) -> None:
self._item_on_plot = value
def add_to_plot(self) -> None:
"""Add the item to the plot"""
raise NotImplementedError("Method add_to_plot not implemented")
@ -25,8 +38,11 @@ class BECIndicatorItem(QObject):
"""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"""
def set_position(self, pos) -> None:
"""This method should implement the logic to set the position of the
item on the plot. Depending on the child class, the position can be
a tuple (x,y) or a single value, i.e. x position where y position is fixed.
"""
raise NotImplementedError("Method set_position not implemented")
def check_log(self):
@ -35,11 +51,6 @@ class BECIndicatorItem(QObject):
self.is_log_y = self.plot_item.ctrl.logYCheck.isChecked()
self.set_position(self._pos)
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.
@ -51,15 +62,17 @@ class BECTickItem(BECIndicatorItem):
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_item = pg.TickSliderItem(
parent=parent, allowAdd=False, allowRemove=False, orientation="bottom"
)
self.tick_item.skip_auto_range = True
self.tick = None
# Set _pos to float
self._pos = 0
self._pos = 0.0
self._range = [0, 1]
@Slot(float)
def set_position(self, pos: float) -> None:
"""Set the position of the tick item
"""Set the x position of the tick item
Args:
pos (float): The position of the tick item.
@ -74,36 +87,71 @@ class BECTickItem(BECIndicatorItem):
self.position_changed.emit(pos)
self.position_changed_str.emit(str(pos))
def update_range(self, vb, viewRange) -> None:
@Slot()
def update_range(self, _, view_range: tuple[float, float]) -> 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
if self._pos < view_range[0] or self._pos > view_range[1]:
self.tick_item.setVisible(False)
else:
self.tick_item.setVisible(True)
lengthIncludingPadding = length + self.tick_item.tickSize + 2
if self.tick_item.isVisible():
origin = self.tick_item.tickSize / 2.0
length = self.tick_item.length
self._range = viewRange
tickValueIncludingPadding = (self._pos - viewRange[0]) / (viewRange[1] - viewRange[0])
tickValue = (tickValueIncludingPadding * lengthIncludingPadding - origin) / length
self.tick_item.setTickValue(self.tick, tickValue)
length_with_padding = length + self.tick_item.tickSize + 2
self._range = view_range
tick_with_padding = (self._pos - view_range[0]) / (view_range[1] - view_range[0])
tick_value = (tick_with_padding * length_with_padding - origin) / length
self.tick_item.setTickValue(self.tick, tick_value)
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)
self.plot_item.ctrl.logXCheck.checkStateChanged.connect(self.check_log)
self.plot_item.ctrl.logYCheck.checkStateChanged.connect(self.check_log)
if self.plot_item is None:
return
self.plot_item.layout.addItem(self.tick_item, 2, 1)
self.tick_item.setOrientation("top")
self.tick = self.tick_item.addTick(0, movable=False, color=self.accent_colors.highlight)
self.update_tick_pos_y()
self.plot_item.vb.sigXRangeChanged.connect(self.update_range)
self.plot_item.ctrl.logXCheck.checkStateChanged.connect(self.check_log)
self.plot_item.ctrl.logYCheck.checkStateChanged.connect(self.check_log)
self.plot_item.vb.geometryChanged.connect(self.update_tick_pos_y)
self.item_on_plot = True
@Slot()
def update_tick_pos_y(self):
"""Update tick position, while respecting the tick_item coordinates"""
pos = self.tick.pos()
pos = self.tick_item.mapToParent(pos)
new_pos = self.plot_item.vb.geometry().bottom()
new_pos = self.tick_item.mapFromParent(QPointF(pos.x(), new_pos))
self.tick.setPos(new_pos)
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)
if self.plot_item is not None and self.item_on_plot is True:
self.plot_item.vb.sigXRangeChanged.disconnect(self.update_range)
self.plot_item.ctrl.logXCheck.checkStateChanged.disconnect(self.check_log)
self.plot_item.ctrl.logYCheck.checkStateChanged.disconnect(self.check_log)
if self.plot_item.layout is not None:
self.plot_item.layout.removeItem(self.tick_item)
self.item_on_plot = False
def cleanup(self) -> None:
"""Cleanup the item"""
self.remove_from_plot()
if self.tick_item is not None:
self.tick_item.close()
self.tick_item.deleteLater()
self.tick_item = None
class BECArrowItem(BECIndicatorItem):
@ -126,8 +174,10 @@ class BECArrowItem(BECIndicatorItem):
def __init__(self, plot_item: pg.PlotItem = None, parent=None):
super().__init__(plot_item=plot_item, parent=parent)
self.arrow_item = pg.ArrowItem()
self.arrow_item = pg.ArrowItem(parent=parent)
self.arrow_item.skip_auto_range = True
self._pos = (0, 0)
self.arrow_item.setVisible(False)
@Slot(dict)
def set_style(self, style: dict) -> None:
@ -166,24 +216,38 @@ class BECArrowItem(BECIndicatorItem):
def add_to_plot(self):
"""Add the arrow item to the view box or plot item."""
if not self.arrow_item:
logger.warning(f"Arrow item was already destroyed, cannot be created")
return
self.arrow_item.setStyle(
angle=-90,
pen=pg.mkPen(self.accent_colors.emergency, width=1),
brush=pg.mkBrush(self.accent_colors.highlight),
headLen=20,
)
self.arrow_item.setVisible(True)
if self.plot_item is not None:
self.plot_item.addItem(self.arrow_item)
self.plot_item.ctrl.logXCheck.checkStateChanged.connect(self.check_log)
self.plot_item.ctrl.logYCheck.checkStateChanged.connect(self.check_log)
self.item_on_plot = True
def remove_from_plot(self):
"""Remove the arrow item from the view box or plot item."""
if self.plot_item is not None:
if self.plot_item is not None and self.item_on_plot is True:
self.plot_item.ctrl.logXCheck.checkStateChanged.disconnect(self.check_log)
self.plot_item.ctrl.logYCheck.checkStateChanged.disconnect(self.check_log)
self.plot_item.removeItem(self.arrow_item)
self.item_on_plot = False
def check_log(self):
"""Checks if the x or y axis is in log scale and updates the internal state accordingly."""
self.is_log_x = self.plot_item.ctrl.logXCheck.isChecked()
self.is_log_y = self.plot_item.ctrl.logYCheck.isChecked()
self.set_position(self._pos)
def cleanup(self) -> None:
"""Cleanup the item"""
self.remove_from_plot()
self.arrow_item = None

View File

@ -106,7 +106,7 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout):
self.add_legend()
self.crosshair = None
self.motor_pos_tick = BECTickItem(parent=self, plot_item=self.plot_item)
self.tick_item = BECTickItem(parent=self, plot_item=self.plot_item)
self.arrow_item = BECArrowItem(parent=self, plot_item=self.plot_item)
self._connect_to_theme_change()
@ -431,7 +431,7 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout):
def cleanup_pyqtgraph(self):
"""Cleanup pyqtgraph items."""
self.unhook_crosshair()
self.motor_pos_tick.cleanup()
self.tick_item.cleanup()
self.arrow_item.cleanup()
item = self.plot_item
item.vb.menu.close()

View File

@ -1,6 +1,6 @@
# pylint: disable=too_many_lines
from __future__ import annotations
import time
from collections import defaultdict
from typing import Any, Literal, Optional
@ -13,7 +13,7 @@ from bec_lib.logger import bec_logger
from pydantic import Field, ValidationError, field_validator
from pyqtgraph.exporters import MatplotlibExporter
from qtpy.QtCore import Signal as pyqtSignal
from qtpy.QtWidgets import QApplication, QWidget
from qtpy.QtWidgets import QWidget
from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
from bec_widgets.utils import Colors, EntryValidator
@ -42,11 +42,12 @@ class BECSignalProxy(pg.SignalProxy):
self.args = args
if self.blocking:
return
# self.blocking = True
super().signalReceived(*args)
def flush(self):
"""If there is a signal queued send it out"""
super().flush()
# def flush(self):
# """If there is a signal queued send it out"""
# super().flush()
@Slot()
def unblock_proxy(self):
@ -190,7 +191,7 @@ class BECWaveform(BECPlotBase):
self.roi_select.linear_region_selector.setRegion(region)
except Exception as e:
logger.error(f"Error setting region {tuple}; Exception raised: {e}")
raise ValueError(f"Error setting region {tuple}; Exception raised: {e}")
raise ValueError(f"Error setting region {tuple}; Exception raised: {e}") from e
def _hook_roi(self):
"""Hook the linear region selector to the plot."""
@ -244,7 +245,7 @@ class BECWaveform(BECPlotBase):
# Reset curves
self._curves_data = defaultdict(dict)
self._curves = self.plot_item.curves
for curve_id, curve_config in self.config.curves.items():
for curve_config in self.config.curves.values():
self.add_curve_by_config(curve_config)
if replot_last_scan:
self.scan_history(scan_index=-1)
@ -367,7 +368,7 @@ class BECWaveform(BECPlotBase):
Returns:
CurveConfig|dict: Configuration of the curve.
"""
for source, curves in self._curves_data.items():
for curves in self._curves_data.values():
if curve_id in curves:
if dict_output:
return curves[curve_id].config.model_dump()
@ -387,7 +388,7 @@ class BECWaveform(BECPlotBase):
if isinstance(identifier, int):
return self.plot_item.curves[identifier]
elif isinstance(identifier, str):
for source_type, curves in self._curves_data.items():
for curves in self._curves_data.values():
if identifier in curves:
return curves[identifier]
raise ValueError(f"Curve with ID '{identifier}' not found.")
@ -538,6 +539,7 @@ class BECWaveform(BECPlotBase):
@Slot()
def auto_range(self):
"""Manually set auto range of the plotitem"""
self.plot_item.autoRange()
def set_auto_range(self, enabled: bool, axis: str = "xy"):
@ -576,7 +578,6 @@ class BECWaveform(BECPlotBase):
Returns:
BECCurve: The curve object.
"""
curve_source = curve_source
curve_id = label or f"Curve {len(self.plot_item.curves) + 1}"
curve_exits = self._check_curve_id(curve_id, self._curves_data)
@ -750,7 +751,7 @@ class BECWaveform(BECPlotBase):
if self.x_axis_mode["readout_priority"] == "async":
raise ValueError(
f"Async signals cannot be fitted at the moment. Please switch to 'monitored' or 'baseline' signals."
"Async signals cannot be fitted at the moment. Please switch to 'monitored' or 'baseline' signals."
)
if validate_bec is True:
@ -1024,7 +1025,7 @@ class BECWaveform(BECPlotBase):
Args:
curve_id(str): ID of the curve to be removed.
"""
for source, curves in self._curves_data.items():
for curves in self._curves_data.values():
if curve_id in curves:
curve = curves.pop(curve_id)
self.plot_item.removeItem(curve)
@ -1047,7 +1048,7 @@ class BECWaveform(BECPlotBase):
self.plot_item.removeItem(curve)
del self.config.curves[curve_id]
# Remove from self.curve_data
for source, curves in self._curves_data.items():
for curves in self._curves_data.values():
if curve_id in curves:
del curves[curve_id]
break
@ -1077,7 +1078,7 @@ class BECWaveform(BECPlotBase):
if self._curves_data["DAP"]:
self.setup_dap(self.old_scan_id, self.scan_id)
if self._curves_data["async"]:
for curve_id, curve in self._curves_data["async"].items():
for curve in self._curves_data["async"].values():
self.setup_async(
name=curve.config.signals.y.name, entry=curve.config.signals.y.entry
)
@ -1212,7 +1213,8 @@ class BECWaveform(BECPlotBase):
"""Callback for DAP response message."""
if self.proxy_update_dap is not None:
self.proxy_update_dap.unblock_proxy()
self.msg = msg
# pylint: disable=unused-variable
scan_id, x_name, x_entry, y_name, y_entry = msg["dap_request"].content["config"]["args"]
model = msg["dap_request"].content["config"]["class_kwargs"]["model"]
@ -1241,7 +1243,7 @@ class BECWaveform(BECPlotBase):
metadata(dict): Metadata of the message.
"""
instruction = metadata.get("async_update")
for curve_id, curve in self._curves_data["async"].items():
for curve in self._curves_data["async"].values():
y_name = curve.config.signals.y.name
y_entry = curve.config.signals.y.entry
x_name = self._x_axis_mode["name"]
@ -1365,7 +1367,7 @@ class BECWaveform(BECPlotBase):
if self._x_axis_mode["name"] is None or self._x_axis_mode["name"] == "best_effort":
if len(self._curves_data["async"]) > 0:
x_data = None
self._x_axis_mode["label_suffix"] = f" [auto: index]"
self._x_axis_mode["label_suffix"] = " [auto: index]"
current_label = "" if self.config.axis.x_label is None else self.config.axis.x_label
self.plot_item.setLabel(
"bottom", f"{current_label}{self._x_axis_mode['label_suffix']}"
@ -1511,7 +1513,7 @@ class BECWaveform(BECPlotBase):
self.bec_dispatcher.disconnect_slot(
self.update_dap, MessageEndpoints.dap_response(self.scan_id)
)
for curve_id, curve in self._curves_data["async"].items():
for curve_id in self._curves_data["async"]:
self.bec_dispatcher.disconnect_slot(
self.on_async_readback,
MessageEndpoints.device_async_readback(self.scan_id, curve_id),

View File

@ -1,12 +1,12 @@
import os
from bec_lib.endpoints import MessageEndpoints
from bec_lib.logger import bec_logger
from qtpy.QtCore import Property, Signal, Slot
from qtpy.QtWidgets import QPushButton, QTreeWidgetItem, QVBoxLayout, QWidget
from bec_widgets.utils import UILoader
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.colors import get_accent_colors
logger = bec_logger.logger
@ -17,8 +17,8 @@ class LMFitDialog(BECWidget, QWidget):
ICON_NAME = "monitoring"
# Signal to emit the currently selected fit curve_id
selected_fit = Signal(str)
# Signal to emit a position to move to.
move_to_position = Signal(float)
# Signal to emit a move action in form of a tuple (param_name, value)
move_action = Signal(tuple)
def __init__(
self,
@ -53,56 +53,44 @@ class LMFitDialog(BECWidget, QWidget):
self._fit_curve_id = None
self._deci_precision = 3
self._always_show_latest = False
self._activated_buttons_for_move_action = []
self.ui.curve_list.currentItemChanged.connect(self.display_fit_details)
self.layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(self.layout)
self._enable_move_to_buttons = False
self._active_actions = []
self._enable_actions = True
self._move_buttons = []
self._accent_colors = get_accent_colors()
self.action_buttons = {}
@Property(bool)
def enable_move_to_buttons(self):
@property
def enable_actions(self) -> bool:
"""Property to enable the move to buttons."""
return self._enable_move_to_buttons
return self._enable_actions
@enable_move_to_buttons.setter
def enable_move_to_buttons(self, enable: bool):
self._enable_move_to_buttons = enable
for button in self._move_buttons:
if button.text().split(" ")[-1] in self.activated_buttons_for_move_action:
button.setEnabled(enable)
@enable_actions.setter
def enable_actions(self, enable: bool):
self._enable_actions = enable
for button in self.action_buttons.values():
button.setEnabled(enable)
@Property(list)
def active_action_list(self) -> list[str]:
"""Property to list the names of the fit parameters for which actions should be enabled."""
return self._active_actions
@active_action_list.setter
def active_action_list(self, actions: list[str]):
self._active_actions = actions
# This slot needed?
@Slot(bool)
def set_enable_move_to_buttons(self, enable: bool):
def set_actions_enabled(self, enable: bool) -> bool:
"""Slot to enable the move to buttons.
Args:
enable (bool): Whether to enable the move to buttons.
enable (bool): Whether to enable the action buttons.
"""
self.enable_move_to_buttons = enable
@Property(list)
def activated_buttons_for_move_action(self) -> list:
"""Property for the buttons that should be activated for the move action in the parameter list."""
return self._activated_buttons_for_move_action
@activated_buttons_for_move_action.setter
def activated_buttons_for_move_action(self, buttons: list):
"""Setter for the buttons that should be activated for the move action.
Args:
buttons (list): The buttons that should be activated for the move action.
"""
self._activated_buttons_for_move_action = buttons
@Slot(list)
def update_activated_button_list(self, names: list) -> None:
"""Update the list of activated buttons for the move action.
Args:
names (list): List of button names to be activated.
"""
self.activated_buttons_for_move_action = names
self.enable_actions = enable
@Property(bool)
def always_show_latest(self):
@ -128,7 +116,7 @@ class LMFitDialog(BECWidget, QWidget):
self.ui.group_curve_selection.setVisible(not show)
@Property(bool)
def hide_summary(self):
def hide_summary(self) -> bool:
"""Property for showing the summary."""
return not self.ui.group_summary.isVisible()
@ -142,7 +130,7 @@ class LMFitDialog(BECWidget, QWidget):
self.ui.group_summary.setVisible(not show)
@Property(bool)
def hide_parameters(self):
def hide_parameters(self) -> bool:
"""Property for showing the parameters."""
return not self.ui.group_parameters.isVisible()
@ -156,7 +144,7 @@ class LMFitDialog(BECWidget, QWidget):
self.ui.group_parameters.setVisible(not show)
@property
def fit_curve_id(self):
def fit_curve_id(self) -> str:
"""Property for the currently displayed fit curve_id."""
return self._fit_curve_id
@ -266,28 +254,48 @@ class LMFitDialog(BECWidget, QWidget):
param_std = f"{param_std:.{self._deci_precision}f}"
else:
param_std = "None"
# Create a push button to move the motor to a specific position
# Per default, this feature is deactivated
widget = QWidget()
layout = QVBoxLayout(widget)
push_button = QPushButton(f"Move to {param_name}")
if param_name in self.activated_buttons_for_move_action:
push_button.setEnabled(True)
push_button.clicked.connect(
lambda _, value=param[1]: self.move_to_position.emit(float(value))
)
else:
push_button.setEnabled(False)
self._move_buttons.append(push_button)
layout.addWidget(push_button)
layout.setContentsMargins(0, 0, 0, 0)
tree_item = QTreeWidgetItem(self.ui.param_tree, [param_name, param_value, param_std])
self.ui.param_tree.setItemWidget(tree_item, 3, widget)
if param_name in self.active_action_list: # pylint: disable=unsupported-membership-test
# Create a push button to move the motor to a specific position
widget = QWidget()
button = QPushButton(f"Move to {param_name}")
button.clicked.connect(self._create_move_action(param_name, param[1]))
if self.enable_actions is True:
button.setEnabled(True)
else:
button.setEnabled(False)
button.setStyleSheet(
f"""
QPushButton:enabled {{ background-color: {self._accent_colors.success.name()};color: white; }}
QPushButton:disabled {{ background-color: grey;color: white; }}
"""
)
self.action_buttons[param_name] = button
layout = QVBoxLayout()
layout.addWidget(self.action_buttons[param_name])
layout.setContentsMargins(0, 0, 0, 0)
widget.setLayout(layout)
self.ui.param_tree.setItemWidget(tree_item, 3, widget)
def _create_move_action(self, param_name: str, param_value: float) -> callable:
"""Create a move action for the given parameter name and value.
Args:
param_name (str): The name of the parameter.
param_value (float): The value of the parameter.
Returns:
callable: The move action with the given parameter name and value.
"""
def move_action():
self.move_action.emit((param_name, param_value))
return move_action
def populate_curve_list(self):
"""Populate the curve list with the available fit curves."""
for curve_name in self.summary_data.keys():
for curve_name in self.summary_data:
self.ui.curve_list.addItem(curve_name)
def refresh_curve_list(self):
@ -310,10 +318,10 @@ class LMFitDialog(BECWidget, QWidget):
self.update_summary_tree(data, {"curve_id": curve_name})
if __name__ == "__main__":
if __name__ == "__main__": # pragma: no cover
import sys
from qtpy.QtWidgets import QApplication
from qtpy.QtWidgets import QApplication # pylint: disable=ungrouped-imports
app = QApplication(sys.argv)
dialog = LMFitDialog()

View File

@ -321,7 +321,7 @@ class PositionerBox(BECWidget, QWidget):
if __name__ == "__main__": # pragma: no cover
import sys
from qtpy.QtWidgets import QApplication
from qtpy.QtWidgets import QApplication # pylint: disable=ungrouped-imports
app = QApplication(sys.argv)
set_theme("dark")

View File

@ -1,6 +1,6 @@
from bec_qthemes import material_icon
from qtpy.QtCore import Qt
from qtpy.QtWidgets import QHBoxLayout, QPushButton, QToolButton, QWidget
from qtpy.QtWidgets import QHBoxLayout, QPushButton, QSizePolicy, QToolButton, QWidget
from bec_widgets.qt_utils.error_popups import SafeSlot
from bec_widgets.utils.bec_widget import BECWidget
@ -28,9 +28,10 @@ class StopButton(BECWidget, QWidget):
self.button.setToolTip("Stop the scan queue")
else:
self.button = QPushButton()
self.button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
self.button.setText("Stop")
self.button.setStyleSheet(
"background-color: #cc181e; color: white; font-weight: bold; font-size: 12px;"
f"background-color: #cc181e; color: white; font-weight: bold; font-size: 12px;"
)
self.button.clicked.connect(self.stop_scan)
@ -47,3 +48,14 @@ class StopButton(BECWidget, QWidget):
scan_id(str|None): The scan id to stop. If None, the current scan will be stopped.
"""
self.queue.request_scan_halt()
if __name__ == "__main__":
import sys
from qtpy.QtWidgets import QApplication
app = QApplication(sys.argv)
w = StopButton()
w.show()
sys.exit(app.exec_())

View File

@ -168,6 +168,7 @@ def test_remove_dap_data(lmfit_dialog):
def test_update_summary_tree(lmfit_dialog, lmfit_message):
"""Test display_fit_details method"""
lmfit_dialog.active_action_list = ["center", "amplitude"]
lmfit_dialog.update_summary_tree(data=lmfit_message, metadata={"curve_id": "test_curve_id"})
# Check if the data is updated
assert lmfit_dialog.summary_data == {"test_curve_id": lmfit_message}

View File

@ -1,5 +1,6 @@
import pyqtgraph as pg
import pytest
from qtpy.QtCore import QPointF
from bec_widgets.widgets.waveform.waveform_widget import BECWaveformWidget
@ -21,7 +22,7 @@ def plot_widget_with_tick_item(qtbot, mocked_client):
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget.waveform.motor_pos_tick, widget.waveform.plot_item
yield widget.waveform.tick_item, widget.waveform.plot_item
def test_arrow_item_add_to_plot(plot_widget_with_arrow_item):
@ -33,15 +34,6 @@ def test_arrow_item_add_to_plot(plot_widget_with_arrow_item):
assert arrow_item.plot_item.items == [arrow_item.arrow_item]
def test_arrow_item_remove_to_plot(plot_widget_with_arrow_item):
"""Test the remove_from_plot method"""
arrow_item, plot_item = plot_widget_with_arrow_item
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(plot_widget_with_arrow_item):
"""Test the set_position method"""
arrow_item, plot_item = plot_widget_with_arrow_item
@ -53,28 +45,37 @@ def test_arrow_item_set_position(plot_widget_with_arrow_item):
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)
point = QPointF(1.0, 1.0)
assert arrow_item.arrow_item.pos() == point
arrow_item.set_position(pos=(2, 2))
assert arrow_item.arrow_item.pos().toTuple() == (2, 2)
point = QPointF(2.0, 2.0)
assert arrow_item.arrow_item.pos() == point
assert container == [(1, 1), (2, 2)]
def test_arrow_item_cleanup(plot_widget_with_arrow_item):
"""Test cleanup procedure"""
arrow_item, plot_item = plot_widget_with_arrow_item
arrow_item.add_to_plot()
assert arrow_item.item_on_plot is True
arrow_item.cleanup()
assert arrow_item.plot_item.items == []
assert arrow_item.item_on_plot is False
assert arrow_item.arrow_item is None
def test_tick_item_add_to_plot(plot_widget_with_tick_item):
"""Test the add_to_plot method"""
tick_item, plot_item = plot_widget_with_tick_item
assert tick_item.plot_item is not None
assert tick_item.plot_item.items == []
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(plot_widget_with_tick_item):
"""Test the remove_from_plot method"""
tick_item, plot_item = plot_widget_with_tick_item
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
assert tick_item.plot_item.layout.itemAt(2, 1) == tick_item.tick_item
assert tick_item.item_on_plot is True
new_pos = plot_item.vb.geometry().bottom()
pos = tick_item.tick.pos()
new_pos = tick_item.tick_item.mapFromParent(QPointF(pos.x(), new_pos))
assert new_pos.y() == pos.y()
def test_tick_item_set_position(plot_widget_with_tick_item):
@ -93,3 +94,15 @@ def test_tick_item_set_position(plot_widget_with_tick_item):
tick_item.set_position(pos=2)
assert tick_item._pos == 2
assert container == [1.0, 2.0]
def test_tick_item_cleanup(plot_widget_with_tick_item):
"""Test cleanup procedure"""
tick_item, plot_item = plot_widget_with_tick_item
tick_item.add_to_plot()
assert tick_item.item_on_plot is True
tick_item.cleanup()
ticks = getattr(tick_item.plot_item.layout.itemAt(3, 1), "ticks", None)
assert ticks == None
assert tick_item.item_on_plot is False
assert tick_item.tick_item is None