Skip to content

Commit b4a9089

Browse files
authored
feat: enhance grid plan handling and x/y position management in MDASeq and PositionTable (#511)
feat: enhance grid plan handling and x/y position management in MDASequence and PositionTable
1 parent 14bde93 commit b4a9089

4 files changed

Lines changed: 102 additions & 2 deletions

File tree

src/pymmcore_widgets/mda/_core_mda.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,12 @@ def value(self) -> MDASequence:
189189

190190
# if there are no stage positions, use the current stage position
191191
if not val.stage_positions:
192-
replace["stage_positions"] = (self._get_current_stage_position(),)
192+
pos = self._get_current_stage_position()
193+
# clear x/y if using a global absolute grid
194+
# (they're meaningless and useq will warn if they're included)
195+
if val.grid_plan is not None and not val.grid_plan.is_relative:
196+
pos = pos.replace(x=None, y=None)
197+
replace["stage_positions"] = (pos,)
193198
# if "p" is not in the axis order, we need to add it or the position will
194199
# not be in the event
195200
if "p" not in val.axis_order:

src/pymmcore_widgets/mda/_core_positions.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,15 @@ def _perform_full_focus() -> None:
441441
self._mmc.waitForSystem()
442442
_perform_full_focus()
443443

444+
def _set_row_xy_enabled(self, row: int, enabled: bool, tip: str = "") -> None:
445+
"""Enable/disable the XY columns and button for a specific row."""
446+
super()._set_row_xy_enabled(row, enabled, tip)
447+
table = self.table()
448+
xy_btn_col = table.indexOf(self._xy_btn_col)
449+
if xy_btn := table.cellWidget(row, xy_btn_col):
450+
xy_btn.setEnabled(enabled)
451+
xy_btn.setToolTip(tip)
452+
444453
def _on_include_z_toggled(self, checked: bool) -> None:
445454
z_btn_col = self.table().indexOf(self._z_btn_col)
446455
self.table().setColumnHidden(z_btn_col, not checked)

src/pymmcore_widgets/useq_widgets/_mda_sequence.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ def __init__(
347347
self.time_plan.valueChanged.connect(self.valueChanged)
348348
self.stage_positions.valueChanged.connect(self.valueChanged)
349349
self.z_plan.valueChanged.connect(self._validate_af_with_z_plan)
350-
self.grid_plan.valueChanged.connect(self.valueChanged)
350+
self.grid_plan.valueChanged.connect(self._on_grid_plan_value_changed)
351351
self.tab_wdg.tabChecked.connect(self._on_tab_checked)
352352
self.axis_order.currentTextChanged.connect(self.valueChanged)
353353
self.valueChanged.connect(self._update_time_estimate)
@@ -560,8 +560,22 @@ def _on_tab_checked(self, tab_idx: int) -> None:
560560
else:
561561
self._enable_af(True)
562562

563+
if tab_idx in (
564+
self.tab_wdg.indexOf(self.grid_plan),
565+
self.tab_wdg.indexOf(self.stage_positions),
566+
):
567+
with signals_blocked(self):
568+
self._on_grid_plan_value_changed()
569+
563570
self._update_available_axis_orders()
564571

572+
def _on_grid_plan_value_changed(self) -> None:
573+
"""Disable position X/Y when a global absolute grid is active."""
574+
gp = self.grid_plan
575+
has_abs_grid = self.tab_wdg.isChecked(gp) and not gp.value().is_relative
576+
self.stage_positions.setXYEnabled(not has_abs_grid)
577+
self.valueChanged.emit()
578+
565579
def _on_af_toggled(self) -> None:
566580
# if the 'af_per_position' checkbox in the PositionTable is checked, set checked
567581
# also the autofocus p axis checkbox.

src/pymmcore_widgets/useq_widgets/_positions.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,15 @@ class PositionTable(DataTableWidget):
171171
def __init__(self, rows: int = 0, parent: QWidget | None = None):
172172
super().__init__(rows, parent)
173173

174+
# track whether a global absolute grid disables all x/y
175+
self._global_xy_disabled = False
176+
177+
# when a sub-sequence changes, update x/y enabled state for that row
178+
if model := self.table().model():
179+
model.rowsInserted.connect(self._on_table_rows_inserted)
180+
if (rows := self.table().rowCount()) > 0:
181+
self._on_table_rows_inserted(None, 0, rows - 1)
182+
174183
self.include_z = QCheckBox("Include Z")
175184
self.include_z.setChecked(True)
176185
self.include_z.toggled.connect(self._on_include_z_toggled)
@@ -346,6 +355,64 @@ def load(self, file: str | Path | None = None) -> None:
346355
except Exception as e: # pragma: no cover
347356
raise ValueError(f"Failed to load MDASequence file: {src}") from e
348357

358+
def setXYEnabled(self, enabled: bool) -> None:
359+
"""Disable or enable X/Y columns for all rows (e.g. global absolute grid)."""
360+
self._global_xy_disabled = not enabled
361+
table = self.table()
362+
seq_col = table.indexOf(self.SEQ)
363+
tip = "X/Y defined by the global absolute grid plan." if not enabled else ""
364+
for row in range(table.rowCount()):
365+
# skip rows that have their own absolute sub-sequence grid
366+
if enabled:
367+
wdg = table.cellWidget(row, seq_col)
368+
if isinstance(wdg, MDAButton) and _seq_has_absolute_grid(wdg.value()):
369+
continue
370+
self._set_row_xy_enabled(row, enabled, tip)
371+
372+
# ------------------- sub-sequence grid helpers -------------------
373+
374+
def _on_table_rows_inserted(self, parent: object, start: int, end: int) -> None:
375+
"""Connect MDAButton.valueChanged for newly inserted rows."""
376+
table = self.table()
377+
seq_col = table.indexOf(self.SEQ)
378+
tip = "X/Y defined by the global absolute grid plan."
379+
for row in range(start, end + 1):
380+
wdg = table.cellWidget(row, seq_col)
381+
if isinstance(wdg, MDAButton):
382+
wdg.valueChanged.connect(self._on_sub_seq_changed)
383+
# apply global disable to newly added rows
384+
if self._global_xy_disabled:
385+
self._set_row_xy_enabled(row, False, tip)
386+
387+
def _on_sub_seq_changed(self) -> None:
388+
"""Disable x/y for the row if its sub-sequence has an absolute grid."""
389+
btn = self.sender()
390+
if not isinstance(btn, MDAButton):
391+
return
392+
table = self.table()
393+
seq_col = table.indexOf(self.SEQ)
394+
for row in range(table.rowCount()):
395+
if table.cellWidget(row, seq_col) is btn:
396+
has_abs = _seq_has_absolute_grid(btn.value())
397+
# global disable takes precedence
398+
if self._global_xy_disabled and not has_abs:
399+
return
400+
tip = (
401+
"X/Y defined by the absolute grid in the sub-sequence."
402+
if has_abs
403+
else ""
404+
)
405+
self._set_row_xy_enabled(row, not has_abs, tip)
406+
break
407+
408+
def _set_row_xy_enabled(self, row: int, enabled: bool, tip: str = "") -> None:
409+
"""Enable/disable x/y widgets for a single row."""
410+
table = self.table()
411+
for col_info in (self.X, self.Y):
412+
if wdg := table.cellWidget(row, table.indexOf(col_info)):
413+
wdg.setEnabled(enabled)
414+
wdg.setToolTip(tip)
415+
349416
# ------------------------- Private API -------------------------
350417

351418
def _on_include_z_toggled(self, checked: bool) -> None:
@@ -357,3 +424,8 @@ def _on_af_per_position_toggled(self, checked: bool) -> None:
357424
af_col = self.table().indexOf(self.AF)
358425
self.table().setColumnHidden(af_col, not checked)
359426
self.valueChanged.emit()
427+
428+
429+
def _seq_has_absolute_grid(seq: useq.MDASequence | None) -> bool:
430+
"""Return True if the sequence has an absolute grid plan."""
431+
return bool(seq and seq.grid_plan and not seq.grid_plan.is_relative)

0 commit comments

Comments
 (0)