tomwer 1.4.0rc0__py3-none-any.whl → 1.4.0rc1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. orangecontrib/tomwer/tutorials/test_cor.ows +3 -3
  2. orangecontrib/tomwer/widgets/reconstruction/AxisOW.py +6 -14
  3. orangecontrib/tomwer/widgets/reconstruction/NabuVolumeOW.py +4 -2
  4. tomwer/app/axis.py +0 -3
  5. tomwer/app/multipag.py +11 -3
  6. tomwer/core/process/reconstruction/axis/axis.py +27 -736
  7. tomwer/core/process/reconstruction/axis/mode.py +86 -24
  8. tomwer/core/process/reconstruction/axis/params.py +127 -138
  9. tomwer/core/process/reconstruction/axis/side.py +8 -0
  10. tomwer/core/process/reconstruction/nabu/nabuscores.py +17 -20
  11. tomwer/core/process/reconstruction/nabu/nabuslices.py +5 -1
  12. tomwer/core/process/reconstruction/saaxis/saaxis.py +4 -4
  13. tomwer/core/process/reconstruction/sadeltabeta/sadeltabeta.py +4 -4
  14. tomwer/core/process/reconstruction/tests/test_axis.py +1 -1
  15. tomwer/core/process/reconstruction/tests/test_utils.py +4 -4
  16. tomwer/core/process/reconstruction/utils/cor.py +8 -4
  17. tomwer/core/process/tests/test_nabu.py +1 -3
  18. tomwer/core/scan/scanbase.py +4 -4
  19. tomwer/core/scan/tests/test_process_registration.py +0 -18
  20. tomwer/gui/fonts.py +5 -0
  21. tomwer/gui/reconstruction/axis/AxisMainWindow.py +20 -9
  22. tomwer/gui/reconstruction/axis/AxisOptionsWidget.py +239 -79
  23. tomwer/gui/reconstruction/axis/AxisSettingsWidget.py +38 -17
  24. tomwer/gui/reconstruction/axis/AxisWidget.py +16 -8
  25. tomwer/gui/reconstruction/axis/CalculationWidget.py +40 -200
  26. tomwer/gui/reconstruction/axis/ControlWidget.py +10 -2
  27. tomwer/gui/reconstruction/axis/EstimatedCORWidget.py +383 -0
  28. tomwer/gui/reconstruction/axis/EstimatedCorComboBox.py +118 -0
  29. tomwer/gui/reconstruction/axis/InputWidget.py +11 -155
  30. tomwer/gui/reconstruction/saaxis/corrangeselector.py +19 -10
  31. tomwer/gui/reconstruction/scores/scoreplot.py +5 -2
  32. tomwer/gui/reconstruction/tests/test_nabu.py +8 -0
  33. tomwer/gui/stitching/z_stitching/fineestimation.py +1 -1
  34. tomwer/gui/tests/test_axis_gui.py +31 -15
  35. tomwer/synctools/stacks/reconstruction/axis.py +5 -23
  36. tomwer/synctools/stacks/reconstruction/dkrefcopy.py +1 -1
  37. tomwer/synctools/stacks/reconstruction/nabu.py +2 -2
  38. tomwer/synctools/stacks/reconstruction/normalization.py +1 -1
  39. tomwer/synctools/stacks/reconstruction/saaxis.py +1 -1
  40. tomwer/synctools/stacks/reconstruction/sadeltabeta.py +1 -1
  41. tomwer/tests/orangecontrib/tomwer/widgets/reconstruction/tests/test_axis.py +0 -16
  42. tomwer/tests/test_ewoks/test_single_node_execution.py +1 -1
  43. tomwer/tests/test_ewoks/test_workflows.py +1 -1
  44. tomwer/version.py +1 -1
  45. {tomwer-1.4.0rc0.dist-info → tomwer-1.4.0rc1.dist-info}/METADATA +2 -2
  46. {tomwer-1.4.0rc0.dist-info → tomwer-1.4.0rc1.dist-info}/RECORD +50 -47
  47. tomwer/core/process/tests/test_axis.py +0 -231
  48. {tomwer-1.4.0rc0.dist-info → tomwer-1.4.0rc1.dist-info}/LICENSE +0 -0
  49. {tomwer-1.4.0rc0.dist-info → tomwer-1.4.0rc1.dist-info}/WHEEL +0 -0
  50. {tomwer-1.4.0rc0.dist-info → tomwer-1.4.0rc1.dist-info}/entry_points.txt +0 -0
  51. {tomwer-1.4.0rc0.dist-info → tomwer-1.4.0rc1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,383 @@
1
+ from __future__ import annotations
2
+
3
+ from silx.gui import qt
4
+ from processview.gui.DropDownWidget import DropDownWidget
5
+ from tomwer.synctools.axis import QAxisRP
6
+ from tomwer.gui.utils.qt_utils import block_signals
7
+ from tomwer.gui.utils.buttons import PadlockButton
8
+ from tomwer.gui.reconstruction.axis.EstimatedCorComboBox import EstimatedCorComboBox
9
+ from tomwer.core.process.reconstruction.axis.side import Side
10
+ from tomwer.core.process.reconstruction.axis import mode as axis_mode
11
+ from tomwer.gui.fonts import FONT_SMALL
12
+ from tomwer.gui.utils.scrollarea import QDoubleSpinBoxIgnoreWheel
13
+ from pyunitsystem.metricsystem import MetricSystem
14
+
15
+
16
+ class EstimatedCORWidget(qt.QGroupBox):
17
+ """
18
+ Widget to define the estimated center of rotation.
19
+ (based on the motor offset and the 'x_rotation_axis_pixel_position')
20
+ """
21
+
22
+ sigValueChanged = qt.Signal()
23
+ """Emit when one of the value changed"""
24
+ sigUpdateXRotAxisPixelPosOnNewScan = qt.Signal()
25
+ """Emit when user want to stop / activate x rotation axis pixel position when a new scan arrives"""
26
+ sigYAxisInvertedChanged = qt.Signal(bool)
27
+
28
+ def __init__(self, parent, axis_params: QAxisRP):
29
+ self._axis_params = axis_params
30
+ self._imageWidth = None
31
+
32
+ super().__init__(parent)
33
+ self.setLayout(qt.QGridLayout())
34
+ # estimated cor
35
+ self._estimatedCORLabel = qt.QLabel("Estimated CoR (relative)", self)
36
+ self.layout().addWidget(self._estimatedCORLabel, 0, 0, 1, 1)
37
+
38
+ self._estimatedCORQCB = EstimatedCorComboBox(self)
39
+ self.layout().addWidget(self._estimatedCORQCB, 0, 1, 1, 1)
40
+
41
+ # offset calibration
42
+ self._offsetWidgetDropdown = DropDownWidget(
43
+ parent=self, direction=qt.Qt.LayoutDirection.RightToLeft
44
+ )
45
+ self._offsetWidget = _OffsetCalibration(parent=self, axis_params=axis_params)
46
+ self.layout().addWidget(self._offsetWidgetDropdown, 1, 0, 2, 2)
47
+ self._offsetWidgetDropdown.setWidget(self._offsetWidget)
48
+
49
+ # set up
50
+ self._offsetWidgetDropdown.setChecked(False)
51
+
52
+ # connect signal / slot
53
+ self._estimatedCORQCB.sigEstimatedCorChanged.connect(self._corChanged)
54
+ self._offsetWidget.sigOffsetChanged.connect(
55
+ self._updateEstimatedCorFromMotorOffsetWidget
56
+ )
57
+ self._offsetWidget.sigXRotationAxisPixelPositionChanged.connect(
58
+ self._updateEstimatedCorFromMotorOffsetWidget
59
+ )
60
+ self._offsetWidget.sigUpdateXRotAxisPixelPosOnNewScan.connect(
61
+ self.sigUpdateXRotAxisPixelPosOnNewScan
62
+ )
63
+ self._offsetWidget.sigYAxisInvertedChanged.connect(self.sigYAxisInvertedChanged)
64
+
65
+ def getEstimatedCor(self):
66
+ return self._estimatedCORQCB.getCurrentCorValue()
67
+
68
+ def setEstimatedCor(
69
+ self, value: float | Side | str, provided_with_offset: bool = False
70
+ ):
71
+
72
+ if isinstance(value, float):
73
+ if self._offsetWidget.isYAxisInverted():
74
+ value = -1.0 * value
75
+
76
+ if provided_with_offset:
77
+ value_with_offset = value
78
+ value_without_offset = value - self._offsetWidget.getOffset()
79
+ else:
80
+ value_with_offset = value + self._offsetWidget.getOffset()
81
+ value_without_offset = value
82
+ with block_signals(self._offsetWidget):
83
+ self._offsetWidget.setXRotationAxisPixelPosition(
84
+ value_without_offset, apply_flip=False
85
+ )
86
+ else:
87
+ # case this is a side
88
+ value_with_offset = value
89
+
90
+ self._estimatedCORQCB.setCurrentCorValue(value_with_offset)
91
+
92
+ with block_signals(self._axis_params):
93
+ self._axis_params.estimated_cor = value_with_offset
94
+
95
+ def _corChanged(self, value):
96
+ assert value is None or isinstance(value, (float, Side, None))
97
+ with block_signals(self._axis_params):
98
+ self._axis_params.estimated_cor = value
99
+ self.sigValueChanged.emit()
100
+
101
+ def _updateVisibleSides(self, mode: axis_mode.AxisMode):
102
+ """
103
+ Update the visibility and selection of sides (Left, Center, Right)
104
+ for the Estimated Center of Rotation (CoR) ComboBox based on the
105
+ provided axis mode and a calculated CoR guess.
106
+
107
+ This method adjusts the available sides and determines the new
108
+ side (Left, Center, or Right) based on the position of a CoR guess
109
+ relative to the width of the image. The image is divided into thirds:
110
+ - Left: From -infinity to the first third.
111
+ - Center: From the first third to the second third.
112
+ - Right: From the second third to infinity.
113
+
114
+ If the CoR guess is not valid or falls outside the calculated bounds,
115
+ a default valid side is selected.
116
+
117
+ Behavior:
118
+ ---------
119
+ - By default if the method allows it, the side will be the estimated CoR.
120
+ - If the method does not allow it, it will update the visible sides in
121
+ the Estimated CoR ComboBox based on the valid sides defined in the axis
122
+ mode metadata and dynamically determines the new side (Left, Center, Right)
123
+ based on the CoR guess position within the image width.
124
+
125
+ """
126
+ mode = axis_mode.AxisMode.from_value(mode)
127
+ self._estimatedCORQCB.setSidesVisible(
128
+ axis_mode.AXIS_MODE_METADATAS[mode].valid_sides
129
+ )
130
+ first_guess_available = axis_mode.AXIS_MODE_METADATAS[
131
+ mode
132
+ ].allows_estimated_cor_as_numerical_value
133
+ self._estimatedCORQCB.setFirstGuessAvailable(first_guess_available)
134
+
135
+ if first_guess_available:
136
+ # if the first guess is valid, when the sides visibility is modify we want to activate it.
137
+ self._estimatedCORQCB.selectFirstGuess()
138
+ elif not isinstance(self._estimatedCORQCB.getCurrentCorValue(), Side):
139
+ # else if the current side cannot be re-used, pick the next one available
140
+ if axis_mode.AXIS_MODE_METADATAS[mode].valid_sides:
141
+ with block_signals(self._estimatedCORQCB):
142
+ cor_guess = self._estimatedCORQCB.getCurrentCorValue()
143
+ if cor_guess is not None and self._imageWidth is not None:
144
+ left_boundary = -float("inf")
145
+ right_boundary = float("inf")
146
+ middle_left_boundary = (
147
+ self._imageWidth / 3 - self._imageWidth / 2
148
+ )
149
+ middle_right_boundary = (
150
+ 2 * self._imageWidth / 3 - self._imageWidth / 2
151
+ )
152
+
153
+ if left_boundary <= cor_guess < middle_left_boundary:
154
+ new_side = Side.LEFT
155
+ elif middle_left_boundary <= cor_guess < middle_right_boundary:
156
+ new_side = Side.CENTER
157
+ elif middle_right_boundary <= cor_guess <= right_boundary:
158
+ new_side = Side.RIGHT
159
+ else:
160
+ new_side = axis_mode.AXIS_MODE_METADATAS[mode].valid_sides[
161
+ 0
162
+ ] # Handle out-of-bound cases if needed
163
+ else:
164
+ new_side = axis_mode.AXIS_MODE_METADATAS[mode].valid_sides[0]
165
+ self._estimatedCORQCB.setCurrentCorValue(new_side)
166
+ self._axis_params.estimated_cor = new_side
167
+
168
+ def _updateEstimatedCorFromMotorOffsetWidget(self):
169
+ self._estimatedCORQCB.setCurrentCorValue(self._offsetWidget.getEstimatedCor())
170
+ self.sigValueChanged.emit()
171
+
172
+ def setImageWidth(self, image_width: float | None):
173
+ self._imageWidth = image_width
174
+
175
+ # expose API
176
+ def updateXRotationAxisPixelPositionOnNewScan(self) -> bool:
177
+ return self._offsetWidget.updateXRotationAxisPixelPositionOnNewScan()
178
+
179
+ def setUpdateXRotationAxisPixelPositionOnNewScan(self, update: bool):
180
+ self._offsetWidget.setUpdateXRotationAxisPixelPositionOnNewScan(update=update)
181
+
182
+ def setPixelSize(self, pixel_size_m: float | None) -> None:
183
+ self._offsetWidget.setPixelSize(pixel_size_m=pixel_size_m)
184
+
185
+ def isYAxisInverted(self) -> bool:
186
+ return self._offsetWidget.isYAxisInverted()
187
+
188
+ def setYAxisInverted(self, checked: bool):
189
+ return self._offsetWidget.setYAxisInverted(checked=checked)
190
+
191
+
192
+ class _OffsetCalibration(qt.QGroupBox):
193
+
194
+ sigOffsetChanged = qt.Signal()
195
+ sigXRotationAxisPixelPositionChanged = qt.Signal()
196
+ sigUpdateXRotAxisPixelPosOnNewScan = qt.Signal()
197
+ sigYAxisInvertedChanged = qt.Signal(bool)
198
+
199
+ def __init__(self, parent, axis_params: QAxisRP):
200
+ super().__init__(parent)
201
+ self._axis_params = axis_params
202
+
203
+ self.setLayout(qt.QVBoxLayout())
204
+
205
+ # x_rotation_axis_pixel_position
206
+ self._xRotationAxisPixelPositionGroup = qt.QGroupBox("NXtomo metadata ")
207
+ self._xRotationAxisPixelPositionGroup.setLayout(qt.QGridLayout())
208
+ self.layout().addWidget(self._xRotationAxisPixelPositionGroup)
209
+ self._xRotationAxisPixelPositionLabel = qt.QLabel(
210
+ "x_rotation_axis_pixel_position"
211
+ )
212
+ self._xRotationAxisPixelPositionGroup.layout().addWidget(
213
+ self._xRotationAxisPixelPositionLabel, 0, 0, 2, 1
214
+ )
215
+ self._xRotationAxisPixelPositionDSB = QDoubleSpinBoxIgnoreWheel(self)
216
+ self._xRotationAxisPixelPositionDSB.setDecimals(2)
217
+ self._xRotationAxisPixelPositionDSB.setRange(-float("inf"), float("inf"))
218
+ self._xRotationAxisPixelPositionDSB.setSuffix(" px")
219
+ self._xRotationAxisPixelPositionDSB.setEnabled(False)
220
+ self._xRotationAxisPixelPositionGroup.layout().addWidget(
221
+ self._xRotationAxisPixelPositionDSB, 0, 1, 2, 1
222
+ )
223
+ self._xRotationAxisPixelPositionKeepUpdatedCB = qt.QCheckBox(
224
+ "Update with\n new scan"
225
+ )
226
+ self._xRotationAxisPixelPositionKeepUpdatedCB.setFont(FONT_SMALL)
227
+ self._xRotationAxisPixelPositionKeepUpdatedCB.setToolTip(
228
+ "Updates the value when a new scan arrives.\n"
229
+ "Once updated this will change the numerical value of the estimated cor (from estimated relative cor)"
230
+ )
231
+ self._xRotationAxisPixelPositionGroup.layout().addWidget(
232
+ self._xRotationAxisPixelPositionKeepUpdatedCB, 0, 2, 1, 1
233
+ )
234
+ self._yAxisInvertedCB = qt.QCheckBox("Y axis inverted")
235
+ self._yAxisInvertedCB.setFont(FONT_SMALL)
236
+ self._yAxisInvertedCB.setToolTip(
237
+ "Sometime the y axis can be inverted (like on ID11).\nIn this case the estimation of the CoR has a flip that must be handled downstream"
238
+ )
239
+ self._xRotationAxisPixelPositionGroup.layout().addWidget(
240
+ self._yAxisInvertedCB, 1, 2, 1, 1
241
+ )
242
+
243
+ # offset group
244
+ self._offsetGroup = qt.QGroupBox("Custom Offset")
245
+ self._offsetGroup.setLayout(qt.QFormLayout())
246
+ self.layout().addWidget(self._offsetGroup)
247
+
248
+ # offset
249
+ self._offsetSB = QDoubleSpinBoxIgnoreWheel(self)
250
+ self._offsetSB.setSuffix(" px")
251
+ self._offsetSB.setDecimals(2)
252
+ self._offsetSB.setRange(-float("inf"), float("inf"))
253
+ self._offsetSB.setEnabled(False)
254
+ self._offsetGroup.layout().addRow("Offset", self._offsetSB)
255
+
256
+ # motor offset
257
+ self._motorOffsetSB = QDoubleSpinBoxIgnoreWheel(self)
258
+ self._motorOffsetSB.setSuffix(" mm")
259
+ self._motorOffsetSB.setDecimals(4)
260
+ self._motorOffsetSB.setRange(-float("inf"), float("inf"))
261
+ self._offsetGroup.layout().addRow("Y Motor Offset", self._motorOffsetSB)
262
+
263
+ # pixel size
264
+ self._pixelSizeSB = QDoubleSpinBoxIgnoreWheel(self)
265
+ self._pixelSizeSB.setSuffix(" µm")
266
+ self._pixelSizeSB.setDecimals(3)
267
+ self._pixelSizeSB.setRange(0, float("inf"))
268
+
269
+ # Add a horizontal layout for the pixel size and the padlock
270
+ pixelSizeLayout = qt.QHBoxLayout()
271
+
272
+ # Add the spin box to the layout
273
+ pixelSizeLayout.addWidget(self._pixelSizeSB)
274
+
275
+ # Add a padlock button
276
+ self._pixelSizePadlock = PadlockButton(self)
277
+ self._pixelSizePadlock.setChecked(True) # Default to locked
278
+ self._pixelSizePadlock.setFixedSize(24, 24)
279
+ self._pixelSizePadlock.setToolTip("Lock/Unlock pixel size")
280
+ pixelSizeLayout.addWidget(self._pixelSizePadlock)
281
+
282
+ # Add the layout to the offset group
283
+ self._offsetGroup.layout().addRow("Pixel Size", pixelSizeLayout)
284
+
285
+ # Initialize the spin box as disabled (locked by default)
286
+ self._pixelSizeSB.setEnabled(False)
287
+
288
+ # Connect signal to handle locking behavior
289
+ self._pixelSizePadlock.toggled.connect(self._togglePixelSizeLock)
290
+
291
+ # set up
292
+ self._xRotationAxisPixelPositionKeepUpdatedCB.setChecked(True)
293
+ self._yAxisInvertedCB.setChecked(False)
294
+
295
+ # connect signal / slot
296
+ self._offsetSB.editingFinished.connect(self._offsetEdited)
297
+ self._xRotationAxisPixelPositionKeepUpdatedCB.toggled.connect(
298
+ self.sigUpdateXRotAxisPixelPosOnNewScan
299
+ )
300
+ self._motorOffsetSB.valueChanged.connect(self._motorOffsetChanged)
301
+ self._pixelSizeSB.valueChanged.connect(self._pixelSizeChanged)
302
+ self._yAxisInvertedCB.toggled.connect(self._yAxisInverted)
303
+
304
+ def getXRotationAxisPixelPosition(self) -> float:
305
+ return self._xRotationAxisPixelPositionDSB.value()
306
+
307
+ def updateXRotationAxisPixelPositionOnNewScan(self) -> bool:
308
+ return self._xRotationAxisPixelPositionKeepUpdatedCB.isChecked()
309
+
310
+ def setUpdateXRotationAxisPixelPositionOnNewScan(self, update):
311
+ self._xRotationAxisPixelPositionKeepUpdatedCB.setChecked(update)
312
+
313
+ def setXRotationAxisPixelPosition(
314
+ self, value: float, apply_flip: bool = True
315
+ ) -> float:
316
+ """Set the 'x_rotation_axis_pixel_position' and flip it if necessary"""
317
+ if apply_flip and self.isYAxisInverted():
318
+ value = -1.0 * value
319
+ self._xRotationAxisPixelPositionDSB.setValue(value)
320
+ return value
321
+
322
+ def getOffset(self) -> float:
323
+ return self._offsetSB.value()
324
+
325
+ def setOffset(self, value: float) -> None:
326
+ self._offsetSB.setValue(value)
327
+ self._offsetEdited()
328
+
329
+ def _offsetEdited(self):
330
+ with block_signals(self):
331
+ self._axis_params.x_rotation_axis_pos_px_offset = self.getOffset()
332
+ self.sigOffsetChanged.emit()
333
+
334
+ def _motorOffsetChanged(self):
335
+ """Recalculate the offset when the motor offset changes."""
336
+ if self._pixelSizeSB.value() and self._motorOffsetSB.value():
337
+ pixel_size_m = self._pixelSizeSB.value() * MetricSystem.MICROMETER.value
338
+ motor_offset_m = self._motorOffsetSB.value() * MetricSystem.MILLIMETER.value
339
+ with block_signals(self):
340
+ self.setOffset(motor_offset_m / pixel_size_m)
341
+ self.sigOffsetChanged.emit()
342
+
343
+ def _pixelSizeChanged(self):
344
+ """Recalculate the offset when the pixel size changes."""
345
+ if self._pixelSizeSB.value() and self._motorOffsetSB.value():
346
+ pixel_size_m = self._pixelSizeSB.value() * MetricSystem.MICROMETER.value
347
+ motor_offset_m = self._motorOffsetSB.value() * MetricSystem.MILLIMETER.value
348
+ with block_signals(self):
349
+ self.setOffset(motor_offset_m / pixel_size_m)
350
+ self.sigOffsetChanged.emit()
351
+
352
+ def _xRotationAxisPixelPositionEdited(self):
353
+ with block_signals(self._axis_params):
354
+ self._axis_params.x_rotation_axis_pixel_position = (
355
+ self.getXRotationAxisPixelPosition()
356
+ )
357
+ self.sigXRotationAxisPixelPositionChanged.emit()
358
+
359
+ def _yAxisInverted(self):
360
+ self.setXRotationAxisPixelPosition(
361
+ value=-1.0 * self.getXRotationAxisPixelPosition(),
362
+ apply_flip=False,
363
+ )
364
+ self._xRotationAxisPixelPositionEdited()
365
+
366
+ def isYAxisInverted(self) -> bool:
367
+ return self._yAxisInvertedCB.isChecked()
368
+
369
+ def setYAxisInverted(self, checked: bool):
370
+ self._yAxisInvertedCB.setChecked(checked)
371
+
372
+ def getEstimatedCor(self) -> float:
373
+ return self.getXRotationAxisPixelPosition() + self.getOffset()
374
+
375
+ def setPixelSize(self, pixel_size_m: float | None):
376
+ if pixel_size_m is None:
377
+ self._pixelSizeSB.clear()
378
+ else:
379
+ self._pixelSizeSB.setValue(pixel_size_m / MetricSystem.MICROMETER.value)
380
+
381
+ def _togglePixelSizeLock(self, locked: bool):
382
+ """Lock or unlock the pixel size spin box."""
383
+ self._pixelSizeSB.setEnabled(not locked)
@@ -0,0 +1,118 @@
1
+ from __future__ import annotations
2
+
3
+ from silx.gui import qt
4
+ from tomwer.core.process.reconstruction.axis.side import Side
5
+ from tomwer.gui.utils.scrollarea import QComboBoxIgnoreWheel
6
+ from tomwer.gui.utils.qt_utils import block_signals
7
+
8
+
9
+ class _EstimatedCorValidator(qt.QDoubleValidator):
10
+ def __init__(self, parent=None):
11
+ super().__init__(parent)
12
+ self.setDecimals(3)
13
+
14
+ def validate(self, a0: str, a1: int):
15
+ """validate float or string that could be part of the side values..."""
16
+ for value in Side.values():
17
+ if value.startswith(a0):
18
+ return (qt.QDoubleValidator.Acceptable, a0, a1)
19
+ return super().validate(a0, a1)
20
+
21
+
22
+ class EstimatedCorComboBox(QComboBoxIgnoreWheel):
23
+ """
24
+ Combobox that display the sides available according to the cor algorithm (left, right, center, all)
25
+ and a dedicated item for cor given manually.
26
+
27
+ This combobox is also editable and and make sure the 'estimated cor' item is up to date according to the QCombobox current value
28
+ """
29
+
30
+ ESTIMATED_COR_ITEM_DATA = "estimated_cor"
31
+
32
+ sigEstimatedCorChanged = qt.Signal(object)
33
+ """Emit when the estimated cor changed. Value can be a float (cor value) or a Side"""
34
+
35
+ def __init__(self, parent=None):
36
+ super().__init__(parent)
37
+ self.addItem("0.0", self.ESTIMATED_COR_ITEM_DATA)
38
+ for side in Side:
39
+ self.addItem(side.value, side)
40
+
41
+ self.setValidator(_EstimatedCorValidator())
42
+ self.setEditable(True)
43
+
44
+ self.setToolTip(
45
+ """Estimated position of the center of rotation (COR) to be given to the cor algorithms. \n
46
+ If you don't have a fair estimate you can provide only a side
47
+ """
48
+ )
49
+ # connect signal / slot
50
+ self.lineEdit().editingFinished.connect(self._corHasBeenEdited)
51
+ self.currentIndexChanged.connect(self._corChanged)
52
+
53
+ def _corHasBeenEdited(self):
54
+ current_cor = self.getCurrentCorValue()
55
+ if isinstance(current_cor, float):
56
+ # keep the item up to date
57
+ self._setCorItemValue(current_cor)
58
+ self.sigEstimatedCorChanged.emit(current_cor)
59
+
60
+ def _corChanged(self):
61
+ self.sigEstimatedCorChanged.emit(self.getCurrentCorValue())
62
+
63
+ def getCurrentCorValue(self) -> Side | float:
64
+ try:
65
+ return float(self.currentText())
66
+ except ValueError:
67
+ return Side(self.currentText())
68
+
69
+ def setCurrentCorValue(self, value: float | Side):
70
+ if isinstance(value, float):
71
+ self._setCorItemValue(value)
72
+ else:
73
+ side = Side(value)
74
+ item = self.findData(side)
75
+ self.setCurrentIndex(item)
76
+
77
+ def _setCorItemValue(self, value: float):
78
+ item_index = self.findData(self.ESTIMATED_COR_ITEM_DATA)
79
+ view = self.view()
80
+ hidden = view.isRowHidden(item_index)
81
+ with block_signals(self):
82
+ self.setItemText(item_index, f"{value:.2f}")
83
+ view.setRowHidden(item_index, hidden)
84
+
85
+ def get_hidden_sides(self) -> tuple[Side]:
86
+ """Return all sides currently hidden"""
87
+ view = self.view()
88
+ return tuple(
89
+ filter(
90
+ lambda side: view.isRowHidden(self.findData(side)),
91
+ [Side(side) for side in Side],
92
+ )
93
+ )
94
+
95
+ def setSidesVisible(self, sides: tuple[Side]):
96
+ """Set side to be visible to the user"""
97
+ sides_visibles = tuple([Side(side) for side in sides])
98
+ view = self.view()
99
+ for side in Side:
100
+ item_idx = self.findData(side)
101
+ view.setRowHidden(item_idx, side not in sides_visibles)
102
+
103
+ need_new_current_value = self.getCurrentCorValue() in self.get_hidden_sides()
104
+ if need_new_current_value:
105
+ # if the current side used cannot be used fall back to the estimated cor from motor
106
+ self.setCurrentIndex(self.findData(self.ESTIMATED_COR_ITEM_DATA))
107
+
108
+ def setFirstGuessAvailable(self, available: bool):
109
+ """
110
+ For some method (only growing window at the moment) a first guess (estimated cor as a float) cannot be given
111
+ """
112
+ view = self.view()
113
+ item_index = self.findData(self.ESTIMATED_COR_ITEM_DATA)
114
+ view.setRowHidden(item_index, not available)
115
+
116
+ def selectFirstGuess(self):
117
+ item_index = self.findData(self.ESTIMATED_COR_ITEM_DATA)
118
+ self.setCurrentIndex(item_index)