diff --git a/bec_widgets/examples/__init__.py b/bec_widgets/examples/__init__.py index e69de29b..6d508776 100644 --- a/bec_widgets/examples/__init__.py +++ b/bec_widgets/examples/__init__.py @@ -0,0 +1,6 @@ +from .motor_movement import ( + MotorControlMap, + MotorControlPanel, + MotorControlPanelRelative, + MotorControlPanelAbsolute, +) diff --git a/bec_widgets/examples/motor_movement/__init__.py b/bec_widgets/examples/motor_movement/__init__.py index e69de29b..eb17d1fc 100644 --- a/bec_widgets/examples/motor_movement/__init__.py +++ b/bec_widgets/examples/motor_movement/__init__.py @@ -0,0 +1,6 @@ +from .motor_control_compilations import ( + MotorControlMap, + MotorControlPanel, + MotorControlPanelRelative, + MotorControlPanelAbsolute, +) diff --git a/bec_widgets/widgets/__init__.py b/bec_widgets/widgets/__init__.py index 54387b6f..263fff13 100644 --- a/bec_widgets/widgets/__init__.py +++ b/bec_widgets/widgets/__init__.py @@ -9,4 +9,5 @@ from .motor_control import ( MotorControlAbsolute, MotorControlSelection, MotorThread, + MotorCoordinateTable, ) diff --git a/bec_widgets/widgets/motor_control/__init__.py b/bec_widgets/widgets/motor_control/__init__.py index 4a259ca2..fe609636 100644 --- a/bec_widgets/widgets/motor_control/__init__.py +++ b/bec_widgets/widgets/motor_control/__init__.py @@ -3,4 +3,5 @@ from .motor_control import ( MotorControlAbsolute, MotorControlSelection, MotorThread, + MotorCoordinateTable, ) diff --git a/bec_widgets/widgets/motor_control/motor_control.py b/bec_widgets/widgets/motor_control/motor_control.py index 4556b5b3..75b0d997 100644 --- a/bec_widgets/widgets/motor_control/motor_control.py +++ b/bec_widgets/widgets/motor_control/motor_control.py @@ -3,6 +3,8 @@ import os from enum import Enum import qdarktheme +from PyQt6.QtGui import QDoubleValidator +from PyQt6.QtWidgets import QPushButton, QTableWidgetItem, QCheckBox, QLineEdit from qtpy import uic from qtpy.QtCore import QThread, Slot as pyqtSlot @@ -10,10 +12,13 @@ from qtpy.QtCore import Signal as pyqtSignal, Qt from qtpy.QtGui import QKeySequence from qtpy.QtWidgets import ( QApplication, + QHeaderView, QWidget, QSpinBox, QDoubleSpinBox, QShortcut, + QVBoxLayout, + QTableWidget, ) from qtpy.QtWidgets import QApplication, QMessageBox @@ -205,7 +210,6 @@ class MotorControlAbsolute(MotorControlWidget): # Enable/Disable GUI self.motor_thread.lock_gui.connect(self.enable_motor_controls) - # self.motor_thread.move_finished.connect(lambda: self._enable_motor_controls(True)) # Error messages self.motor_thread.motor_error.connect( @@ -508,6 +512,237 @@ class MotorControlRelative(MotorControlWidget): self.motor_thread.move_relative(motor, step) +class MotorCoordinateTable(MotorControlWidget): + plot_coordinates_signal = pyqtSignal(list) + + def _load_ui(self): + """Load the UI for the coordinate table.""" + current_path = os.path.dirname(__file__) + uic.loadUi(os.path.join(current_path, "motor_control_table.ui"), self) + + def _init_ui(self): + """Initialize the UI""" + # Setup table behaviour + self._setup_table() + self.table.setSelectionBehavior(QTableWidget.SelectRows) + + # for tag columns default tag + self.tag_counter = 1 + + # Connect signals and slots + self.checkBox_resize_auto.stateChanged.connect(self.resize_table_auto) + + # Keyboard shortcuts for deleting a row + self.delete_shortcut = QShortcut(QKeySequence(Qt.Key_Delete), self.table) + self.delete_shortcut.activated.connect(self.delete_selected_row) + self.backspace_shortcut = QShortcut(QKeySequence(Qt.Key_Backspace), self.table) + self.backspace_shortcut.activated.connect(self.delete_selected_row) + + @pyqtSlot(dict) + def on_config_update(self, config: dict) -> None: + """ + Update config dict + Args: + config(dict): New config dict + """ + self.config = config + + # Get motor names + self.motor_x, self.motor_y = ( + self.config["motor_control"]["motor_x"], + self.config["motor_control"]["motor_y"], + ) + + # Decimal precision of the table coordinates + self.precision = self.config["motor_control"].get("precision", 3) + + # Mode switch default option + self.mode = self.config["motor_control"].get("mode", "Individual") + + # Set combobox to default mode + self.mode_combobox.setCurrentText(self.mode) + + self._init_ui() + + def _setup_table(self): + """Setup the table with appropriate headers and configurations.""" + mode = self.mode_combobox.currentText() + + if mode == "Individual": + self._setup_individual_mode() + elif mode == "Start/Stop": + self._setup_start_stop_mode() + self.start_stop_counter = 0 + + def _setup_individual_mode(self): + self.table.setColumnCount(5) + self.table.setHorizontalHeaderLabels(["Show", "Move", "Tag", "X", "Y"]) + self.table.horizontalHeader().setStretchLastSection(True) + self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + self.table.verticalHeader().setVisible(False) + + def _setup_start_stop_mode(self): + self.table.setColumnCount(8) + self.table.setHorizontalHeaderLabels( + [ + "Show", + "Move [start]", + "Move [end]" "Tag", + "X [start]", + "Y [start]", + "X [end]", + "Y [end]", + ] + ) + self.table.horizontalHeader().setStretchLastSection(True) + self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + self.table.verticalHeader().setVisible(False) + + @pyqtSlot(tuple) + def add_coordinate(self, coordinates: tuple): + """ + Add a coordinate to the table. + Args: + coordinates(tuple): Coordinates (x,y) to add to the table. + """ + tag = f"Pos {self.tag_counter}" + self.tag_counter += 1 + x, y = coordinates + self._add_row(tag, x, y) + + def _add_row(self, tag, x, y): + """Internal method to add a row to the table.""" + row_count = self.table.rowCount() + self.table.insertRow(row_count) + + # Checkbox for toggling visibility + show_checkbox = QCheckBox() + show_checkbox.setChecked(True) + show_checkbox.stateChanged.connect(self.emit_plot_coordinates) + self.table.setCellWidget(row_count, 0, show_checkbox) + + # TODO add mode switch recognision + # Move button + move_button = QPushButton("Move") + move_button.clicked.connect(self.handle_move_button_click) + self.table.setCellWidget(row_count, 1, move_button) + + # Tag + self.table.setItem(row_count, 2, QTableWidgetItem(tag)) + + # Adding validator + validator = QDoubleValidator() + validator.setDecimals(self.precision) + + # X as QLineEdit with validator + x_edit = QLineEdit(str(f"{x:.{self.precision}f}")) + x_edit.setValidator(validator) + self.table.setCellWidget(row_count, 3, x_edit) + x_edit.textChanged.connect(self.emit_plot_coordinates) + + # Y as QLineEdit with validator + y_edit = QLineEdit(str(f"{y:.{self.precision}f}")) + y_edit.setValidator(validator) + self.table.setCellWidget(row_count, 4, y_edit) + y_edit.textChanged.connect(self.emit_plot_coordinates) + + # Emit the coordinates to be plotted + self.emit_plot_coordinates() + + # Connect item edit to emit coordinates + self.table.itemChanged.connect(self.emit_plot_coordinates) + + # Auto table resize + self.resize_table_auto() + + # Align table center + self._align_table_center() + + def handle_move_button_click(self): + """Handle the click event of the move button.""" + button = self.sender() + row = self.table.indexAt(button.pos()).row() + + x = self.get_coordinate(row, 3) + y = self.get_coordinate(row, 4) + self.move_motor(x, y) + + # Emit updated coordinates to update the map + self.emit_plot_coordinates() + + def emit_plot_coordinates(self): + """Emit the coordinates to be plotted.""" + coordinates = [] + for row in range(self.table.rowCount()): + show = self.table.cellWidget(row, 0).isChecked() + x = self.get_coordinate(row, 3) + y = self.get_coordinate(row, 4) + + coordinates.append((x, y, show)) # (x, y, show_flag) + self.plot_coordinates_signal.emit(coordinates) + + def get_coordinate(self, row: int, column: int) -> float: + """ + Helper function to get the coordinate from the table QLineEdit cells. + Args: + row(int): Row of the table. + column(int): Column of the table. + Returns: + float: Value of the coordinate. + """ + edit = self.table.cellWidget(row, column) + if edit.text() is not None and edit.text() != "": + value = float(edit.text()) if edit else None + return value + + def delete_selected_row(self): + """Delete the selected row from the table.""" + selected_rows = self.table.selectionModel().selectedRows() + for row in selected_rows: + self.table.removeRow(row.row()) + self.emit_plot_coordinates() + + def resize_table_auto(self): + """Resize the table to fit the contents.""" + if self.checkBox_resize_auto.isChecked(): + self.table.resizeColumnsToContents() + + def _align_table_center(self) -> None: + for row in range(self.table.rowCount()): + for col in range(self.table.columnCount()): + item = self.table.item(row, col) + if item: + item.setTextAlignment(Qt.AlignCenter) + + def move_motor(self, x, y): + """Move the motor to the specified coordinates.""" + self.motor_thread.move_absolute(self.motor_x, self.motor_y, (x, y)) + + @pyqtSlot(str, str) + def change_motors(self, motor_x: str, motor_y: str): + """ + Change the active motors and update config. + Can be connected to the selected_motors_signal from MotorControlSelection. + Args: + motor_x(str): New motor X to be controlled. + motor_y(str): New motor Y to be controlled. + """ + self.motor_x = motor_x + self.motor_y = motor_y + self.config["motor_control"]["motor_x"] = motor_x + self.config["motor_control"]["motor_y"] = motor_y + + @pyqtSlot(int) + def set_precision(self, precision: int) -> None: + """ + Set the precision of the coordinates. + Args: + precision(int): Precision of the coordinates. + """ + self.precision = precision + self.config["motor_control"]["precision"] = precision + + class MotorControlErrors: """Class for displaying formatted error messages.""" diff --git a/bec_widgets/widgets/motor_control/motor_control_table.ui b/bec_widgets/widgets/motor_control/motor_control_table.ui new file mode 100644 index 00000000..f3a526c7 --- /dev/null +++ b/bec_widgets/widgets/motor_control/motor_control_table.ui @@ -0,0 +1,85 @@ + + + Form + + + + 0 + 0 + 676 + 667 + + + + Motor Coordinates Table + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Resize Auto + + + true + + + + + + + false + + + Edit Custom Column + + + + + + + Entries Mode: + + + + + + + false + + + + Individual + + + + + Start/Stop + + + + + + + + + + + + + + diff --git a/bec_widgets/widgets/motor_map/motor_map.py b/bec_widgets/widgets/motor_map/motor_map.py index df9df48d..d57f4599 100644 --- a/bec_widgets/widgets/motor_map/motor_map.py +++ b/bec_widgets/widgets/motor_map/motor_map.py @@ -513,6 +513,26 @@ class MotorMap(pg.GraphicsLayoutWidget): self.curves_data[plot_name]["highlight_V"].setPos(current_x) self.curves_data[plot_name]["highlight_H"].setPos(current_y) + @pyqtSlot(list) + def plot_saved_coordinates(self, coordinates): + """Plot the saved coordinates on the map.""" + for plot_name in self.plots: + plot = self.plots[plot_name] + + # Clear previous saved points + if "saved_points" in self.curves_data[plot_name]: + plot.removeItem(self.curves_data[plot_name]["saved_points"]) + + # Filter coordinates to be shown + visible_coords = [coord[:2] for coord in coordinates if coord[2]] + + if visible_coords: + saved_points = pg.ScatterPlotItem( + pos=np.array(visible_coords), brush=pg.mkBrush(0, 255, 0, 255) + ) + plot.addItem(saved_points) + self.curves_data[plot_name]["saved_points"] = saved_points + @pyqtSlot(dict) def on_device_readback(self, msg: dict): """