From 97ac0acde9588ea0e587a34332f8401bb03cd0c2 Mon Sep 17 00:00:00 2001 From: x01dc Date: Tue, 14 Jul 2026 12:44:28 +0200 Subject: [PATCH] feat(bec_widgets): add "Load into editor" to reuse a queued job's settings Select a single tomo job (command jobs have no params, so they're excluded) in the queue dialog and click "Load into editor" to enter edit mode on the params panel populated from that job's saved snapshot instead of the live backend. Reuses the existing edit-mode machinery (_enter_edit_mode_with(), factored out of enter_edit_mode()) so the operator reviews/tweaks and then uses the normal Submit or "Add to queue" paths -- no new write path, nothing touches live params or the queue itself just by loading. If an edit is already in progress, confirms before discarding it. The button is disabled outside a single-tomo-job selection and during sort mode, and re-evaluated on selection change and on every table refresh. Co-Authored-By: Claude Sonnet 5 --- .../widgets/tomo_params/tomo_params.py | 78 ++++++++++++++++++- 1 file changed, 75 insertions(+), 3 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 69d2298..5789368 100644 --- a/csaxs_bec/bec_widgets/widgets/tomo_params/tomo_params.py +++ b/csaxs_bec/bec_widgets/widgets/tomo_params/tomo_params.py @@ -858,9 +858,37 @@ class TomoParamsWidget(BECWidget, QWidget): # ── edit-mode workflow ──────────────────────────────────────────────────── def enter_edit_mode(self) -> None: - """Unlock all parameter fields for editing.""" - # Refresh first so fields reflect latest backend state - params = self._load_params() + """Unlock all parameter fields for editing, populated from the live + backend.""" + self._enter_edit_mode_with(self._load_params()) + + def load_params_into_editor(self, params: dict[str, Any], source_label: str = "") -> None: + """Enter edit mode populated from ``params`` (e.g. a queued job's + saved snapshot) instead of the live backend -- lets an operator + reuse an earlier job's settings as the starting point for a new + one. Never touches live global vars itself; from here the normal + Submit (writes live, blocked while busy) or "Add to queue" + (queue-only, always allowed) paths take over, same as any other + edit. + + If an edit is already in progress, confirms before discarding it + -- a stray click on a queue row would otherwise silently throw + away unsaved changes. + """ + if self._edit_mode: + what = f"'{source_label}'" if source_label else "these settings" + reply = QMessageBox.question( + self, + "Discard current edit?", + f"An edit is already in progress. Loading {what} will discard " + "it. Continue?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel, + ) + if reply != QMessageBox.StandardButton.Yes: + return + self._enter_edit_mode_with(params) + + def _enter_edit_mode_with(self, params: dict[str, Any]) -> None: self._populate_fields(params) self._set_fields_enabled(True) self._edit_mode = True @@ -1177,11 +1205,19 @@ class TomoQueueDialog(QDialog): self._table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) self._table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) self._table.itemSelectionChanged.connect(self._update_sort_buttons) + self._table.itemSelectionChanged.connect(self._update_load_button) vbox.addWidget(self._table) btn_row = QHBoxLayout() self._btn_add = QPushButton("Add current params to queue") self._btn_add_cmd = QPushButton("Add command…") + self._btn_load = QPushButton("Load into editor") + self._btn_load.setToolTip( + "Load the selected tomo job's saved settings into the params panel's " + "editor -- doesn't touch live params or the queue itself. Review, " + "tweak, then Submit or \"Add to queue\" from there." + ) + self._btn_load.setEnabled(False) self._btn_del = QPushButton("Delete selected") self._btn_clr = QPushButton("Clear all") self._btn_sort = QPushButton("Sort queue…") @@ -1197,6 +1233,7 @@ class TomoQueueDialog(QDialog): btn_exe = QPushButton("Execute queue…") self._btn_add.clicked.connect(self.add_to_queue) self._btn_add_cmd.clicked.connect(self.add_command_to_queue) + self._btn_load.clicked.connect(self._on_load_into_editor) self._btn_del.clicked.connect(self._delete_selected) self._btn_clr.clicked.connect(self._clear_queue) self._btn_sort.toggled.connect(self._toggle_sort_mode) @@ -1205,6 +1242,7 @@ class TomoQueueDialog(QDialog): btn_exe.clicked.connect(self._show_execute_hint) btn_row.addWidget(self._btn_add) btn_row.addWidget(self._btn_add_cmd) + btn_row.addWidget(self._btn_load) btn_row.addWidget(self._btn_del) btn_row.addWidget(self._btn_clr) btn_row.addWidget(self._btn_sort) @@ -1262,6 +1300,10 @@ class TomoQueueDialog(QDialog): item.setForeground(_color_from_hex(color)) item.setToolTip(tooltip) self._table.setItem(row, col, item) + # A row's underlying job can change identity across a refresh (the + # queue was edited from elsewhere while a row stayed selected), so + # re-check eligibility here too, not just on selection-change. + self._update_load_button() # ── queue actions ───────────────────────────────────────────────────────── @@ -1299,6 +1341,35 @@ class TomoQueueDialog(QDialog): self._save_queue(jobs) self._refresh() + def _selected_single_tomo_job(self) -> Optional[dict]: + """Return the single selected job if it's a tomo job, else None + (nothing selected, more than one row selected, or a command job -- + command jobs have no `params` to load into the editor).""" + rows = sorted({idx.row() for idx in self._table.selectedIndexes()}) + if len(rows) != 1: + return None + jobs = self._load_queue() + row = rows[0] + if not 0 <= row < len(jobs): + return None + job = jobs[row] + return job if _job_kind(job) == "tomo" else None + + def _update_load_button(self) -> None: + self._btn_load.setEnabled( + not self._sort_mode and self._selected_single_tomo_job() is not None + ) + + def _on_load_into_editor(self) -> None: + job = self._selected_single_tomo_job() + if job is None: + return + parent = self.parent() + if parent is None or not hasattr(parent, "load_params_into_editor"): + QMessageBox.warning(self, "Can't load", "No params panel available to load into.") + return + parent.load_params_into_editor(dict(job.get("params", {})), job.get("label", "")) + # ── sort mode ───────────────────────────────────────────────────────────── def _toggle_sort_mode(self, checked: bool) -> None: @@ -1322,6 +1393,7 @@ class TomoQueueDialog(QDialog): ) self._table.clearSelection() self._update_sort_buttons() + self._update_load_button() def _selected_single_row(self) -> int: rows = sorted({idx.row() for idx in self._table.selectedIndexes()})