diff --git a/bec_widgets/applications/__init__.py b/bec_widgets/applications/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bec_widgets/applications/alignment/__init__.py b/bec_widgets/applications/alignment/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bec_widgets/applications/alignment/alignment_1d/__init__.py b/bec_widgets/applications/alignment/alignment_1d/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bec_widgets/applications/alignment/alignment_1d/alignment_1d.py b/bec_widgets/applications/alignment/alignment_1d/alignment_1d.py index 33a35558..c5d7ebc8 100644 --- a/bec_widgets/applications/alignment/alignment_1d/alignment_1d.py +++ b/bec_widgets/applications/alignment/alignment_1d/alignment_1d.py @@ -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_()) diff --git a/bec_widgets/applications/alignment/alignment_1d/alignment_1d.ui b/bec_widgets/applications/alignment/alignment_1d/alignment_1d.ui index 6f17e8c0..4972f1dd 100644 --- a/bec_widgets/applications/alignment/alignment_1d/alignment_1d.ui +++ b/bec_widgets/applications/alignment/alignment_1d/alignment_1d.ui @@ -6,28 +6,121 @@ 0 0 - 1578 - 962 + 1335 + 939 Form - - 6 - - - 12 - - - 0 - - - 12 - - + + + + + + + + false + + + BEC Server State + + + true + + + true + + + false + + + + + + + false + + + BEC Queue + + + true + + + true + + + false + + + + + + + false + + + SLS Light On + + + true + + + true + + + false + + + + + + + false + + + BEAMLINE Checks + + + true + + + true + + + false + + + + + + + + 0 + 0 + + + + + 200 + 40 + + + + + 200 + 40 + + + + + + + + @@ -39,22 +132,22 @@ 0 - + Alignment Control - 4 + 2 - 4 + 2 - 4 + 2 - 4 + 2 @@ -91,19 +184,19 @@ - + - 600 + 450 95 - 600 - 95 + 450 + 16777215 @@ -131,19 +224,6 @@ 8 - - - - - - - LMFit Model - - - - - - @@ -154,9 +234,6 @@ - - - @@ -167,6 +244,22 @@ + + + + + + + LMFit Model + + + + + + + + + @@ -174,299 +267,271 @@ - + - 600 - 191 + 450 + 343 - 600 - 191 + 450 + 16777215 - - - 15 - true - + + 0 - - false - - - LineScan - - - - 8 - - - 8 - - - 8 - - - 8 - - - - - - false - - - - Relative - - - - - - - 3 - - - -10000000.000000000000000 - - - 10000000.000000000000000 - - - 0.000000000000000 - - - - - - - 1 - - - 10000000 - - - 1 - - - - - - - - - Run Scan - - - false - - - false - - - - - - - - - - - - - false - - - - Start - - - - - - - - false - - - - Exposure Time - - - - - - - - false - - - - Steps - - - - - - - - false - - - - Stop - - - - - - - - false - - - - Burst at each point - - - - - - - 3 - - - -10000000.000000000000000 - - - 10000000.000000000000000 - - - 0.000000000000000 - - - - - - - 3 - - - -10000000.000000000000000 - - - 10000000.000000000000000 - - - 0.000000000000000 - - - - - - - 0 - - - 10000000 - - - 0 - - - - - - - - - - - + + + LineScan + + + + + + + false + + + + Relative + + + + + + + 3 + + + -10000000.000000000000000 + + + 10000000.000000000000000 + + + 0.000000000000000 + + + + + + + + false + + + + Exposure Time + + + + + + + + false + + + + Start + + + + + + + + false + + + + Burst at each point + + + + + + + + + + + + + + + false + + + + Stop + + + + + + + 0 + + + 10000000 + + + 0 + + + + + + + + 0 + 40 + + + + + + + Run Scan + + + false + + + false + + + + + + + 3 + + + -10000000.000000000000000 + + + 10000000.000000000000000 + + + 0.000000000000000 + + + + + + + 1 + + + 10000000 + + + 1 + + + + + + + + false + + + + Steps + + + + + + + 3 + + + -10000000.000000000000000 + + + 10000000.000000000000000 + + + 0.000000000000000 + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + + MotorTweak + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + true + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + - - + + + Qt::Orientation::Vertical + + - 600 - 201 + 20 + 40 - - - 600 - 201 - - - - - 15 - true - - - - Motor Tweak - - - - 0 - - - 12 - - - 0 - - - 12 - - - - - true - - - - - - - - - - - 600 - 85 - - - - - 600 - 85 - - - - - 15 - true - - - - Utils - - - - - - Send summary to SciLog - - - - - + @@ -477,7 +542,7 @@ 600 - 0 + 450 @@ -508,6 +573,18 @@ + + + 0 + 0 + + + + + 0 + 190 + + true @@ -530,6 +607,18 @@ Logbook + + 2 + + + 2 + + + 2 + + + 2 + @@ -544,14 +633,20 @@ - - - - - - - - + + + + 0 + 25 + + + + + 16777215 + 25 + + + @@ -586,16 +681,16 @@ QWidget
positioner_box
- - PositionerControlLine - PositionerBox -
positioner_control_line
-
BECProgressBar QWidget
bec_progress_bar
+ + DarkModeButton + QWidget +
dark_mode_button
+
BECWaveformWidget QWidget @@ -611,16 +706,11 @@ QWidget
lm_fit_dialog
- - AbortButton - QWidget -
abort_button
-
- tabWidget device_combobox device_combobox_2 + tabWidget_2 linescan_start linescan_stop linescan_step @@ -638,12 +728,12 @@ select_x_axis(QString) - 214 - 130 + 162 + 170 - 629 - 130 + 467 + 170 @@ -654,12 +744,12 @@ select_y_axis(QString) - 388 - 130 + 297 + 170 - 629 - 130 + 467 + 170 @@ -670,8 +760,8 @@ add_dap(QString,QString,QString) - 629 - 130 + 467 + 170 1099 @@ -679,22 +769,6 @@ - - device_combobox - currentTextChanged(QString) - positioner_control_line - set_positioner(QString) - - - 214 - 130 - - - 174 - 550 - - - scan_button clicked() @@ -702,12 +776,12 @@ run_scan() - 332 - 336 + 455 + 511 - 28 - 319 + 16 + 441 @@ -718,8 +792,8 @@ plot(QString) - 388 - 130 + 297 + 170 1099 @@ -734,8 +808,8 @@ set_x(QString) - 214 - 130 + 162 + 170 1099 @@ -750,8 +824,8 @@ toogle_roi_select(bool) - 691 - 536 + 529 + 728 1099 @@ -770,8 +844,24 @@ 258 - 1098 - 668 + 1157 + 929 + + + + + device_combobox + currentTextChanged(QString) + positioner_box + set_positioner(QString) + + + 109 + 155 + + + 160 + 286 diff --git a/bec_widgets/assets/app_icons/alignment_1d.png b/bec_widgets/assets/app_icons/alignment_1d.png new file mode 100644 index 00000000..8067b417 Binary files /dev/null and b/bec_widgets/assets/app_icons/alignment_1d.png differ diff --git a/bec_widgets/qt_utils/toolbar.py b/bec_widgets/qt_utils/toolbar.py index 5e2db8b8..170b3a54 100644 --- a/bec_widgets/qt_utils/toolbar.py +++ b/bec_widgets/qt_utils/toolbar.py @@ -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) diff --git a/bec_widgets/utils/linear_region_selector.py b/bec_widgets/utils/linear_region_selector.py index 7e906088..8fdeb49f 100644 --- a/bec_widgets/utils/linear_region_selector.py +++ b/bec_widgets/utils/linear_region_selector.py @@ -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) diff --git a/bec_widgets/utils/plot_indicator_items.py b/bec_widgets/utils/plot_indicator_items.py index 975fd932..46017737 100644 --- a/bec_widgets/utils/plot_indicator_items.py +++ b/bec_widgets/utils/plot_indicator_items.py @@ -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 diff --git a/bec_widgets/widgets/figure/plots/plot_base.py b/bec_widgets/widgets/figure/plots/plot_base.py index 7f2279b9..49aac682 100644 --- a/bec_widgets/widgets/figure/plots/plot_base.py +++ b/bec_widgets/widgets/figure/plots/plot_base.py @@ -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() diff --git a/bec_widgets/widgets/figure/plots/waveform/waveform.py b/bec_widgets/widgets/figure/plots/waveform/waveform.py index 6d84d846..0c9fd81e 100644 --- a/bec_widgets/widgets/figure/plots/waveform/waveform.py +++ b/bec_widgets/widgets/figure/plots/waveform/waveform.py @@ -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), diff --git a/bec_widgets/widgets/lmfit_dialog/lmfit_dialog.py b/bec_widgets/widgets/lmfit_dialog/lmfit_dialog.py index dbb9f6cb..50ac87c8 100644 --- a/bec_widgets/widgets/lmfit_dialog/lmfit_dialog.py +++ b/bec_widgets/widgets/lmfit_dialog/lmfit_dialog.py @@ -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() diff --git a/bec_widgets/widgets/positioner_box/positioner_box.py b/bec_widgets/widgets/positioner_box/positioner_box.py index c8f00c41..b60df661 100644 --- a/bec_widgets/widgets/positioner_box/positioner_box.py +++ b/bec_widgets/widgets/positioner_box/positioner_box.py @@ -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") diff --git a/bec_widgets/widgets/stop_button/stop_button.py b/bec_widgets/widgets/stop_button/stop_button.py index 62769b64..dbddb226 100644 --- a/bec_widgets/widgets/stop_button/stop_button.py +++ b/bec_widgets/widgets/stop_button/stop_button.py @@ -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_()) diff --git a/tests/unit_tests/test_lmfit_dialog.py b/tests/unit_tests/test_lmfit_dialog.py index f016d353..558f62c6 100644 --- a/tests/unit_tests/test_lmfit_dialog.py +++ b/tests/unit_tests/test_lmfit_dialog.py @@ -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} diff --git a/tests/unit_tests/test_utils_plot_indicators.py b/tests/unit_tests/test_utils_plot_indicators.py index 5eb5e7d5..6dbb8d3c 100644 --- a/tests/unit_tests/test_utils_plot_indicators.py +++ b/tests/unit_tests/test_utils_plot_indicators.py @@ -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