apply fixes
This commit is contained in:
@@ -42,7 +42,7 @@ CSV export uses the same row fields, but additionally prefixes each row with `se
|
||||
|
||||
## Row Fields For Scan Macros
|
||||
|
||||
Each row currently uses `schema_version = 2`.
|
||||
Each row currently uses `schema_version = 3`.
|
||||
|
||||
Core scan fields:
|
||||
|
||||
@@ -73,6 +73,7 @@ Coordinate and motor context:
|
||||
| `x_positioner` / `y_positioner` | Motors selected for the ROI X/Y coordinates. |
|
||||
| `rotx_axis` / `roty_axis` | Rotation motors selected for tensor tomography rows. |
|
||||
| `rotx` / `roty` | Rotation angles for this row. They are `None` for non-tensor rows. |
|
||||
| `camera_center_x`, `camera_center_y` | X/Y motor positions that corresponded to the image reference point when the row was created. |
|
||||
|
||||
ROI and mapping provenance:
|
||||
|
||||
@@ -81,13 +82,24 @@ ROI and mapping provenance:
|
||||
| `raw_roi_x`, `raw_roi_y` | Raw ROI origin in image coordinates. |
|
||||
| `raw_roi_width`, `raw_roi_height` | Raw ROI size in image coordinates. |
|
||||
| `correction_scale_x`, `correction_scale_y` | Scale factors used to convert ROI coordinates to positions. |
|
||||
| `correction_offset_x`, `correction_offset_y` | Physical positions at the mapping reference point. |
|
||||
| `correction_offset_x`, `correction_offset_y` | Calibrated offsets from the camera reference point to the x-ray beam, in motor units. |
|
||||
| `correction_direction_x`, `correction_direction_y` | Direction signs, usually `1` or `-1`. |
|
||||
| `correction_reference_x`, `correction_reference_y` | Image coordinates used as the zero/reference point for the correction. New rows use the image center when image data exists, otherwise the ROI center. |
|
||||
| `correction_reference_x`, `correction_reference_y` | Image coordinates used as the camera reference point. New rows use the image center when image data exists, otherwise the ROI center. |
|
||||
|
||||
The mapping reference is reset to the image center only when a first image becomes available
|
||||
or the image shape changes. While no image data is available, it follows the ROI center.
|
||||
|
||||
Coordinate conversion follows this workflow:
|
||||
|
||||
```text
|
||||
camera-space motor position = camera_center + direction * scale * (raw_roi - reference)
|
||||
x-ray beam scan position = camera-space motor position + correction_offset
|
||||
```
|
||||
|
||||
`Move to selected` restores the saved raw ROI and moves back to `camera_center_x` /
|
||||
`camera_center_y`, i.e. the motor positions saved when the row was created. The exported table scan
|
||||
coordinates include both the scaled ROI displacement and the calibrated x-ray offset.
|
||||
|
||||
Bookkeeping and compatibility fields:
|
||||
|
||||
| Field | Meaning |
|
||||
@@ -121,7 +133,7 @@ def run_saxs_table(variable_message):
|
||||
rows = variable_message.value
|
||||
|
||||
for row in rows:
|
||||
if row.get("schema_version") != 2:
|
||||
if row.get("schema_version") != 3:
|
||||
raise ValueError(f"Unsupported SAXS row schema: {row.get('schema_version')}")
|
||||
|
||||
metadata = {
|
||||
@@ -135,6 +147,10 @@ def run_saxs_table(variable_message):
|
||||
"width": row["raw_roi_width"],
|
||||
"height": row["raw_roi_height"],
|
||||
},
|
||||
"camera_center": {
|
||||
"x": row["camera_center_x"],
|
||||
"y": row["camera_center_y"],
|
||||
},
|
||||
}
|
||||
|
||||
if row["template"] == "tensor":
|
||||
|
||||
@@ -221,6 +221,7 @@ class ROIMappingBox(QGroupBox):
|
||||
"""Controls for converting image ROI coordinates into scan coordinates."""
|
||||
|
||||
apply_requested = Signal()
|
||||
roi_size_requested = Signal(float, float)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__("ROI mapping", parent)
|
||||
@@ -241,6 +242,8 @@ class ROIMappingBox(QGroupBox):
|
||||
self.mapping_lock_toggle = ToggleSwitch(self, checked=False)
|
||||
self.reference_x = QLineEdit("0", self)
|
||||
self.reference_y = QLineEdit("0", self)
|
||||
self.roi_width = positive_spinbox(50.0, single_step=1.0, parent=self)
|
||||
self.roi_height = positive_spinbox(50.0, single_step=1.0, parent=self)
|
||||
for reference in [self.reference_x, self.reference_y]:
|
||||
reference.setObjectName("saxs_reference_field")
|
||||
reference.setReadOnly(True)
|
||||
@@ -269,6 +272,18 @@ class ROIMappingBox(QGroupBox):
|
||||
layout.addWidget(self.reference_y, 4, 2)
|
||||
layout.addWidget(field_label("x / y", self), 4, 3)
|
||||
|
||||
self.apply_roi_size_button = QPushButton("Apply ROI size", self)
|
||||
self.apply_roi_size_button.setIcon(material_icon("aspect_ratio", size=(16, 16)))
|
||||
self.apply_roi_size_button.setIconSize(QSize(18, 18))
|
||||
self.apply_roi_size_button.setMinimumHeight(32)
|
||||
self.apply_roi_size_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.apply_roi_size_button.setFlat(True)
|
||||
self.apply_roi_size_button.setProperty("saxsButtonRole", "toolbar")
|
||||
layout.addWidget(field_label("ROI size", self), 5, 0)
|
||||
layout.addWidget(self.roi_width, 5, 1)
|
||||
layout.addWidget(self.roi_height, 5, 2)
|
||||
layout.addWidget(self.apply_roi_size_button, 5, 3)
|
||||
|
||||
self.apply_corrections_button = QPushButton("Apply corrections", self)
|
||||
self.apply_corrections_button.setIcon(
|
||||
material_icon("published_with_changes", size=(16, 16))
|
||||
@@ -278,8 +293,9 @@ class ROIMappingBox(QGroupBox):
|
||||
self.apply_corrections_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.apply_corrections_button.setFlat(True)
|
||||
self.apply_corrections_button.setProperty("saxsButtonRole", "toolbar")
|
||||
layout.addWidget(self.apply_corrections_button, 5, 0, 1, 4)
|
||||
layout.addWidget(self.apply_corrections_button, 6, 0, 1, 4)
|
||||
|
||||
self.apply_roi_size_button.clicked.connect(self._emit_roi_size_requested)
|
||||
self.apply_corrections_button.clicked.connect(self.apply_requested.emit)
|
||||
self.mapping_lock_toggle.stateChanged.connect(self.set_locked)
|
||||
self.set_locked(self.mapping_lock_toggle.isChecked())
|
||||
@@ -296,10 +312,59 @@ class ROIMappingBox(QGroupBox):
|
||||
reference_y=float(self.reference_y.text() or 0.0),
|
||||
)
|
||||
|
||||
def set_correction(self, correction: Correction) -> None:
|
||||
for widget in [
|
||||
self.scale_x,
|
||||
self.offset_x,
|
||||
self.direction_x,
|
||||
self.scale_y,
|
||||
self.offset_y,
|
||||
self.direction_y,
|
||||
]:
|
||||
widget.blockSignals(True)
|
||||
try:
|
||||
self.scale_x.setValue(correction.scale_x)
|
||||
self.offset_x.setValue(correction.offset_x)
|
||||
self._set_combo_current_data(self.direction_x, correction.direction_x)
|
||||
self.scale_y.setValue(correction.scale_y)
|
||||
self.offset_y.setValue(correction.offset_y)
|
||||
self._set_combo_current_data(self.direction_y, correction.direction_y)
|
||||
finally:
|
||||
for widget in [
|
||||
self.scale_x,
|
||||
self.offset_x,
|
||||
self.direction_x,
|
||||
self.scale_y,
|
||||
self.offset_y,
|
||||
self.direction_y,
|
||||
]:
|
||||
widget.blockSignals(False)
|
||||
self.set_reference_position(correction.reference_x, correction.reference_y)
|
||||
|
||||
def set_reference_position(self, x: float, y: float) -> None:
|
||||
self.reference_x.setText(f"{x:.6g}")
|
||||
self.reference_y.setText(f"{y:.6g}")
|
||||
|
||||
def set_roi_size(self, width: float, height: float) -> None:
|
||||
self.roi_width.blockSignals(True)
|
||||
self.roi_height.blockSignals(True)
|
||||
try:
|
||||
self.roi_width.setValue(width)
|
||||
self.roi_height.setValue(height)
|
||||
finally:
|
||||
self.roi_width.blockSignals(False)
|
||||
self.roi_height.blockSignals(False)
|
||||
|
||||
@staticmethod
|
||||
def _set_combo_current_data(combo: QComboBox, value: int) -> None:
|
||||
for index in range(combo.count()):
|
||||
if combo.itemData(index) == value:
|
||||
combo.setCurrentIndex(index)
|
||||
return
|
||||
|
||||
def _emit_roi_size_requested(self) -> None:
|
||||
self.roi_size_requested.emit(self.roi_width.value(), self.roi_height.value())
|
||||
|
||||
def is_locked(self) -> bool:
|
||||
return self.mapping_lock_toggle.isChecked()
|
||||
|
||||
@@ -311,6 +376,9 @@ class ROIMappingBox(QGroupBox):
|
||||
self.scale_y,
|
||||
self.offset_y,
|
||||
self.direction_y,
|
||||
self.roi_width,
|
||||
self.roi_height,
|
||||
self.apply_roi_size_button,
|
||||
self.apply_corrections_button,
|
||||
]:
|
||||
widget.setEnabled(not locked)
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from csaxs_bec.bec_widgets.widgets.saxs_widget.models import Correction, RawROI
|
||||
|
||||
|
||||
def scaled_delta(raw_value: float, reference: float, scale: float, direction: int) -> float:
|
||||
"""Convert a raw image coordinate into a motor-space delta."""
|
||||
return int(direction) * float(scale) * (float(raw_value) - float(reference))
|
||||
|
||||
|
||||
def mapped_coordinate(
|
||||
raw_value: float,
|
||||
*,
|
||||
reference: float,
|
||||
scale: float,
|
||||
direction: int,
|
||||
camera_center: float = 0.0,
|
||||
beam_offset: float = 0.0,
|
||||
) -> float:
|
||||
"""Map one image coordinate to motor coordinates."""
|
||||
return (
|
||||
float(camera_center)
|
||||
+ float(beam_offset)
|
||||
+ scaled_delta(raw_value, reference, scale, direction)
|
||||
)
|
||||
|
||||
|
||||
def axis_limits(
|
||||
raw_start: float,
|
||||
raw_size: float,
|
||||
*,
|
||||
reference: float,
|
||||
scale: float,
|
||||
direction: int,
|
||||
camera_center: float = 0.0,
|
||||
beam_offset: float = 0.0,
|
||||
) -> tuple[float, float]:
|
||||
"""Return start/stop motor coordinates for one raw ROI axis."""
|
||||
start = mapped_coordinate(
|
||||
raw_start,
|
||||
reference=reference,
|
||||
scale=scale,
|
||||
direction=direction,
|
||||
camera_center=camera_center,
|
||||
beam_offset=beam_offset,
|
||||
)
|
||||
stop = mapped_coordinate(
|
||||
float(raw_start) + float(raw_size),
|
||||
reference=reference,
|
||||
scale=scale,
|
||||
direction=direction,
|
||||
camera_center=camera_center,
|
||||
beam_offset=beam_offset,
|
||||
)
|
||||
return start, stop
|
||||
|
||||
|
||||
def point_position(
|
||||
image_x: float,
|
||||
image_y: float,
|
||||
correction: Correction,
|
||||
*,
|
||||
camera_center: tuple[float, float] = (0.0, 0.0),
|
||||
include_beam_offset: bool = True,
|
||||
) -> tuple[float, float]:
|
||||
"""Map one image point into X/Y motor coordinates."""
|
||||
offset_x = correction.offset_x if include_beam_offset else 0.0
|
||||
offset_y = correction.offset_y if include_beam_offset else 0.0
|
||||
return (
|
||||
mapped_coordinate(
|
||||
image_x,
|
||||
reference=correction.reference_x,
|
||||
scale=correction.scale_x,
|
||||
direction=correction.direction_x,
|
||||
camera_center=camera_center[0],
|
||||
beam_offset=offset_x,
|
||||
),
|
||||
mapped_coordinate(
|
||||
image_y,
|
||||
reference=correction.reference_y,
|
||||
scale=correction.scale_y,
|
||||
direction=correction.direction_y,
|
||||
camera_center=camera_center[1],
|
||||
beam_offset=offset_y,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def roi_origin_position(
|
||||
raw_roi: RawROI,
|
||||
correction: Correction,
|
||||
*,
|
||||
camera_center: tuple[float, float] = (0.0, 0.0),
|
||||
include_beam_offset: bool = True,
|
||||
) -> tuple[float, float]:
|
||||
"""Map the raw ROI origin into X/Y motor coordinates."""
|
||||
return point_position(
|
||||
raw_roi.x,
|
||||
raw_roi.y,
|
||||
correction,
|
||||
camera_center=camera_center,
|
||||
include_beam_offset=include_beam_offset,
|
||||
)
|
||||
@@ -11,6 +11,8 @@ from typing import Literal
|
||||
from bec_lib import messages
|
||||
from bec_lib.endpoints import EndpointInfo, EndpointType, MessageOp
|
||||
|
||||
from csaxs_bec.bec_widgets.widgets.saxs_widget.coordinates import axis_limits
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError: # pragma: no cover - depends on the local environment
|
||||
@@ -222,9 +224,17 @@ class RawROI:
|
||||
height: float
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CameraCenter:
|
||||
"""Motor coordinates that correspond to the camera reference point."""
|
||||
|
||||
x: float = 0.0
|
||||
y: float = 0.0
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Correction:
|
||||
"""Corrections to convert raw ROI coordinates to physical positions."""
|
||||
"""Corrections to convert raw ROI coordinates to beam positions."""
|
||||
|
||||
scale_x: float
|
||||
offset_x: float
|
||||
@@ -295,6 +305,7 @@ class SAXSRow:
|
||||
correction: Correction
|
||||
geometry: ScanGeometry
|
||||
exposure_time: float
|
||||
camera_center: CameraCenter
|
||||
comment: str = ""
|
||||
template: Literal["scanning", "tensor"] = "scanning"
|
||||
rotx: float | None = None
|
||||
@@ -364,7 +375,7 @@ class SAXSRow:
|
||||
separators=(",", ":"),
|
||||
)
|
||||
return {
|
||||
"schema_version": 2,
|
||||
"schema_version": 3,
|
||||
"template": self.template,
|
||||
"sample_name": self.sample_name,
|
||||
"sample_id": self.sample_id,
|
||||
@@ -389,6 +400,8 @@ class SAXSRow:
|
||||
"roty_axis": self.roty_axis,
|
||||
"rotx": self.rotx,
|
||||
"roty": self.roty,
|
||||
"camera_center_x": self.camera_center.x,
|
||||
"camera_center_y": self.camera_center.y,
|
||||
"raw_roi_x": self.raw_roi.x,
|
||||
"raw_roi_y": self.raw_roi.y,
|
||||
"raw_roi_width": self.raw_roi.width,
|
||||
@@ -427,7 +440,7 @@ class SAXSRow:
|
||||
def help() -> dict[str, str]:
|
||||
"""Return human-readable documentation for all exported payload fields."""
|
||||
return {
|
||||
"schema_version": "Payload schema version for consumers. Current value is 2.",
|
||||
"schema_version": "Payload schema version for consumers. Current value is 3.",
|
||||
"template": "Row template type, for example scanning or tensor.",
|
||||
"sample_name": "User-provided sample label.",
|
||||
"sample_id": "Generated short unique ID for this row.",
|
||||
@@ -452,15 +465,17 @@ class SAXSRow:
|
||||
"roty_axis": "Configured rotation motor name for RotY.",
|
||||
"rotx": "RotX angle value for this row, null for non-tensor rows.",
|
||||
"roty": "RotY angle value for this row, null for non-tensor rows.",
|
||||
"camera_center_x": "X motor position that corresponded to the image reference point when the row was created.",
|
||||
"camera_center_y": "Y motor position that corresponded to the image reference point when the row was created.",
|
||||
"raw_roi_x": "Raw ROI X origin in image coordinates.",
|
||||
"raw_roi_y": "Raw ROI Y origin in image coordinates.",
|
||||
"raw_roi_width": "Raw ROI width in image coordinates.",
|
||||
"raw_roi_height": "Raw ROI height in image coordinates.",
|
||||
"correction_scale_x": "Scale factor applied from ROI X to physical X.",
|
||||
"correction_offset_x": "Physical X position at correction_reference_x.",
|
||||
"correction_offset_x": "Calibrated X offset from the camera reference point to the x-ray beam.",
|
||||
"correction_direction_x": "Direction sign for X mapping, typically +1 or -1.",
|
||||
"correction_scale_y": "Scale factor applied from ROI Y to physical Y.",
|
||||
"correction_offset_y": "Physical Y position at correction_reference_y.",
|
||||
"correction_offset_y": "Calibrated Y offset from the camera reference point to the x-ray beam.",
|
||||
"correction_direction_y": "Direction sign for Y mapping, typically +1 or -1.",
|
||||
"correction_reference_x": "Image coordinate that maps to correction_offset_x.",
|
||||
"correction_reference_y": "Image coordinate that maps to correction_offset_y.",
|
||||
@@ -482,31 +497,42 @@ def corrected_geometry(
|
||||
fast_step: float = DEFAULT_STEP_SIZE,
|
||||
stepper_axis: Literal["x", "y"] = "y",
|
||||
stepper_step: float = DEFAULT_STEP_SIZE,
|
||||
*,
|
||||
camera_center: CameraCenter | tuple[float, float],
|
||||
) -> ScanGeometry:
|
||||
fast_axis = _normalize_axis(fast_axis)
|
||||
stepper_axis = _normalize_axis(stepper_axis)
|
||||
if fast_axis == stepper_axis:
|
||||
raise ValueError("Fast axis and stepper axis must be different.")
|
||||
|
||||
def axis_limits(axis: Literal["x", "y"]) -> tuple[float, float]:
|
||||
if axis == "x":
|
||||
start = correction.offset_x + correction.direction_x * correction.scale_x * (
|
||||
raw_roi.x - correction.reference_x
|
||||
)
|
||||
stop = correction.offset_x + correction.direction_x * correction.scale_x * (
|
||||
raw_roi.x + raw_roi.width - correction.reference_x
|
||||
)
|
||||
return start, stop
|
||||
start = correction.offset_y + correction.direction_y * correction.scale_y * (
|
||||
raw_roi.y - correction.reference_y
|
||||
)
|
||||
stop = correction.offset_y + correction.direction_y * correction.scale_y * (
|
||||
raw_roi.y + raw_roi.height - correction.reference_y
|
||||
)
|
||||
return start, stop
|
||||
if isinstance(camera_center, CameraCenter):
|
||||
center_x, center_y = camera_center.x, camera_center.y
|
||||
else:
|
||||
center_x, center_y = camera_center
|
||||
|
||||
fast_start, fast_stop = axis_limits(fast_axis)
|
||||
stepper_start, stepper_stop = axis_limits(stepper_axis)
|
||||
def roi_axis_limits(axis: Literal["x", "y"]) -> tuple[float, float]:
|
||||
if axis == "x":
|
||||
return axis_limits(
|
||||
raw_roi.x,
|
||||
raw_roi.width,
|
||||
reference=correction.reference_x,
|
||||
scale=correction.scale_x,
|
||||
direction=correction.direction_x,
|
||||
camera_center=center_x,
|
||||
beam_offset=correction.offset_x,
|
||||
)
|
||||
return axis_limits(
|
||||
raw_roi.y,
|
||||
raw_roi.height,
|
||||
reference=correction.reference_y,
|
||||
scale=correction.scale_y,
|
||||
direction=correction.direction_y,
|
||||
camera_center=center_y,
|
||||
beam_offset=correction.offset_y,
|
||||
)
|
||||
|
||||
fast_start, fast_stop = roi_axis_limits(fast_axis)
|
||||
stepper_start, stepper_stop = roi_axis_limits(stepper_axis)
|
||||
images_per_line, num_lines, total_frames = cont_grid_point_counts(
|
||||
fast_start, fast_stop, stepper_start, stepper_stop, fast_step, stepper_step
|
||||
)
|
||||
|
||||
@@ -55,6 +55,7 @@ from csaxs_bec.bec_widgets.widgets.saxs_widget.dialogs import (
|
||||
from csaxs_bec.bec_widgets.widgets.saxs_widget.models import (
|
||||
DEFAULT_EXPOSURE_TIME,
|
||||
DEFAULT_STEP_SIZE,
|
||||
CameraCenter,
|
||||
Correction,
|
||||
MessageEndpointSAXSWidget,
|
||||
RawROI,
|
||||
@@ -83,6 +84,7 @@ class SAXSWidget(BECWidget, QWidget):
|
||||
"rows_as_dicts",
|
||||
"set_positioners",
|
||||
"mark_scan_result",
|
||||
"move_to_selected_row",
|
||||
]
|
||||
|
||||
ROI_NAME = "SAXS ROI"
|
||||
@@ -130,10 +132,6 @@ class SAXSWidget(BECWidget, QWidget):
|
||||
top_layout.setContentsMargins(0, 0, 0, 0)
|
||||
top_layout.setSpacing(8)
|
||||
|
||||
self.image = Image(parent=top)
|
||||
self._customize_image_widget(self.image)
|
||||
top_layout.addWidget(self.image, 1)
|
||||
|
||||
side_panel = QFrame(top)
|
||||
side_panel.setObjectName("saxs_side_panel")
|
||||
side_panel.setFrameShape(QFrame.Shape.NoFrame)
|
||||
@@ -157,6 +155,10 @@ class SAXSWidget(BECWidget, QWidget):
|
||||
device_mapping_layout.addWidget(self.device_box, 1)
|
||||
device_mapping_layout.addWidget(self.mapping_box, 1)
|
||||
|
||||
self.image = Image(parent=top)
|
||||
self._customize_image_widget(self.image)
|
||||
top_layout.addWidget(self.image, 1)
|
||||
|
||||
side_layout.addWidget(side_title)
|
||||
side_layout.addWidget(device_mapping_panel)
|
||||
side_layout.addWidget(separator(side_panel))
|
||||
@@ -167,6 +169,9 @@ class SAXSWidget(BECWidget, QWidget):
|
||||
for direction in ["horizontal", "vertical"]:
|
||||
ui = self.positioner_box._device_ui_components_hv(direction)
|
||||
for k, w in ui.items():
|
||||
if k == "step_size":
|
||||
w.setValue(1)
|
||||
continue
|
||||
if k not in [
|
||||
"tweak_increase",
|
||||
"tweak_decrease",
|
||||
@@ -260,7 +265,7 @@ class SAXSWidget(BECWidget, QWidget):
|
||||
# If enabled, set the camera to live mode, if disabled, stop the live mode.
|
||||
print(f"Toggling camera live mode to {'enabled' if enabled else 'disabled'}")
|
||||
|
||||
def _refresh_crosshair_pin_if_enabled(self, *_args, force: bool = False) -> None:
|
||||
def _refresh_crosshair_pin_if_enabled(self, force: bool = False) -> None:
|
||||
if self._crosshair_pin_enabled:
|
||||
pin_position = self._mapping_reference_position()
|
||||
if (
|
||||
@@ -268,7 +273,6 @@ class SAXSWidget(BECWidget, QWidget):
|
||||
and self._positions_match(self._crosshair_pin_position, pin_position)
|
||||
and self._has_reference_crosshair_pin()
|
||||
):
|
||||
self._update_reference_crosshair_label(*pin_position)
|
||||
return
|
||||
self._place_reference_crosshair_pin(pin_position)
|
||||
|
||||
@@ -293,6 +297,7 @@ class SAXSWidget(BECWidget, QWidget):
|
||||
self._refresh_crosshair_pin_if_enabled()
|
||||
|
||||
def _on_roi_reference_changed(self, *_args) -> None:
|
||||
self._sync_roi_size_fields()
|
||||
shape = self._display_image_shape()
|
||||
if shape is None:
|
||||
self._set_mapping_reference_position(*self._roi_center_position(), image_shape=None)
|
||||
@@ -362,16 +367,7 @@ class SAXSWidget(BECWidget, QWidget):
|
||||
pinned_label = detached_pin.get("label")
|
||||
if pinned_label is None:
|
||||
return
|
||||
samx, samy = self._map_image_point_to_position(image_x, image_y)
|
||||
x_name = (
|
||||
getattr(self, "device_box", None).x_positioner if hasattr(self, "device_box") else ""
|
||||
)
|
||||
y_name = (
|
||||
getattr(self, "device_box", None).y_positioner if hasattr(self, "device_box") else ""
|
||||
)
|
||||
x_name = x_name or "samx"
|
||||
y_name = y_name or "samy"
|
||||
pinned_label.setText(f"pin\n{x_name}: {samx:.6g}\n{y_name}: {samy:.6g}")
|
||||
pinned_label.setText(f"pin\nx: {image_x:.6g} px\ny: {image_y:.6g} px")
|
||||
|
||||
def _display_image_shape(self) -> tuple[int, int] | None:
|
||||
try:
|
||||
@@ -410,16 +406,6 @@ class SAXSWidget(BECWidget, QWidget):
|
||||
self._sync_mapping_reference_to_display()
|
||||
return self._mapping_reference or (0.0, 0.0)
|
||||
|
||||
def _map_image_point_to_position(self, image_x: float, image_y: float) -> tuple[float, float]:
|
||||
correction = self._current_correction()
|
||||
samx = correction.offset_x + correction.direction_x * correction.scale_x * (
|
||||
image_x - correction.reference_x
|
||||
)
|
||||
samy = correction.offset_y + correction.direction_y * correction.scale_y * (
|
||||
image_y - correction.reference_y
|
||||
)
|
||||
return samx, samy
|
||||
|
||||
def _build_table_session_bar(self) -> QWidget:
|
||||
bar = QFrame(self.table_group)
|
||||
bar.setObjectName("saxs_session_bar")
|
||||
@@ -474,10 +460,11 @@ class SAXSWidget(BECWidget, QWidget):
|
||||
self.device_box.y_positioner_changed.connect(self.positioner_box.set_positioner_ver)
|
||||
self.device_box.sastt_enabled_changed.connect(self._update_sastt_enabled)
|
||||
self.mapping_box.apply_requested.connect(self._prompt_apply_corrections)
|
||||
self.mapping_box.roi_size_requested.connect(self._apply_roi_size)
|
||||
self.image.image_updated.connect(self._on_image_updated)
|
||||
self._connect_mapping_reference_signals()
|
||||
|
||||
self.table_widget.edit_requested.connect(self._edit_row_by_index)
|
||||
self.table_widget.move_to_selected_requested.connect(self.move_to_selected_row)
|
||||
self.table_widget.clear_selected_requested.connect(self.delete_selected_rows)
|
||||
self.table_widget.clear_all_requested.connect(self.clear_rows)
|
||||
self.table_widget.save_requested.connect(self.save_session)
|
||||
@@ -493,21 +480,6 @@ class SAXSWidget(BECWidget, QWidget):
|
||||
|
||||
self._update_sastt_enabled(self.device_box.is_sastt_enabled())
|
||||
|
||||
def _connect_mapping_reference_signals(self) -> None:
|
||||
for spin_box in [
|
||||
self.mapping_box.scale_x,
|
||||
self.mapping_box.offset_x,
|
||||
self.mapping_box.scale_y,
|
||||
self.mapping_box.offset_y,
|
||||
]:
|
||||
spin_box.valueChanged.connect(self._refresh_crosshair_pin_if_enabled)
|
||||
self.mapping_box.direction_x.currentIndexChanged.connect(
|
||||
self._refresh_crosshair_pin_if_enabled
|
||||
)
|
||||
self.mapping_box.direction_y.currentIndexChanged.connect(
|
||||
self._refresh_crosshair_pin_if_enabled
|
||||
)
|
||||
|
||||
@SafeSlot(str)
|
||||
@SafeSlot()
|
||||
def apply_theme(self, _theme: str | None = None) -> None:
|
||||
@@ -671,8 +643,9 @@ class SAXSWidget(BECWidget, QWidget):
|
||||
)
|
||||
roi.line_color = self._roi_color
|
||||
roi.removable = False
|
||||
roi.edgesChanged.connect(self._on_roi_reference_changed)
|
||||
roi.sigRegionChangeFinished.connect(self._on_roi_reference_changed)
|
||||
self._saxs_roi = roi
|
||||
self._sync_roi_size_fields()
|
||||
return roi
|
||||
|
||||
def _on_roi_removed(self, roi) -> None:
|
||||
@@ -688,6 +661,28 @@ class SAXSWidget(BECWidget, QWidget):
|
||||
x=float(x), y=float(y), width=float(coords["width"]), height=float(coords["height"])
|
||||
)
|
||||
|
||||
def _sync_roi_size_fields(self) -> None:
|
||||
if not hasattr(self, "mapping_box") or self._saxs_roi is None:
|
||||
return
|
||||
coords = self._saxs_roi.get_coordinates(typed=True)
|
||||
self.mapping_box.set_roi_size(float(coords["width"]), float(coords["height"]))
|
||||
|
||||
def _set_roi_from_raw(self, raw_roi: RawROI) -> None:
|
||||
roi = self._ensure_saxs_roi()
|
||||
roi.setSize((raw_roi.width, raw_roi.height))
|
||||
roi.setPos(raw_roi.x, raw_roi.y)
|
||||
self._sync_roi_size_fields()
|
||||
|
||||
def _apply_roi_size(self, width: float, height: float) -> None:
|
||||
roi = self._ensure_saxs_roi()
|
||||
coords = roi.get_coordinates(typed=True)
|
||||
center_x = float(coords["center_x"])
|
||||
center_y = float(coords["center_y"])
|
||||
roi.setSize((float(width), float(height)))
|
||||
roi.setPos(center_x - float(width) / 2.0, center_y - float(height) / 2.0)
|
||||
self._sync_roi_size_fields()
|
||||
self._refresh_crosshair_pin_if_enabled()
|
||||
|
||||
def _current_correction(self) -> Correction:
|
||||
reference_x, reference_y = self._mapping_reference_position()
|
||||
if not hasattr(self, "mapping_box"):
|
||||
@@ -713,6 +708,88 @@ class SAXSWidget(BECWidget, QWidget):
|
||||
reference_y=reference_y,
|
||||
)
|
||||
|
||||
def _current_camera_center(self) -> CameraCenter:
|
||||
current_x, current_y = self._current_positioner_positions()
|
||||
return CameraCenter(x=current_x, y=current_y)
|
||||
|
||||
def _current_positioner_positions(self) -> tuple[float, float]:
|
||||
x_positioner = self.device_box.x_positioner
|
||||
y_positioner = self.device_box.y_positioner
|
||||
if not x_positioner or not y_positioner:
|
||||
raise ValueError("Select X and Y positioners before computing scan coordinates.")
|
||||
|
||||
missing = [name for name in [x_positioner, y_positioner] if name not in self.dev]
|
||||
if missing:
|
||||
raise ValueError(f"Device(s) not available: {', '.join(missing)}.")
|
||||
|
||||
return (
|
||||
self._read_positioner_position(x_positioner),
|
||||
self._read_positioner_position(y_positioner),
|
||||
)
|
||||
|
||||
def _read_positioner_position(self, device_name: str) -> float:
|
||||
device = self.dev[device_name]
|
||||
errors: list[str] = []
|
||||
for source in (device, getattr(device, "obj", None)):
|
||||
if source is None:
|
||||
continue
|
||||
try:
|
||||
value = getattr(source, "position", None)
|
||||
if value is not None:
|
||||
return float(value)
|
||||
except (TypeError, ValueError) as exc:
|
||||
errors.append(str(exc))
|
||||
|
||||
readback = getattr(source, "readback", None)
|
||||
if readback is not None:
|
||||
if hasattr(readback, "get"):
|
||||
try:
|
||||
return float(readback.get())
|
||||
except Exception as exc: # noqa: BLE001 - try the next supported read path
|
||||
errors.append(str(exc))
|
||||
if hasattr(readback, "read"):
|
||||
try:
|
||||
value = self._value_from_reading(readback.read(), device_name)
|
||||
if value is not None:
|
||||
return value
|
||||
except Exception as exc: # noqa: BLE001 - try the next supported read path
|
||||
errors.append(str(exc))
|
||||
|
||||
read = getattr(source, "read", None)
|
||||
if callable(read):
|
||||
try:
|
||||
value = self._value_from_reading(read(), device_name)
|
||||
if value is not None:
|
||||
return value
|
||||
except Exception as exc: # noqa: BLE001 - collect context for the user-facing error
|
||||
errors.append(str(exc))
|
||||
|
||||
details = f" ({'; '.join(errors)})" if errors else ""
|
||||
raise ValueError(f"Could not read current position for {device_name}{details}.")
|
||||
|
||||
@staticmethod
|
||||
def _value_from_reading(reading, device_name: str) -> float | None:
|
||||
if isinstance(reading, (int, float)):
|
||||
return float(reading)
|
||||
if not isinstance(reading, dict):
|
||||
return None
|
||||
|
||||
candidates = [device_name]
|
||||
candidates.extend(
|
||||
key
|
||||
for key in reading
|
||||
if str(key).startswith(f"{device_name}_") or str(key).endswith("_readback")
|
||||
)
|
||||
candidates.extend(key for key in reading if key not in candidates)
|
||||
|
||||
for key in candidates:
|
||||
entry = reading.get(key)
|
||||
if isinstance(entry, dict) and "value" in entry:
|
||||
return float(entry["value"])
|
||||
if isinstance(entry, (int, float)):
|
||||
return float(entry)
|
||||
return None
|
||||
|
||||
def _make_sample_id(self) -> str:
|
||||
return uuid4().hex
|
||||
|
||||
@@ -731,11 +808,15 @@ class SAXSWidget(BECWidget, QWidget):
|
||||
roty: float | None = None,
|
||||
raw_roi: RawROI | None = None,
|
||||
correction: Correction | None = None,
|
||||
camera_center: CameraCenter | None = None,
|
||||
sample_id: str | None = None,
|
||||
) -> SAXSRow:
|
||||
raw = raw_roi or self._current_raw_roi()
|
||||
corr = correction or self._current_correction()
|
||||
geometry = corrected_geometry(raw, corr, fast_axis, fast_step, stepper_axis, stepper_step)
|
||||
center = camera_center or self._current_camera_center()
|
||||
geometry = corrected_geometry(
|
||||
raw, corr, fast_axis, fast_step, stepper_axis, stepper_step, camera_center=center
|
||||
)
|
||||
return SAXSRow(
|
||||
sample_name=sample_name,
|
||||
sample_id=sample_id or self._make_sample_id(),
|
||||
@@ -743,6 +824,7 @@ class SAXSWidget(BECWidget, QWidget):
|
||||
correction=corr,
|
||||
geometry=geometry,
|
||||
exposure_time=exposure_time,
|
||||
camera_center=center,
|
||||
comment=comment,
|
||||
template=template,
|
||||
rotx=rotx,
|
||||
@@ -893,6 +975,7 @@ class SAXSWidget(BECWidget, QWidget):
|
||||
try:
|
||||
raw_roi = self._current_raw_roi()
|
||||
correction = self._current_correction()
|
||||
camera_center = self._current_camera_center()
|
||||
for rotx, roty_value in normalize_angle_pairs(values["angle_pairs"]):
|
||||
rows.append(
|
||||
self._build_row(
|
||||
@@ -908,6 +991,7 @@ class SAXSWidget(BECWidget, QWidget):
|
||||
roty=float(roty_value),
|
||||
raw_roi=raw_roi,
|
||||
correction=correction,
|
||||
camera_center=camera_center,
|
||||
)
|
||||
)
|
||||
except ValueError as exc:
|
||||
@@ -935,6 +1019,72 @@ class SAXSWidget(BECWidget, QWidget):
|
||||
return
|
||||
self._edit_row_by_index(selected[0])
|
||||
|
||||
@SafeSlot(int)
|
||||
def move_to_selected_row(self, index: int) -> None:
|
||||
if index < 0 or index >= len(self._rows):
|
||||
self._warn("Select exactly one row to move to.")
|
||||
return
|
||||
|
||||
row = self._rows[index]
|
||||
x_positioner = row.x_positioner
|
||||
y_positioner = row.y_positioner
|
||||
if not x_positioner or not y_positioner:
|
||||
self._warn("The selected row does not contain X/Y positioners.")
|
||||
return
|
||||
|
||||
missing = [name for name in [x_positioner, y_positioner] if name not in self.dev]
|
||||
if missing:
|
||||
self._warn(f"Device(s) not available: {', '.join(missing)}.")
|
||||
return
|
||||
|
||||
target_x, target_y = self._row_camera_center_position(row)
|
||||
if not self._confirm_move_to_selected(row, x_positioner, y_positioner, target_x, target_y):
|
||||
return
|
||||
|
||||
self._restore_row_view(row)
|
||||
try:
|
||||
self.dev[x_positioner].move(target_x, relative=False)
|
||||
self.dev[y_positioner].move(target_y, relative=False)
|
||||
except Exception as exc: # noqa: BLE001 - device move errors are user-facing here
|
||||
self._warn(f"Could not move to selected row: {exc}")
|
||||
return
|
||||
|
||||
self.status_label.setText(
|
||||
f"Moving {x_positioner}={target_x:.6g}, {y_positioner}={target_y:.6g}"
|
||||
)
|
||||
|
||||
def _confirm_move_to_selected(
|
||||
self, row: SAXSRow, x_positioner: str, y_positioner: str, target_x: float, target_y: float
|
||||
) -> bool:
|
||||
box = QMessageBox(self)
|
||||
box.setIcon(QMessageBox.Icon.Question)
|
||||
box.setWindowTitle("Move to selected")
|
||||
box.setText(f"Move to selected row '{row.sample_name}'?")
|
||||
box.setInformativeText(
|
||||
"This restores the submitted ROI and moves the row's X/Y positioners to "
|
||||
"the saved motor positions from when the row was created:\n"
|
||||
f"{x_positioner}: {target_x:.6g}\n"
|
||||
f"{y_positioner}: {target_y:.6g}"
|
||||
)
|
||||
box.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel)
|
||||
box.setDefaultButton(QMessageBox.StandardButton.Cancel)
|
||||
return box.exec() == QMessageBox.StandardButton.Yes
|
||||
|
||||
def _restore_row_view(self, row: SAXSRow) -> None:
|
||||
self.set_positioners(row.x_positioner, row.y_positioner)
|
||||
self.mapping_box.set_correction(row.correction)
|
||||
self._set_roi_from_raw(row.raw_roi)
|
||||
self._set_mapping_reference_position(
|
||||
row.correction.reference_x,
|
||||
row.correction.reference_y,
|
||||
image_shape=self._display_image_shape(),
|
||||
)
|
||||
self._refresh_crosshair_pin_if_enabled(force=True)
|
||||
|
||||
@staticmethod
|
||||
def _row_camera_center_position(row: SAXSRow) -> tuple[float, float]:
|
||||
return row.camera_center.x, row.camera_center.y
|
||||
|
||||
def _edit_row_by_index(self, index: int) -> None:
|
||||
if index < 0 or index >= len(self._rows):
|
||||
return
|
||||
@@ -957,6 +1107,7 @@ class SAXSWidget(BECWidget, QWidget):
|
||||
roty=values["roty"] if row.template == "tensor" else None,
|
||||
raw_roi=row.raw_roi,
|
||||
correction=row.correction,
|
||||
camera_center=row.camera_center,
|
||||
sample_id=row.sample_id,
|
||||
)
|
||||
except ValueError as exc:
|
||||
@@ -989,7 +1140,11 @@ class SAXSWidget(BECWidget, QWidget):
|
||||
if self.mapping_box.is_locked():
|
||||
return
|
||||
indices = rows if rows is not None else list(range(len(self._rows)))
|
||||
correction = self._current_correction()
|
||||
try:
|
||||
correction = self._current_correction()
|
||||
except ValueError as exc:
|
||||
self._warn(str(exc))
|
||||
return
|
||||
for index in indices:
|
||||
row = self._rows[index]
|
||||
try:
|
||||
@@ -1001,6 +1156,7 @@ class SAXSWidget(BECWidget, QWidget):
|
||||
row.fast_step,
|
||||
row.stepper_axis,
|
||||
row.stepper_step,
|
||||
camera_center=row.camera_center,
|
||||
)
|
||||
except ValueError as exc:
|
||||
self._warn(f"Could not update row {index + 1}: {exc}")
|
||||
@@ -1090,9 +1246,17 @@ class SAXSWidget(BECWidget, QWidget):
|
||||
def set_positioners(self, x_positioner: str, y_positioner: str) -> None:
|
||||
self.device_box.set_positioners(x_positioner, y_positioner)
|
||||
if x_positioner:
|
||||
ui = self.positioner_box._device_ui_components_hv("horizontal")
|
||||
step_size = ui["step_size"].value()
|
||||
self.positioner_box.set_positioner_hor(x_positioner)
|
||||
ui = self.positioner_box._device_ui_components_hv("horizontal")
|
||||
ui["step_size"].setValue(step_size)
|
||||
if y_positioner:
|
||||
ui = self.positioner_box._device_ui_components_hv("vertical")
|
||||
step_size = ui["step_size"].value()
|
||||
self.positioner_box.set_positioner_ver(y_positioner)
|
||||
ui = self.positioner_box._device_ui_components_hv("vertical")
|
||||
ui["step_size"].setValue(step_size)
|
||||
|
||||
@SafeSlot(object, object, bool)
|
||||
def mark_scan_result(self, sample_id_or_row, scan_number, success: bool) -> None:
|
||||
@@ -1297,6 +1461,9 @@ class SAXSWidget(BECWidget, QWidget):
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
try:
|
||||
schema_version = int(entry["schema_version"])
|
||||
if schema_version != 3:
|
||||
raise ValueError(f"Unsupported SAXS row schema: {schema_version}")
|
||||
raw_roi = RawROI(
|
||||
x=float(entry["raw_roi_x"]),
|
||||
y=float(entry["raw_roi_y"]),
|
||||
@@ -1310,8 +1477,11 @@ class SAXSWidget(BECWidget, QWidget):
|
||||
scale_y=float(entry["correction_scale_y"]),
|
||||
offset_y=float(entry["correction_offset_y"]),
|
||||
direction_y=int(entry["correction_direction_y"]),
|
||||
reference_x=float(entry.get("correction_reference_x", 0.0)),
|
||||
reference_y=float(entry.get("correction_reference_y", 0.0)),
|
||||
reference_x=float(entry["correction_reference_x"]),
|
||||
reference_y=float(entry["correction_reference_y"]),
|
||||
)
|
||||
camera_center = CameraCenter(
|
||||
x=float(entry["camera_center_x"]), y=float(entry["camera_center_y"])
|
||||
)
|
||||
geometry = corrected_geometry(
|
||||
raw_roi,
|
||||
@@ -1320,6 +1490,7 @@ class SAXSWidget(BECWidget, QWidget):
|
||||
fast_step=float(entry.get("fast_step", DEFAULT_STEP_SIZE)),
|
||||
stepper_axis=str(entry.get("stepper_axis_coordinate", "y")),
|
||||
stepper_step=float(entry.get("stepper_step", DEFAULT_STEP_SIZE)),
|
||||
camera_center=camera_center,
|
||||
)
|
||||
row = SAXSRow(
|
||||
sample_name=str(entry.get("sample_name", "")),
|
||||
@@ -1328,6 +1499,7 @@ class SAXSWidget(BECWidget, QWidget):
|
||||
correction=correction,
|
||||
geometry=geometry,
|
||||
exposure_time=float(entry.get("exposure_time", DEFAULT_EXPOSURE_TIME)),
|
||||
camera_center=camera_center,
|
||||
comment=str(entry.get("comment", "")),
|
||||
template=(
|
||||
"tensor"
|
||||
@@ -1358,7 +1530,7 @@ class SAXSWidget(BECWidget, QWidget):
|
||||
]
|
||||
restored_rows.append(row)
|
||||
except (KeyError, TypeError, ValueError) as exc:
|
||||
logger.warning("Skipping invalid stored SAXS row payload: %s", exc)
|
||||
logger.warning("Skipping invalid stored SAXS row payload: {}", exc)
|
||||
return restored_rows
|
||||
|
||||
def _extract_variable_message(self, entry: dict[str, Any]) -> VariableMessage | None:
|
||||
|
||||
@@ -49,6 +49,7 @@ class SAXSTableWidget(QWidget):
|
||||
store_csv_requested = Signal()
|
||||
add_scanning_requested = Signal()
|
||||
add_tensor_requested = Signal()
|
||||
move_to_selected_requested = Signal(int)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
@@ -143,10 +144,10 @@ class SAXSTableWidget(QWidget):
|
||||
output_label.setObjectName("saxs_toolbar_label")
|
||||
|
||||
self.edit_button = QPushButton("Edit", bar)
|
||||
self.move_to_selected_button = QPushButton("Move to selected", bar)
|
||||
self.clear_selected_button = QPushButton("Remove", bar)
|
||||
self.clear_all_button = QPushButton("Clear all", bar)
|
||||
self.save_button = QPushButton("Save", bar)
|
||||
self.upload_button = QPushButton("Upload", bar)
|
||||
self.store_csv_button = QPushButton("Store CSV", bar)
|
||||
self.status_label = QLabel("Ready", bar)
|
||||
self.status_label.setObjectName("saxs_table_status")
|
||||
@@ -161,14 +162,15 @@ class SAXSTableWidget(QWidget):
|
||||
self.edit_button.setToolTip(
|
||||
"Edit the selected row.\nHint: double-click a row to open the editor directly."
|
||||
)
|
||||
self.move_to_selected_button.setToolTip(
|
||||
"Move the X/Y positioners to the saved motor positions from when the row "
|
||||
"was created and restore its saved ROI."
|
||||
)
|
||||
self.clear_selected_button.setToolTip(
|
||||
"Remove all selected rows from the table.\nHint: use Delete or Backspace."
|
||||
)
|
||||
self.clear_all_button.setToolTip("Remove every row currently listed in the table.")
|
||||
self.save_button.setToolTip("Save the current table state for the active session.")
|
||||
self.upload_button.setToolTip(
|
||||
"Upload selected rows, or the full table if none are selected."
|
||||
)
|
||||
self.store_csv_button.setToolTip(
|
||||
"Export selected rows or the full table into CSV and save the active session state."
|
||||
)
|
||||
@@ -176,29 +178,29 @@ class SAXSTableWidget(QWidget):
|
||||
self.add_scanning_button.setIcon(material_icon("add", size=(16, 16)))
|
||||
self.add_tensor_button.setIcon(material_icon("add", size=(16, 16)))
|
||||
self.edit_button.setIcon(material_icon("edit", size=(16, 16)))
|
||||
self.move_to_selected_button.setIcon(material_icon("my_location", size=(16, 16)))
|
||||
self.clear_selected_button.setIcon(material_icon("delete", size=(16, 16)))
|
||||
self.clear_all_button.setIcon(material_icon("clear_all", size=(16, 16)))
|
||||
self.save_button.setIcon(material_icon("save", size=(16, 16)))
|
||||
self.upload_button.setIcon(material_icon("upload", size=(16, 16)))
|
||||
self.store_csv_button.setIcon(material_icon("download", size=(16, 16)))
|
||||
|
||||
self.add_scanning_button.clicked.connect(self.add_scanning_requested.emit)
|
||||
self.add_tensor_button.clicked.connect(self.add_tensor_requested.emit)
|
||||
self.edit_button.clicked.connect(self._edit_selected)
|
||||
self.move_to_selected_button.clicked.connect(self._move_to_selected)
|
||||
self.clear_selected_button.clicked.connect(self.clear_selected_requested.emit)
|
||||
self.clear_all_button.clicked.connect(self.clear_all_requested.emit)
|
||||
self.save_button.clicked.connect(self.save_requested.emit)
|
||||
self.upload_button.clicked.connect(self.upload_requested.emit)
|
||||
self.store_csv_button.clicked.connect(self.store_csv_requested.emit)
|
||||
|
||||
for button in [
|
||||
self.add_scanning_button,
|
||||
self.add_tensor_button,
|
||||
self.edit_button,
|
||||
self.move_to_selected_button,
|
||||
self.clear_selected_button,
|
||||
self.clear_all_button,
|
||||
self.save_button,
|
||||
self.upload_button,
|
||||
self.store_csv_button,
|
||||
]:
|
||||
button.setProperty("saxsButtonRole", "toolbar")
|
||||
@@ -213,13 +215,14 @@ class SAXSTableWidget(QWidget):
|
||||
self.add_tensor_button.setMinimumWidth(150)
|
||||
for button in [
|
||||
self.edit_button,
|
||||
self.move_to_selected_button,
|
||||
self.clear_selected_button,
|
||||
self.clear_all_button,
|
||||
self.save_button,
|
||||
self.upload_button,
|
||||
self.store_csv_button,
|
||||
]:
|
||||
button.setMinimumWidth(84)
|
||||
self.move_to_selected_button.setMinimumWidth(128)
|
||||
|
||||
layout.addWidget(add_label)
|
||||
layout.addWidget(self.add_scanning_button)
|
||||
@@ -231,12 +234,12 @@ class SAXSTableWidget(QWidget):
|
||||
layout.addWidget(self._vertical_separator(bar))
|
||||
layout.addWidget(rows_label)
|
||||
layout.addWidget(self.edit_button)
|
||||
layout.addWidget(self.move_to_selected_button)
|
||||
layout.addWidget(self.clear_selected_button)
|
||||
layout.addWidget(self.clear_all_button)
|
||||
layout.addWidget(self._vertical_separator(bar))
|
||||
layout.addWidget(output_label)
|
||||
layout.addWidget(self.save_button)
|
||||
layout.addWidget(self.upload_button)
|
||||
layout.addWidget(self.store_csv_button)
|
||||
layout.addWidget(self.status_label)
|
||||
return bar
|
||||
@@ -254,6 +257,11 @@ class SAXSTableWidget(QWidget):
|
||||
if len(selected) == 1:
|
||||
self.edit_requested.emit(selected[0])
|
||||
|
||||
def _move_to_selected(self) -> None:
|
||||
selected = self.selected_rows()
|
||||
if len(selected) == 1:
|
||||
self.move_to_selected_requested.emit(selected[0])
|
||||
|
||||
def _edit_row_from_double_click(self, row: int, _column: int) -> None:
|
||||
self.table.selectRow(row)
|
||||
self.edit_requested.emit(row)
|
||||
@@ -266,10 +274,10 @@ class SAXSTableWidget(QWidget):
|
||||
else:
|
||||
self.row_summary_label.setText(f"{row_count} rows")
|
||||
self.edit_button.setEnabled(selected_count == 1)
|
||||
self.move_to_selected_button.setEnabled(selected_count == 1)
|
||||
self.clear_selected_button.setEnabled(selected_count > 0)
|
||||
self.clear_all_button.setEnabled(row_count > 0)
|
||||
self.save_button.setEnabled(row_count > 0)
|
||||
self.upload_button.setEnabled(row_count > 0)
|
||||
self.store_csv_button.setEnabled(row_count > 0)
|
||||
|
||||
@staticmethod
|
||||
|
||||
Reference in New Issue
Block a user