From 26e5466982f555d64d95c22cdfd59bca84fa1465 Mon Sep 17 00:00:00 2001 From: x01dc Date: Mon, 13 Jul 2026 21:24:42 +0200 Subject: [PATCH] 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 --- .../widgets/tomo_params/tomo_params.py | 48 +++++++++++++++---- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/csaxs_bec/bec_widgets/widgets/tomo_params/tomo_params.py b/csaxs_bec/bec_widgets/widgets/tomo_params/tomo_params.py index e1b2c25..61be0cb 100644 --- a/csaxs_bec/bec_widgets/widgets/tomo_params/tomo_params.py +++ b/csaxs_bec/bec_widgets/widgets/tomo_params/tomo_params.py @@ -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)