From 606f1b62fbb11969268d261cba6c4a217636b405 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Sun, 29 Jun 2025 12:19:47 +0200 Subject: [PATCH] GUI: Add completed grid scan table --- common/src/aaredaqlib/raster_grid.py | 2 + gui/src/aaregui/main_window.py | 3 + .../aaregui/panels/raster_data_collection.py | 8 +- .../aaregui/scan_logic/raster_grid_manager.py | 75 ++++++++--- gui/src/aaregui/widgets/raster_grid_table.py | 121 ++++++++++++++++++ 5 files changed, 190 insertions(+), 19 deletions(-) create mode 100644 gui/src/aaregui/widgets/raster_grid_table.py diff --git a/common/src/aaredaqlib/raster_grid.py b/common/src/aaredaqlib/raster_grid.py index 655d9377..0aaad8fa 100644 --- a/common/src/aaredaqlib/raster_grid.py +++ b/common/src/aaredaqlib/raster_grid.py @@ -26,6 +26,8 @@ class RasterGridRequest(BaseModel): # omega angle omega_deg: float = 0.0 + visible: bool = True + def get_image_number(self) -> int: return self.n_x * self.n_y diff --git a/gui/src/aaregui/main_window.py b/gui/src/aaregui/main_window.py index f81ecf1f..d44c41be 100644 --- a/gui/src/aaregui/main_window.py +++ b/gui/src/aaregui/main_window.py @@ -147,6 +147,9 @@ class MainWindow(QMainWindow): self.beamline.smargon_panel.smargon.connect(self.daq.move_smargon) self.camera_image.samcam_updated.connect(self.daq.samcam_settings) + self.raster.omega.connect(self.daq.set_omega) + self.raster.smargon.connect(self.daq.move_smargon) + self.beamline.omega_panel.set_omega.connect(self.daq.set_omega) self.camera_image.set_omega.connect(self.daq.set_omega) self.beamline.zoom_panel.zoom.connect(self.daq.zoom) diff --git a/gui/src/aaregui/panels/raster_data_collection.py b/gui/src/aaregui/panels/raster_data_collection.py index b3a89271..05dcb67b 100644 --- a/gui/src/aaregui/panels/raster_data_collection.py +++ b/gui/src/aaregui/panels/raster_data_collection.py @@ -6,6 +6,7 @@ from aaredaqlib.diffraction_geometry import DiffractionGeometry from aaregui.panels.scan_settings_panel import ScanSettingsPanel from aaregui.scan_logic.raster_grid_manager import RasterGridManager, RasterGridMetric from aaregui.widgets.number_line_edit import NumberLineEdit +from aaregui.widgets.raster_grid_table import RasterGridTable class RasterDataCollectionPanel(ScanSettingsPanel): @@ -97,15 +98,18 @@ class RasterDataCollectionPanel(ScanSettingsPanel): slider.valueChanged.connect(lambda: self.raster_alpha_changed.emit(255 - slider.value())) self._layout.addWidget(slider, 8, 1, 1, 3) + self._table = RasterGridTable(raster_mgr) + self._layout.addWidget(self._table, 9, 0, 1, 5) + horizontal_spacer = QSpacerItem( 40, 20, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding ) - self._layout.addItem(horizontal_spacer, 9, 0, 1, 4) + self._layout.addItem(horizontal_spacer, 10, 0, 1, 5) self.start_button = QPushButton("Evaluate grid") self.start_button.setStyleSheet("color: rgb(78, 154, 6);") self.start_button.clicked.connect(self.evaluate_grid) - self._layout.addWidget(self.start_button, 10, 0, 1, 5) + self._layout.addWidget(self.start_button, 11, 0, 1, 5) self.update_grid_scan_size() diff --git a/gui/src/aaregui/scan_logic/raster_grid_manager.py b/gui/src/aaregui/scan_logic/raster_grid_manager.py index df2ed287..63a90ae2 100644 --- a/gui/src/aaregui/scan_logic/raster_grid_manager.py +++ b/gui/src/aaregui/scan_logic/raster_grid_manager.py @@ -72,10 +72,14 @@ class RasterGridManager(QObject): grid_updated = Signal() grid_scan = Signal(RasterGridRequest) grid_scan_size_changed = Signal(int, int) + completed_grid_updated = Signal() file_ready = Signal(str) image_selected = Signal(int) + omega = Signal(float) + smargon = Signal(SmargonCoordinate) + def __init__(self, geom: SampleGeometryModel): super().__init__() @@ -109,9 +113,10 @@ class RasterGridManager(QObject): def _is_grid_visible(self, grid: RasterGridRequest): if ( - abs(normalize_angle(grid.omega_deg - self.__geom.omega_deg)) < 1.0 - and abs(grid.smargon.phi_deg - self.__geom.smargon.phi_deg) < 1.0 - and abs(grid.smargon.chi_deg - self.__geom.smargon.chi_deg) < 1.0 + grid.visible + and abs(normalize_angle(grid.omega_deg - self.__geom.omega_deg)) < 0.2 + and abs(grid.smargon.phi_deg - self.__geom.smargon.phi_deg) < 0.2 + and abs(grid.smargon.chi_deg - self.__geom.smargon.chi_deg) < 0.2 and grid.n_x > 0 and grid.n_y > 0 ): @@ -182,10 +187,6 @@ class RasterGridManager(QObject): self.__active_grid.n_y = 0 self.grid_scan_size_changed.emit(self.__active_grid.n_x, self.__active_grid.n_y) - @Slot() - def clear_completed_grids(self): - self.__completed_grids.clear() - def get_grid_coord(self, grid: RasterGridRequest, point: QPointF) -> Tuple[int, int]: point_bl = self.__geom.picture_to_sample(Coordinate(x=point.x(), y=point.y())) @@ -227,7 +228,7 @@ class RasterGridManager(QObject): 0 <= delta.y < self.__active_grid.n_y * self.__active_grid.grid_size_mm.y ) - def is_part_of_completed_grid(self, point: QPointF) -> str: + def is_part_of_completed_grid(self, point: QPointF) -> str | None: for grid in self.__completed_grids: if self._is_grid_visible(grid.request): n_x = grid.request.n_x @@ -259,7 +260,7 @@ class RasterGridManager(QObject): txt += f"Mosaicity {grid.result.images[cell].mos:.2f}" return txt - return "" + return None @Slot(float, float) @@ -279,14 +280,6 @@ class RasterGridManager(QObject): grid = copy.deepcopy(self.__active_grid) self.grid_scan.emit(grid) - @Slot(CompletedRasterGrid) - def grid_scan_completed(self, r: CompletedRasterGrid): - self.__completed_grids.append(r) - self.__active_grid.n_x = 0 - self.__active_grid.n_y = 0 - if r.result.file_prefix is not None: - self.file_ready.emit(r.result.file_prefix) - def draw_grid(self, painter: QPainter, alpha: int = 127): self._draw_grid(painter, self.__active_grid) for i in self.__completed_grids: @@ -362,3 +355,51 @@ class RasterGridManager(QObject): @Slot(RasterGridMetric) def metric(self, r: RasterGridMetric): self.__metric = r + + @Slot() + def clear_completed_grids(self): + self.__completed_grids.clear() + self.completed_grid_updated.emit() + + @Slot(CompletedRasterGrid) + def grid_scan_completed(self, r: CompletedRasterGrid): + self.__completed_grids.append(r) + self.__active_grid.n_x = 0 + self.__active_grid.n_y = 0 + if r.result.file_prefix is not None: + self.file_ready.emit(r.result.file_prefix) + self.completed_grid_updated.emit() + + @Slot(int, bool) + def completed_grid_scan_toggle_visible(self, row: int, visible: bool): + if 0 <= row < len(self.__completed_grids): + self.__completed_grids[row].request.visible = visible + self.completed_grid_updated.emit() + + @Slot(int) + def completed_grid_scan_delete(self, row: int): + if 0 <= row < len(self.__completed_grids): + del self.__completed_grids[row] + self.completed_grid_updated.emit() + + @Slot(int) + def completed_grid_scan_goto(self, row: int): + if 0 <= row < len(self.__completed_grids): + self.omega.emit(self.__completed_grids[row].request.omega_deg) + self.smargon.emit(SmargonCoordinate( + phi_deg=self.__completed_grids[row].request.smargon.phi_deg, + chi_deg=self.__completed_grids[row].request.smargon.chi_deg, + )) + + @Slot(int) + def completed_grid_redo(self, row: int): + if 0 <= row < len(self.__completed_grids): + self.__active_grid = copy.deepcopy(self.__completed_grids[row].request) + self.__active_grid.visible = True + self.__completed_grids[row].request.visible = False + + self.grid_scan_size_changed.emit(self.__active_grid.n_x, self.__active_grid.n_y) + self.completed_grid_updated.emit() + + def get_completed_grids(self) -> List[CompletedRasterGrid]: + return self.__completed_grids \ No newline at end of file diff --git a/gui/src/aaregui/widgets/raster_grid_table.py b/gui/src/aaregui/widgets/raster_grid_table.py new file mode 100644 index 00000000..dd24e0d4 --- /dev/null +++ b/gui/src/aaregui/widgets/raster_grid_table.py @@ -0,0 +1,121 @@ +from PySide6.QtCore import Signal, Slot +from PySide6.QtWidgets import QTableWidget, QHeaderView, QTableWidgetItem, QWidget, QHBoxLayout, QPushButton + +from aaredaqlib.raster_grid import RasterGridRequest +from aaregui.scan_logic.raster_grid_manager import RasterGridManager + + +class RasterGridTable(QTableWidget): + copy_clicked = Signal(int) + delete_clicked = Signal(int) + visible_clicked = Signal(int, bool) + goto_clicked = Signal(int) + + def __init__(self, raster_mgr: RasterGridManager, parent=None): + super().__init__(parent) + self.__raster_mgr = raster_mgr + + self.setColumnCount(5) + self.setHorizontalHeaderLabels(['Omega', 'Chi', 'Phi', 'Size (WxH)', 'Actions']) + + self.verticalHeader().setVisible(False) + + # Configure header + header = self.horizontalHeader() + for i in range(4): + header.setSectionResizeMode(i, QHeaderView.ResizeMode.ResizeToContents) + header.setSectionResizeMode(4, QHeaderView.ResizeMode.Stretch) + + # Set minimum height + self.setMinimumHeight(100) + + # Connect to raster manager signals + self.__raster_mgr.completed_grid_updated.connect(self.refresh_table) + self.copy_clicked.connect(self.__raster_mgr.completed_grid_redo) + self.delete_clicked.connect(self.__raster_mgr.completed_grid_scan_delete) + self.visible_clicked.connect(self.__raster_mgr.completed_grid_scan_toggle_visible) + self.goto_clicked.connect(self.__raster_mgr.completed_grid_scan_goto) + + def add_rows(self): + for r in self.__raster_mgr.get_completed_grids(): + req = r.request + + row_position = self.rowCount() + self.insertRow(row_position) + + # Add data cells + self.setItem(row_position, 0, QTableWidgetItem(f"{req.omega_deg:.1f}")) + self.setItem(row_position, 1, QTableWidgetItem(f"{req.smargon.chi_deg:.1f}")) + self.setItem(row_position, 2, QTableWidgetItem(f"{req.smargon.phi_deg:.1f}")) + self.setItem(row_position, 3, QTableWidgetItem(f"{req.n_x} x {req.n_y}")) + + # Add action buttons + self.add_action_buttons(row_position, req.visible) + + + def add_action_buttons(self, row: int, visible: bool): + """Create and add action buttons for the specified row.""" + actions_widget = QWidget() + actions_layout = QHBoxLayout(actions_widget) + actions_layout.setContentsMargins(4, 4, 4, 4) + actions_layout.setSpacing(4) + + button_style = """ + QPushButton { + border: none; + background: transparent; + font-size: 14px; + } + QPushButton:hover { + background-color: #e0e0e0; + border-radius: 3px; + } + """ + + # Copy button + copy_btn = QPushButton('⧉') + copy_btn.setFixedSize(24, 24) + copy_btn.setToolTip('Copy') + copy_btn.setStyleSheet(button_style) + copy_btn.clicked.connect(lambda checked, r=row: self.copy_clicked.emit(r)) + actions_layout.addWidget(copy_btn) + + # Redo button + goto_btn = QPushButton('🏃‍♂️') # Circular arrow for goto + goto_btn.setFixedSize(24, 24) + goto_btn.setToolTip('Goto scan') + goto_btn.setStyleSheet(button_style) + goto_btn.clicked.connect(lambda checked, r=row: self.goto_clicked.emit(r)) + actions_layout.addWidget(goto_btn) + + # Visible button + if visible: + visible_btn = QPushButton('👁') + else: + visible_btn = QPushButton('‍🗨') + visible_btn.setChecked(visible) # Initially visible + + visible_btn.setFixedSize(24, 24) + visible_btn.setToolTip('Toggle visibility') + visible_btn.setStyleSheet(button_style) + visible_btn.setCheckable(True) + visible_btn.clicked.connect(lambda checked, r=row: self.visible_clicked.emit(r, not visible)) + actions_layout.addWidget(visible_btn) + + # Delete button + delete_btn = QPushButton('🗑') + delete_btn.setFixedSize(24, 24) + delete_btn.setToolTip('Delete') + delete_btn.setStyleSheet(button_style) + delete_btn.clicked.connect(lambda checked, r=row: self.delete_clicked.emit(r)) + actions_layout.addWidget(delete_btn) + + self.setCellWidget(row, 4, actions_widget) + + @Slot() + def refresh_table(self): + """Refresh the entire table content.""" + # Clear existing rows + self.setRowCount(0) + # Repopulate with current data + self.add_rows()