feat(bec_widgets): add at_each_angle hook dropdown to TomoParamsWidget

A new "At-each-angle hook" combo box in the params panel, built from
the live tomo_at_each_angle_hooks global var (no hardcoded mirror of
registered names). Rebuilt on every populate (i.e. every 2s poll while
not editing, and on entering edit mode), so hooks registered/
unregistered from the CLI while the dialog is open show up
automatically. The currently-set hook is always shown even if it
isn't in the published list (e.g. registered in a different or
since-restarted session), labeled accordingly, so it's never silently
hidden. Special-cased in _populate_fields()/_read_fields() like
tomo_type -- the generic QComboBox handling assumes integer data,
which doesn't fit a hook name string or None. This also makes the
manual params["at_each_angle_hook"] pass-through in
add_edited_to_queue() (added when there was no widget for this field)
redundant; removed, since _read_fields() now supplies it directly
from the dropdown.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
x01dc
2026-07-13 21:24:42 +02:00
co-authored by Claude Sonnet 5
parent c90be4f336
commit 26e5466982
@@ -386,6 +386,16 @@ class TomoParamsWidget(BECWidget, QWidget):
max_=10.0,
decimals=3,
)
self._pw["at_each_angle_hook"] = QComboBox()
self._pw["at_each_angle_hook"].setEnabled(False)
self._pw["at_each_angle_hook"].setToolTip(
"Run a registered custom function at every projection angle instead "
"of the normal acquisition (e.g. move a polarizer in/out between "
"exposures). Registered from the CLI with "
"flomni.register_at_each_angle_hook(name, func) -- this list refreshes "
"automatically while the dialog is open."
)
common_form.addRow("At-each-angle hook:", self._pw["at_each_angle_hook"])
vbox.addLayout(common_form)
# type-1 section
@@ -523,8 +533,10 @@ class TomoParamsWidget(BECWidget, QWidget):
self._pw["tomo_type"].setCurrentIndex(idx)
self._pw["tomo_type"].blockSignals(False)
self._populate_at_each_angle_hook_combo(params.get("at_each_angle_hook"))
for key, widget in self._pw.items():
if key in ("tomo_type", "_requested_total"):
if key in ("tomo_type", "_requested_total", "at_each_angle_hook"):
continue
val = params.get(key)
if val is None:
@@ -562,14 +574,39 @@ class TomoParamsWidget(BECWidget, QWidget):
self._update_type_visibility(type_val)
def _populate_at_each_angle_hook_combo(self, current: Optional[str]) -> None:
"""Rebuild the at_each_angle_hook dropdown from the live
``tomo_at_each_angle_hooks`` registry (published by
``Flomni.register_/unregister_at_each_angle_hook()``) and select
``current``.
``current`` is added as its own entry even if it isn't in the
published list -- e.g. it was registered in a different, or a
since-restarted, session -- so the operator sees what's actually
set instead of it silently disappearing from the dropdown.
"""
combo = self._pw["at_each_angle_hook"]
registered = self._gv_get("tomo_at_each_angle_hooks") or []
combo.blockSignals(True)
combo.clear()
combo.addItem("None (default)", None)
for name in registered:
combo.addItem(name, name)
if current and current not in registered:
combo.addItem(f"{current} (not registered in this session)", current)
idx = combo.findData(current)
combo.setCurrentIndex(idx if idx >= 0 else 0)
combo.blockSignals(False)
def _read_fields(self) -> dict[str, Any]:
"""Read all input widgets into a params dict."""
params: dict[str, Any] = {}
params["tomo_type"] = self._pw["tomo_type"].currentData()
params["at_each_angle_hook"] = self._pw["at_each_angle_hook"].currentData()
for key, widget in self._pw.items():
if key in ("tomo_type", "_requested_total"):
if key in ("tomo_type", "_requested_total", "at_each_angle_hook"):
continue
if isinstance(widget, QDoubleSpinBox):
params[key] = widget.value()
@@ -933,13 +970,6 @@ class TomoParamsWidget(BECWidget, QWidget):
would silently ignore an in-progress edit.
"""
params = self._read_fields()
# _read_fields() only covers widget-backed params (self._pw); there's
# no editable widget for at_each_angle_hook yet, so it would
# otherwise be silently absent from this job's params -- which would
# make the executor leave whatever hook the *previous* queued job
# left active in place for this one too. Pass the current live value
# through unchanged instead of dropping it.
params["at_each_angle_hook"] = self._gv_get("at_each_angle_hook")
error = self._validate(params)
if error:
QMessageBox.warning(self, "Validation error", error)