diff --git a/bec_widgets/applications/alignment/alignment_1d/alignment_1d.py b/bec_widgets/applications/alignment/alignment_1d/alignment_1d.py new file mode 100644 index 00000000..33a35558 --- /dev/null +++ b/bec_widgets/applications/alignment/alignment_1d/alignment_1d.py @@ -0,0 +1,225 @@ +""" This module contains the GUI for the 1D alignment application. +#TODO at this stage it is a preliminary version of the GUI, which will be added to the main branch although it is not yet fully functional. +It is a work in progress and will be updated in the future. +""" + +import os +from typing import Optional + +from bec_lib.device import Positioner, Signal +from bec_lib.endpoints import MessageEndpoints +from qtpy.QtCore import Signal as pyqtSignal +from qtpy.QtWidgets import QCheckBox, QDoubleSpinBox, QSpinBox, QVBoxLayout, QWidget + +from bec_widgets.qt_utils.error_popups import SafeSlot as Slot +from bec_widgets.qt_utils.toolbar import WidgetAction +from bec_widgets.utils import UILoader +from bec_widgets.utils.bec_widget import BECWidget +from bec_widgets.widgets.bec_progressbar.bec_progressbar import BECProgressBar +from bec_widgets.widgets.device_line_edit.device_line_edit import DeviceLineEdit +from bec_widgets.widgets.lmfit_dialog.lmfit_dialog import LMFitDialog +from bec_widgets.widgets.positioner_box.positioner_control_line import PositionerControlLine +from bec_widgets.widgets.toggle.toggle import ToggleSwitch +from bec_widgets.widgets.waveform.waveform_widget import BECWaveformWidget + + +class Alignment1D(BECWidget, QWidget): + """GUI for 1D alignment""" + + motion_is_active = pyqtSignal(bool) + + def __init__( + self, parent: Optional[QWidget] = None, client=None, gui_id: Optional[str] = None + ) -> None: + """Initialise the widget + + Args: + parent: Parent widget. + config: Configuration of the widget. + client: BEC client object. + gui_id: GUI ID. + """ + super().__init__(client=client, gui_id=gui_id) + QWidget.__init__(self, parent) + self.get_bec_shortcuts() + self.ui_file = "alignment_1d.ui" + self.ui = None + self.progress_bar = None + self.waveform = None + self.init_ui() + + def init_ui(self): + """Initialise the UI from QT Designer file""" + + current_path = os.path.dirname(__file__) + self.ui = UILoader(self).loader(os.path.join(current_path, self.ui_file)) + layout = QVBoxLayout(self) + layout.addWidget(self.ui) + self.setLayout(layout) + self.waveform = self.ui.findChild(BECWaveformWidget, "bec_waveform_widget") + # Customize the plotting widget + self._customise_bec_waveform_widget() + # Setup filters for comboboxes + self._setup_motor_combobox() + self._setup_signal_combobox() + # Setup arrow item + self._setup_arrow_item() + # Setup Scan Control + self._setup_scan_control() + # Setup progress bar + self._setup_progress_bar() + # Add actions buttons + self._add_action_buttons() + # Hook scaninfo updates + self.bec_dispatcher.connect_slot(self._scan_status_callback, MessageEndpoints.scan_status()) + + ############################## + ############ SLOTS ########### + ############################## + + @Slot(dict, dict) + def _scan_status_callback(self, content: dict, metadata: dict) -> None: + """This slot allows to enable/disable the UI critical components when a scan is running""" + if content["status"] in ["running", "open"]: + self.motion_is_active.emit(True) + self._enable_ui(False) + elif content["status"] in ["aborted", "halted", "closed"]: + self.motion_is_active.emit(False) + self._enable_ui(True) + + @Slot(float) + def move_to_center(self, pos: float) -> None: + """Move the selected motor to the center""" + motor = self.ui.device_combobox.currentText() + self.dev[motor].move(float(pos), relative=False) + + @Slot() + def _reset_progress_bar(self) -> None: + """Reset the progress bar""" + self.progress_bar.set_value(0) + self.progress_bar.set_minimum(0) + + @Slot(dict, dict) + def _update_progress_bar(self, content: dict, metadata: dict) -> None: + """Hook to update the progress bar + + Args: + content: Content of the scan progress message. + metadata: Metadata of the message. + """ + if content["max_value"] == 0: + self.progress_bar.set_value(0) + return + self.progress_bar.set_maximum(content["max_value"]) + self.progress_bar.set_value(content["value"]) + + ############################## + ######## END OF SLOTS ######## + ############################## + + def _enable_ui(self, enable: bool) -> None: + """Enable or disable the UI components""" + # Device selection comboboxes + self.ui.device_combobox.setEnabled(enable) + self.ui.device_combobox_2.setEnabled(enable) + self.ui.dap_combo_box.setEnabled(enable) + # Scan button + self.ui.scan_button.setEnabled(enable) + # Positioner control line + # pylint: disable=protected-access + self.ui.positioner_control_line._toogle_enable_buttons(enable) + # Send report to scilog + self.ui.button_send_summary_scilog.setEnabled(enable) + # Disable move to buttons in LMFitDialog + self.ui.findChild(LMFitDialog).set_enable_move_to_buttons(enable) + + def _add_action_buttons(self) -> None: + """Add action buttons for the Action Control""" + fit_dialog = self.ui.findChild(LMFitDialog) + fit_dialog.update_activated_button_list(["center", "center1", "center2"]) + fit_dialog.move_to_position.connect(self.move_to_center) + + def _customise_bec_waveform_widget(self) -> None: + """Customise the BEC Waveform Widget, i.e. hide toolbar buttons except roi selection""" + for button in self.waveform.toolbar.widgets.values(): + if getattr(button, "action", None) is not None: + button.action.setVisible(False) + self.waveform.toolbar.widgets["roi_select"].action.setVisible(True) + toggle_switch = self.ui.findChild(ToggleSwitch, "toggle_switch") + scan_control = self.ui.scan_control + self.waveform.toolbar.populate_toolbar( + { + "label": WidgetAction(label="Enable DAP ROI", widget=toggle_switch), + "scan_control": WidgetAction(widget=scan_control), + }, + self.waveform, + ) + + def _setup_arrow_item(self) -> None: + """Setup the arrow item""" + self.waveform.waveform.motor_pos_tick.add_to_plot() + positioner_line = self.ui.findChild(PositionerControlLine) + positioner_line.position_update.connect(self.waveform.waveform.motor_pos_tick.set_position) + try: + pos = float(positioner_line.ui.readback.text()) + except ValueError: + pos = 0 + self.waveform.waveform.motor_pos_tick.set_position(pos) + + def _setup_motor_combobox(self) -> None: + """Setup motor selection""" + # FIXME after changing the filtering in the combobox + motors = [name for name in self.dev if isinstance(self.dev.get(name), Positioner)] + self.ui.device_combobox.setCurrentText(motors[0]) + self.ui.device_combobox.set_device_filter("Positioner") + + def _setup_signal_combobox(self) -> None: + """Setup signal selection""" + # FIXME after changing the filtering in the combobox + signals = [name for name in self.dev if isinstance(self.dev.get(name), Signal)] + self.ui.device_combobox_2.setCurrentText(signals[0]) + self.ui.device_combobox_2.set_device_filter("Signal") + + def _setup_scan_control(self) -> None: + """Setup scan control, connect spin and check boxes to the scan_control widget""" + device_line_edit = self.ui.scan_control.arg_box.findChild(DeviceLineEdit) + self.ui.device_combobox.currentTextChanged.connect(device_line_edit.setText) + device_line_edit.setText(self.ui.device_combobox.currentText()) + spin_boxes = self.ui.scan_control.arg_box.findChildren(QDoubleSpinBox) + start = self.ui.findChild(QDoubleSpinBox, "linescan_start") + start.valueChanged.connect(spin_boxes[0].setValue) + stop = self.ui.findChild(QDoubleSpinBox, "linescan_stop") + stop.valueChanged.connect(spin_boxes[1].setValue) + step = self.ui.findChild(QSpinBox, "linescan_step") + step.valueChanged.connect( + self.ui.scan_control.kwarg_boxes[0].findChildren(QSpinBox)[0].setValue + ) + exp_time = self.ui.findChild(QDoubleSpinBox, "linescan_exp_time") + exp_time.valueChanged.connect( + self.ui.scan_control.kwarg_boxes[1].findChildren(QDoubleSpinBox)[0].setValue + ) + relative = self.ui.findChild(QCheckBox, "linescan_relative") + relative.toggled.connect( + self.ui.scan_control.kwarg_boxes[0].findChildren(QCheckBox)[0].setChecked + ) + + def _setup_progress_bar(self) -> None: + """Setup progress bar""" + # FIXME once the BECScanProgressBar is implemented + self.progress_bar = self.ui.findChild(BECProgressBar, "bec_progress_bar") + self.progress_bar.set_value(0) + self.ui.bec_waveform_widget.new_scan.connect(self._reset_progress_bar) + self.bec_dispatcher.connect_slot( + self._update_progress_bar, MessageEndpoints.scan_progress() + ) + + +if __name__ == "__main__": + import sys + + from qtpy.QtWidgets import QApplication + + app = QApplication(sys.argv) + window = Alignment1D() + window.show() + sys.exit(app.exec_()) diff --git a/bec_widgets/applications/alignment/alignment_1d/alignment_1d.ui b/bec_widgets/applications/alignment/alignment_1d/alignment_1d.ui new file mode 100644 index 00000000..6f17e8c0 --- /dev/null +++ b/bec_widgets/applications/alignment/alignment_1d/alignment_1d.ui @@ -0,0 +1,779 @@ + + + Form + + + + 0 + 0 + 1578 + 962 + + + + Form + + + + 6 + + + 12 + + + 0 + + + 12 + + + + + + + + 0 + 0 + + + + 0 + + + + Alignment Control + + + + 4 + + + 4 + + + 4 + + + 4 + + + + + + 0 + 0 + + + + line_scan + + + true + + + true + + + true + + + true + + + true + + + true + + + false + + + + + + + + + + 600 + 95 + + + + + 600 + 95 + + + + + 15 + true + + + + false + + + Devices + + + + 8 + + + 8 + + + 8 + + + 8 + + + + + + + + LMFit Model + + + + + + + + + + + + + Motor + + + + + + + + + + + + + Monitor + + + + + + + + + + + + + + 600 + 191 + + + + + 600 + 191 + + + + + 15 + true + + + + 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 + + + + + + + + + + + + + + + + + + 600 + 201 + + + + + 600 + 201 + + + + + 15 + true + + + + Motor Tweak + + + + 0 + + + 12 + + + 0 + + + 12 + + + + + true + + + + + + + + + + + 600 + 85 + + + + + 600 + 85 + + + + + 15 + true + + + + Utils + + + + + + Send summary to SciLog + + + + + + + + + + + + + + + 600 + 0 + + + + true + + + + + + + + 3 + 0 + + + + Activate linear region select for LMFit + + + Qt::LayoutDirection::LeftToRight + + + false + + + + + + + + + true + + + true + + + true + + + + + + + + + + + + Logbook + + + + + + https://scilog.psi.ch/login + + + + + + + + + + + + + + + + + + + + + + + + DapComboBox + QWidget +
dap_combo_box
+
+ + StopButton + QWidget +
stop_button
+
+ + WebsiteWidget + QWidget +
website_widget
+
+ + ScanControl + QWidget +
scan_control
+
+ + ToggleSwitch + QWidget +
toggle_switch
+
+ + PositionerBox + QWidget +
positioner_box
+
+ + PositionerControlLine + PositionerBox +
positioner_control_line
+
+ + BECProgressBar + QWidget +
bec_progress_bar
+
+ + BECWaveformWidget + QWidget +
bec_waveform_widget
+
+ + DeviceComboBox + QComboBox +
device_combobox
+
+ + LMFitDialog + QWidget +
lm_fit_dialog
+
+ + AbortButton + QWidget +
abort_button
+
+
+ + tabWidget + device_combobox + device_combobox_2 + linescan_start + linescan_stop + linescan_step + linescan_relative + linescan_exp_time + linescan_step_2 + scan_button + + + + + device_combobox + currentTextChanged(QString) + dap_combo_box + select_x_axis(QString) + + + 214 + 130 + + + 629 + 130 + + + + + device_combobox_2 + currentTextChanged(QString) + dap_combo_box + select_y_axis(QString) + + + 388 + 130 + + + 629 + 130 + + + + + dap_combo_box + new_dap_config(QString,QString,QString) + bec_waveform_widget + add_dap(QString,QString,QString) + + + 629 + 130 + + + 1099 + 221 + + + + + device_combobox + currentTextChanged(QString) + positioner_control_line + set_positioner(QString) + + + 214 + 130 + + + 174 + 550 + + + + + scan_button + clicked() + scan_control + run_scan() + + + 332 + 336 + + + 28 + 319 + + + + + device_combobox_2 + currentTextChanged(QString) + bec_waveform_widget + plot(QString) + + + 388 + 130 + + + 1099 + 201 + + + + + device_combobox + currentTextChanged(QString) + bec_waveform_widget + set_x(QString) + + + 214 + 130 + + + 1099 + 258 + + + + + toggle_switch + enabled(bool) + bec_waveform_widget + toogle_roi_select(bool) + + + 691 + 536 + + + 1099 + 96 + + + + + bec_waveform_widget + dap_summary_update(QVariantMap,QVariantMap) + lm_fit_dialog + update_summary_tree(QVariantMap,QVariantMap) + + + 1099 + 258 + + + 1098 + 668 + + + + +
diff --git a/bec_widgets/utils/plot_indicator_items.py b/bec_widgets/utils/plot_indicator_items.py new file mode 100644 index 00000000..8121c0ec --- /dev/null +++ b/bec_widgets/utils/plot_indicator_items.py @@ -0,0 +1,151 @@ +"""Module to create an arrow item for a pyqtgraph plot""" + +import pyqtgraph as pg +from qtpy.QtCore import QObject, Qt, Signal, Slot + +from bec_widgets.utils.colors import get_accent_colors + + +class BECIndicatorItem(QObject): + + def __init__(self, plot_item: pg.PlotItem = None, parent=None): + super().__init__(parent=parent) + self.accent_colors = get_accent_colors() + self.plot_item = plot_item + + def add_to_plot(self) -> None: + """Add the item to the plot""" + raise NotImplementedError("Method add_to_plot not implemented") + + def remove_from_plot(self) -> None: + """Remove the item from the plot""" + raise NotImplementedError("Method remove_from_plot not implemented") + + def set_position(self, pos: tuple[float, float] | float) -> None: + """Set the position of the item""" + raise NotImplementedError("Method set_position not implemented") + + def cleanup(self) -> None: + """Cleanup the item""" + self.remove_from_plot() + self.deleteLater() + + +class BECTickItem(BECIndicatorItem): + """Class to create a tick item which can be added to a pyqtgraph plot. + The tick item will be added to the layout of the plot item and can be used to indicate + a position""" + + position_changed = Signal(float) + position_changed_str = Signal(str) + + def __init__(self, plot_item: pg.PlotItem = None, parent=None): + super().__init__(plot_item=plot_item, parent=parent) + self.tick_item = pg.TickSliderItem(allowAdd=False, allowRemove=False) + self.tick = None + self._tick_pos = 0 + self._range = [0, 1] + + @Slot(float) + def set_position(self, pos: float) -> None: + """Set the position of the tick item + + Args: + pos (float): The position of the tick item. + """ + self._tick_pos = pos + self.update_range(self.plot_item.vb, self._range) + + def update_range(self, vb, viewRange) -> None: + """Update the range of the tick item + + Args: + vb (pg.ViewBox): The view box. + viewRange (tuple): The view range. + """ + origin = self.tick_item.tickSize / 2.0 + length = self.tick_item.length + + lengthIncludingPadding = length + self.tick_item.tickSize + 2 + + self._range = viewRange + + tickValueIncludingPadding = (self._tick_pos - viewRange[0]) / (viewRange[1] - viewRange[0]) + tickValue = (tickValueIncludingPadding * lengthIncludingPadding - origin) / length + self.tick_item.setTickValue(self.tick, tickValue) + self.position_changed.emit(self._tick_pos) + self.position_changed_str.emit(str(self._tick_pos)) + + def add_to_plot(self): + """Add the tick item to the view box or plot item.""" + if self.plot_item is not None: + self.plot_item.layout.addItem(self.tick_item, 4, 1) + self.tick = self.tick_item.addTick(0, movable=False, color=self.accent_colors.highlight) + self.plot_item.vb.sigXRangeChanged.connect(self.update_range) + + def remove_from_plot(self): + """Remove the tick item from the view box or plot item.""" + if self.plot_item is not None: + self.plot_item.layout.removeItem(self.tick_item) + + +class BECArrowItem(BECIndicatorItem): + """Class to create an arrow item which can be added to a pyqtgraph plot. + It can be either added directly to a view box or a plot item. + To add the arrow item to a view box or plot item, use the add_to_plot method. + + Args: + view_box (pg.ViewBox | pg.PlotItem): The view box or plot item to which the arrow item should be added. + parent (QObject): The parent object. + + Signals: + position_changed (tuple[float, float]): Signal emitted when the position of the arrow item has changed. + position_changed_str (tuple[str, str]): Signal emitted when the position of the arrow item has changed. + """ + + # Signal to emit if the position of the arrow item has changed + position_changed = Signal(tuple) + position_changed_str = Signal(tuple) + + def __init__(self, plot_item: pg.PlotItem = None, parent=None): + super().__init__(plot_item=plot_item, parent=parent) + self.arrow_item = pg.ArrowItem() + + @Slot(dict) + def set_style(self, style: dict) -> None: + """Set the style of the arrow item + + Args: + style (dict): The style of the arrow item. Dictionary with key, + value pairs which are accepted from the pg.ArrowItem.setStyle method. + """ + self.arrow_item.setStyle(**style) + + @Slot(tuple) + def set_position(self, pos: tuple[float, float]) -> None: + """Set the position of the arrow item + + Args: + pos (tuple): The position of the arrow item as a tuple (x, y). + """ + pos_x = pos[0] + pos_y = pos[1] + self.arrow_item.setPos(pos_x, pos_y) + self.position_changed.emit((pos_x, pos_y)) + self.position_changed_str.emit((str(pos_x), str(pos_y))) + + def add_to_plot(self): + """Add the arrow item to the view box or plot item.""" + self.arrow_item.setStyle( + angle=-90, + pen=pg.mkPen(self.accent_colors.emergency, width=1), + brush=pg.mkBrush(self.accent_colors.highlight), + headLen=20, + ) + if self.plot_item is not None: + self.plot_item.addItem(self.arrow_item) + + def remove_from_plot(self): + """Remove the arrow item from the view box or plot item.""" + if self.plot_item is not None: + self.plot_item.removeItem(self.arrow_item) diff --git a/bec_widgets/widgets/figure/plots/plot_base.py b/bec_widgets/widgets/figure/plots/plot_base.py index c3994d9f..7f2279b9 100644 --- a/bec_widgets/widgets/figure/plots/plot_base.py +++ b/bec_widgets/widgets/figure/plots/plot_base.py @@ -12,6 +12,7 @@ from qtpy.QtWidgets import QApplication, QWidget from bec_widgets.utils import BECConnector, ConnectionConfig from bec_widgets.utils.crosshair import Crosshair +from bec_widgets.utils.plot_indicator_items import BECArrowItem, BECTickItem logger = bec_logger.logger @@ -105,6 +106,8 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout): self.add_legend() self.crosshair = None + self.motor_pos_tick = BECTickItem(parent=self, plot_item=self.plot_item) + self.arrow_item = BECArrowItem(parent=self, plot_item=self.plot_item) self._connect_to_theme_change() def _connect_to_theme_change(self): @@ -428,6 +431,8 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout): def cleanup_pyqtgraph(self): """Cleanup pyqtgraph items.""" self.unhook_crosshair() + self.motor_pos_tick.cleanup() + self.arrow_item.cleanup() item = self.plot_item item.vb.menu.close() item.vb.menu.deleteLater() diff --git a/bec_widgets/widgets/positioner_box/positioner_box.py b/bec_widgets/widgets/positioner_box/positioner_box.py index d95327ee..c8f00c41 100644 --- a/bec_widgets/widgets/positioner_box/positioner_box.py +++ b/bec_widgets/widgets/positioner_box/positioner_box.py @@ -33,6 +33,8 @@ class PositionerBox(BECWidget, QWidget): ICON_NAME = "switch_right" USER_ACCESS = ["set_positioner"] device_changed = Signal(str, str) + # Signal emitted to inform listeners about a position update + position_update = Signal(float) def __init__(self, parent=None, device: Positioner = None, **kwargs): """Initialize the PositionerBox widget. @@ -245,6 +247,7 @@ class PositionerBox(BECWidget, QWidget): if readback_val is not None: self.ui.readback.setText(f"{readback_val:.{precision}f}") + self.position_update.emit(readback_val) if setpoint_val is not None: self.ui.setpoint.setText(f"{setpoint_val:.{precision}f}") diff --git a/tests/unit_tests/test_utils_plot_indicators.py b/tests/unit_tests/test_utils_plot_indicators.py new file mode 100644 index 00000000..46282de6 --- /dev/null +++ b/tests/unit_tests/test_utils_plot_indicators.py @@ -0,0 +1,90 @@ +import pyqtgraph as pg +import pytest +from bec_qthemes._main import AccentColors + +from bec_widgets.utils.plot_indicator_items import BECArrowItem, BECTickItem + + +@pytest.fixture(scope="function") +def arrow_item(): + """Fixture for the BECArrowItem class""" + item = BECArrowItem(plot_item=pg.PlotItem()) + yield item + + +def test_arrow_item_add_to_plot(arrow_item): + """Test the add_to_plot method""" + assert arrow_item.plot_item is not None + assert arrow_item.plot_item.items == [] + arrow_item.accent_colors = AccentColors(theme="dark") + arrow_item.add_to_plot() + assert arrow_item.plot_item.items == [arrow_item.arrow_item] + + +def test_arrow_item_remove_to_plot(arrow_item): + """Test the remove_from_plot method""" + arrow_item.accent_colors = AccentColors(theme="dark") + arrow_item.add_to_plot() + assert arrow_item.plot_item.items == [arrow_item.arrow_item] + arrow_item.remove_from_plot() + assert arrow_item.plot_item.items == [] + + +def test_arrow_item_set_position(arrow_item): + """Test the set_position method""" + container = [] + + def signal_callback(tup: tuple): + container.append(tup) + + arrow_item.accent_colors = AccentColors(theme="dark") + arrow_item.add_to_plot() + arrow_item.position_changed.connect(signal_callback) + arrow_item.set_position(pos=(1, 1)) + assert arrow_item.arrow_item.pos().toTuple() == (1, 1) + arrow_item.set_position(pos=(2, 2)) + assert arrow_item.arrow_item.pos().toTuple() == (2, 2) + assert container == [(1, 1), (2, 2)] + + +@pytest.fixture(scope="function") +def tick_item(): + """Fixture for the BECArrowItem class""" + item = BECTickItem(plot_item=pg.PlotItem()) + yield item + + +def test_tick_item_add_to_plot(tick_item): + """Test the add_to_plot method""" + assert tick_item.plot_item is not None + assert tick_item.plot_item.items == [] + tick_item.accent_colors = AccentColors(theme="dark") + tick_item.add_to_plot() + assert tick_item.plot_item.layout.itemAt(4, 1) == tick_item.tick_item + + +def test_tick_item_remove_to_plot(tick_item): + """Test the remove_from_plot method""" + tick_item.accent_colors = AccentColors(theme="dark") + tick_item.add_to_plot() + assert tick_item.plot_item.layout.itemAt(4, 1) == tick_item.tick_item + tick_item.remove_from_plot() + assert tick_item.plot_item.layout.itemAt(4, 1) is None + + +def test_tick_item_set_position(tick_item): + """Test the set_position method""" + container = [] + + def signal_callback(val: float): + container.append(val) + + tick_item.accent_colors = AccentColors(theme="dark") + tick_item.add_to_plot() + tick_item.position_changed.connect(signal_callback) + + tick_item.set_position(pos=1) + assert tick_item._tick_pos == 1 + tick_item.set_position(pos=2) + assert tick_item._tick_pos == 2 + assert container == [1.0, 2.0]