new_gui Phase 3: staff Tools dialog (beam/sessions/recovery/local-contact/dev-help)

Staff-only Tools button -> StaffToolsDialog with 5 tabs:
- Beam/detector: beam centre/size/mark, ABR GMX/GMY/GMZ tweak + save/goto,
  mono pitch scan, change energy
- Sessions: p-group selector + open-GUI-session admin (request close / force
  remove, force/end session)
- Recovery: confirmation-code actions + maintenance/init/resync ops
- Local contact: simulation toggles, device restart, BEC macros/devices, links
- Dev help: error-code registry + last error payload

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
appleb_m
2026-06-24 17:01:13 +02:00
co-authored by Claude Opus 4.8
parent 5b833bf9eb
commit e4dc3d505c
5 changed files with 532 additions and 2 deletions
+8
View File
@@ -45,6 +45,14 @@ Backend contract is documented in the `new-gui-backend-paths` memory.
readout (`focus_measure`). Camera controls (⚙ gain/exposure → `samcam_settings`,
AF → `autofocus`, ◉ → `send_screenshot_db`) live in the tab strip.
- **Gonio / Beamline tabs** ← axis IP video via `VideoThread` (`frame_ready`).
- **Staff Tools** (⚙ Tools in the top bar, staff only) — a tabbed dialog
(`widgets/staff_tools.py`): **Beam/detector** (beam centre/size/mark, ABR
GMX/GMY/GMZ tweak + save/goto, mono pitch scan, energy), **Sessions**
(p-group selector + open-GUI-session admin: request-close/force-remove,
force/end session), **Recovery** (confirmation-code free/take-over/recover/
unmount + anneal/park&dry/TELL/init/pedestal/resync), **Local contact**
(simulation toggles, device restart, BEC macros/devices, web links), and
**Dev help** (error-code registry + last error payload).
- **Preconditions**: data-collection actions are gated by a ring/shutter/hutch
warning (1-hour snooze); mounting is hard-blocked by the hutch PSS;
`sample_missing` shows a modal (suppressed during automation).
+4 -2
View File
@@ -93,8 +93,10 @@ focus), B (gonio/beamline video), I (alert banner + precondition dialogs +
sample-missing). Still deferred from A: smargon bookmarks (Ctrl-click), helical
markers.
**Phase 3 — staff tooling:** G (beam/detector physics), J (pgroups + sessions),
H (recovery + local-contact admin), developer help.
**Phase 3 — staff tooling:** ✅ DONE (single staff Tools dialog). G (beam/detector
physics), J (pgroups + sessions), H (recovery + local-contact admin), developer
help. Deferred: full device-state status widget, BEC reinit-method picker,
tracebacks/error-log live tabs.
**Phase 4 — polish:** L (file builder), F (face detection), M (analysis panels),
N (shortcuts, layout persistence, tutorials, portrait/compact).
+10
View File
@@ -175,6 +175,7 @@ class MainWindow(QWidget):
self.top_bar.request_baton.connect(self.daq.request_baton)
self.top_bar.release_baton.connect(self.daq.release_baton)
self.top_bar.cancel_baton_request.connect(self._on_cancel_baton)
self.top_bar.tools_clicked.connect(self._open_staff_tools)
self.daq.baton_request_result.connect(self._on_baton_request_result)
self.daq.baton_response_result.connect(self._on_baton_response_result)
if hasattr(self.daq, "baton_incoming_request"):
@@ -340,6 +341,15 @@ class MainWindow(QWidget):
return
self.daq.mount(sample, reference)
def _open_staff_tools(self) -> None:
from aare.gui.new_gui.widgets.staff_tools import StaffToolsDialog
if getattr(self, "_staff_tools", None) is None:
self._staff_tools = StaffToolsDialog(
self.daq, self._palette, lambda: self.manual.last_status, self)
self._staff_tools.show()
self._staff_tools.raise_()
def _on_sample_missing(self, msg: str) -> None:
self.alert.show_message(msg, "error")
if not self._auto_running: # don't interrupt automation with a modal
+14
View File
@@ -51,6 +51,7 @@ class TopBar(QWidget):
request_baton = Signal()
release_baton = Signal()
cancel_baton_request = Signal()
tools_clicked = Signal()
def __init__(self, palette: Palette, beamline_label: str, parent=None):
super().__init__(parent)
@@ -110,6 +111,18 @@ class TopBar(QWidget):
self._baton_btn.clicked.connect(self._on_baton_btn)
lay.addWidget(self._baton_btn)
# staff tools (hidden unless staff)
self._tools_btn = QPushButton("⚙ Tools")
self._tools_btn.setCursor(Qt.PointingHandCursor)
self._tools_btn.setStyleSheet(
f"QPushButton {{ background:{p.surface}; border:1px solid {p.border_control};"
f" border-radius:8px; padding:6px 12px; font-size:12px;"
f" color:{p.text_secondary}; }}"
)
self._tools_btn.clicked.connect(self.tools_clicked)
self._tools_btn.setVisible(False)
lay.addWidget(self._tools_btn)
# avatar
self._avatar = QLabel("--")
self._avatar.setObjectName("Avatar")
@@ -129,6 +142,7 @@ class TopBar(QWidget):
self.toggle.set_mode(mode)
def set_staff(self, is_staff: bool) -> None:
self._tools_btn.setVisible(is_staff)
if is_staff != self._is_staff:
self._is_staff = is_staff
if self._last_baton is not None:
+496
View File
@@ -0,0 +1,496 @@
"""Staff-only Tools dialog: Beam · Sessions · Recovery · Local Contact · Dev help.
A tabbed admin surface wired to the existing DAQWorker. Each tab reads live
values via the ``status_provider`` callable and triggers the matching DAQWorker
methods. Recovery actions use the confirmation-code flow (password prompt).
Everything is staff-gated by the caller (the Tools button only shows for staff).
"""
from __future__ import annotations
import json
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QCheckBox,
QDialog,
QDoubleSpinBox,
QFormLayout,
QGridLayout,
QHBoxLayout,
QHeaderView,
QInputDialog,
QLabel,
QLineEdit,
QMessageBox,
QPlainTextEdit,
QPushButton,
QSpinBox,
QTabWidget,
QTableWidget,
QTableWidgetItem,
QVBoxLayout,
QWidget,
)
from aare.common.coordinate import AerotechCoordinate, Coordinate
from aare.gui.new_gui.theme import build_qss
def _btn(text: str) -> QPushButton:
b = QPushButton(text)
b.setCursor(Qt.PointingHandCursor)
return b
class StaffToolsDialog(QDialog):
def __init__(self, daq, palette, status_provider, parent=None):
super().__init__(parent)
self._daq = daq
self._p = palette
self._status = status_provider
self.setWindowTitle("Staff tools")
self.setModal(False)
self.resize(820, 640)
self.setStyleSheet(build_qss(palette))
lay = QVBoxLayout(self)
self.tabs = QTabWidget()
lay.addWidget(self.tabs)
self.tabs.addTab(self._build_beam_tab(), "Beam / detector")
self.tabs.addTab(self._build_sessions_tab(), "Sessions")
self.tabs.addTab(self._build_recovery_tab(), "Recovery")
self.tabs.addTab(self._build_local_contact_tab(), "Local contact")
self.tabs.addTab(self._build_devhelp_tab(), "Dev help")
self._connect_signals()
# ====================================================== signals / loads
def _connect_signals(self) -> None:
d = self._daq
self._safe_connect(d, "staff_pgroups_loaded", self._on_pgroups)
self._safe_connect(d, "gui_sessions_loaded", self._on_sessions)
self._safe_connect(d, "recovery_action_completed", self._on_recovery_done)
self._safe_connect(d, "sample_resync_completed", self._on_recovery_done)
self._safe_connect(d, "detector_metadata_resync_completed", self._on_recovery_done)
self._safe_connect(d, "local_contact_simulation_state_loaded", self._on_sim_state)
self._safe_connect(d, "local_contact_links_loaded", self._on_links)
self._safe_connect(d, "bec_user_macros_loaded",
lambda v: self._becout.setPlainText("\n".join(v or [])))
self._safe_connect(d, "bec_devices_loaded",
lambda v: self._becout.setPlainText("\n".join(v or [])))
self._safe_connect(d, "error_codes_loaded", self._on_error_codes)
self._safe_connect(d, "last_error_payload_changed", self._on_err_payload)
@staticmethod
def _safe_connect(obj, signal_name, slot):
sig = getattr(obj, signal_name, None)
if sig is not None:
try:
sig.connect(slot)
except Exception:
pass
def _call(self, method: str, *args) -> None:
fn = getattr(self._daq, method, None)
if callable(fn):
fn(*args)
def showEvent(self, event): # noqa: N802
super().showEvent(event)
self._refresh_beam()
self._call("get_all_pgroups")
self._call("load_gui_sessions")
self._call("get_error_codes")
self._call("load_local_contact_simulation_state")
self._call("load_local_contact_links")
self._refresh_pgroup_label()
# ====================================================== Beam / detector
def _build_beam_tab(self) -> QWidget:
w = QWidget()
lay = QVBoxLayout(w)
# Beam centre
form = QFormLayout()
self._bc_x = QDoubleSpinBox(); self._bc_x.setRange(-4000, 4000)
self._bc_y = QDoubleSpinBox(); self._bc_y.setRange(-4000, 4000)
bc_btn = _btn("Set beam centre")
bc_btn.clicked.connect(
lambda: self._call("beam_center", self._bc_x.value(), self._bc_y.value()))
row = QHBoxLayout(); row.addWidget(self._bc_x); row.addWidget(self._bc_y); row.addWidget(bc_btn)
form.addRow("Beam centre (px)", self._wrap(row))
# Beam size (µm)
self._bs_x = QDoubleSpinBox(); self._bs_x.setRange(1, 400)
self._bs_y = QDoubleSpinBox(); self._bs_y.setRange(1, 400)
bs_btn = _btn("Set beam size")
bs_btn.clicked.connect(
lambda: self._call("beam_size_mm", self._bs_x.value() / 1000.0,
self._bs_y.value() / 1000.0))
row2 = QHBoxLayout(); row2.addWidget(self._bs_x); row2.addWidget(self._bs_y); row2.addWidget(bs_btn)
form.addRow("Beam size (µm)", self._wrap(row2))
# Beam mark
self._bm_label = QLabel("")
bm_add = _btn("Mark current beam")
bm_add.clicked.connect(self._beam_mark_add)
bm_clear = _btn("Clear marks")
bm_clear.clicked.connect(lambda: self._call("beam_mark_clear"))
row3 = QHBoxLayout(); row3.addWidget(self._bm_label, 1); row3.addWidget(bm_add); row3.addWidget(bm_clear)
form.addRow("Beam mark (px)", self._wrap(row3))
lay.addLayout(form)
# ABR tweak
lay.addWidget(self._hsep())
lay.addWidget(QLabel("<b>ABR measurement position</b>"))
self._abr_step = QDoubleSpinBox(); self._abr_step.setRange(1, 1000); self._abr_step.setValue(10)
self._abr_step.setSuffix(" µm")
grid = QGridLayout()
self._abr_labels = {}
for r, axis in enumerate(("x", "y", "z")):
grid.addWidget(QLabel(f"GM{axis.upper()}"), r, 0)
minus = _btn(""); minus.setFixedWidth(36)
plus = _btn("+"); plus.setFixedWidth(36)
minus.clicked.connect(lambda _=False, a=axis: self._abr_tweak(a, -1))
plus.clicked.connect(lambda _=False, a=axis: self._abr_tweak(a, +1))
val = QLabel("")
self._abr_labels[axis] = val
grid.addWidget(minus, r, 1); grid.addWidget(plus, r, 2); grid.addWidget(val, r, 3)
steprow = QHBoxLayout(); steprow.addWidget(QLabel("Step")); steprow.addWidget(self._abr_step); steprow.addStretch(1)
save = _btn("Save meas. pos"); save.clicked.connect(lambda: self._call("abr_save"))
goto = _btn("Goto meas. pos"); goto.clicked.connect(lambda: self._call("abr_goto_meas"))
steprow.addWidget(save); steprow.addWidget(goto)
lay.addLayout(steprow)
lay.addLayout(grid)
# Monochromator
lay.addWidget(self._hsep())
lay.addWidget(QLabel("<b>Monochromator</b>"))
mono = QHBoxLayout()
pitch = _btn("Mono pitch scan"); pitch.clicked.connect(lambda: self._call("mono_pitch_scan"))
self._energy = QDoubleSpinBox(); self._energy.setRange(1000, 30000); self._energy.setSingleStep(100)
self._energy.setSuffix(" eV"); self._energy.setValue(12400)
ch = _btn("Change energy")
ch.clicked.connect(lambda: self._call("change_energy", self._energy.value()))
mono.addWidget(pitch); mono.addStretch(1); mono.addWidget(self._energy); mono.addWidget(ch)
lay.addLayout(mono)
refresh = _btn("Refresh readouts"); refresh.clicked.connect(self._refresh_beam)
lay.addWidget(refresh)
lay.addStretch(1)
return w
def _beam_mark_add(self) -> None:
geom = getattr(self._status(), "geom", None)
loc = getattr(geom, "beam_location_pxl", None) if geom else None
if loc is not None:
self._call("beam_mark_add", loc.x, loc.y)
def _abr_tweak(self, axis: str, sign: int) -> None:
step_mm = self._abr_step.value() / 1000.0 * sign
kw = {"x": 0.0, "y": 0.0, "z": 0.0}
kw[axis] = step_mm
self._call("abr_tweak",
AerotechCoordinate(at_mm=Coordinate(**kw), omega_deg=None))
def _refresh_beam(self) -> None:
s = self._status()
geom = getattr(s, "geom", None)
diff = getattr(s, "diffraction", None)
bc = getattr(diff, "beam_center_pxl", None) if diff else None
if bc and len(bc) >= 2:
self._bc_x.setValue(float(bc[0])); self._bc_y.setValue(float(bc[1]))
bs = getattr(geom, "beam_size_mm", None) if geom else None
if bs is not None:
self._bs_x.setValue(float(bs.x) * 1000.0); self._bs_y.setValue(float(bs.y) * 1000.0)
loc = getattr(geom, "beam_location_pxl", None) if geom else None
if loc is not None:
self._bm_label.setText(f"{loc.x:.0f}, {loc.y:.0f}")
meas = getattr(geom, "aerotech_meas", None) if geom else None
if meas is not None:
for axis in ("x", "y", "z"):
self._abr_labels[axis].setText(f"{getattr(meas, axis):.4f} mm")
# ====================================================== Sessions / pgroup
def _build_sessions_tab(self) -> QWidget:
w = QWidget()
lay = QVBoxLayout(w)
# pgroup
pg = QHBoxLayout()
pg.addWidget(QLabel("Current p-group:"))
self._pgroup_label = QLabel("")
pg.addWidget(self._pgroup_label)
pg.addStretch(1)
from PySide6.QtWidgets import QComboBox
self._pgroup_combo = QComboBox(); self._pgroup_combo.setEditable(True)
self._pgroup_combo.setMinimumWidth(160)
set_pg = _btn("Set p-group")
set_pg.clicked.connect(
lambda: self._call("set_pgroup", self._pgroup_combo.currentText().strip()))
pg.addWidget(self._pgroup_combo); pg.addWidget(set_pg)
lay.addLayout(pg)
lay.addWidget(self._hsep())
lay.addWidget(QLabel("<b>Open GUI sessions</b>"))
self._sess_table = QTableWidget(0, 6)
self._sess_table.setHorizontalHeaderLabels(
["Session", "User", "Staff", "Baton", "Last seen", "Close req."])
self._sess_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
self._sess_table.verticalHeader().setVisible(False)
lay.addWidget(self._sess_table, 1)
actions = QHBoxLayout()
refresh = _btn("Refresh"); refresh.clicked.connect(lambda: self._call("load_gui_sessions"))
self._grace = QSpinBox(); self._grace.setRange(5, 3600); self._grace.setValue(60); self._grace.setSuffix(" s")
req = _btn("Request close"); req.clicked.connect(self._request_close)
force = _btn("Force remove"); force.clicked.connect(self._force_remove)
actions.addWidget(refresh); actions.addStretch(1)
actions.addWidget(self._grace); actions.addWidget(req); actions.addWidget(force)
lay.addLayout(actions)
sess = QHBoxLayout()
fs = _btn("Force my session"); fs.clicked.connect(lambda: self._call("force_session"))
es = _btn("End my session"); es.clicked.connect(lambda: self._call("end_session"))
sess.addStretch(1); sess.addWidget(fs); sess.addWidget(es)
lay.addLayout(sess)
return w
def _selected_session_id(self):
row = self._sess_table.currentRow()
if row < 0:
return None
item = self._sess_table.item(row, 0)
return int(item.text()) if item else None
def _request_close(self) -> None:
sid = self._selected_session_id()
if sid is not None:
self._call("request_gui_close", sid, self._grace.value())
def _force_remove(self) -> None:
sid = self._selected_session_id()
if sid is None:
return
if QMessageBox.question(self, "Force remove",
f"Force-remove session {sid}?") == QMessageBox.Yes:
self._call("force_remove_gui_session", sid)
def _on_pgroups(self, pgroups) -> None:
self._pgroup_combo.clear()
self._pgroup_combo.addItems([str(p) for p in (pgroups or [])])
def _refresh_pgroup_label(self) -> None:
sess = getattr(self._status(), "session", None)
cur = getattr(sess, "current_pgroup", None) if sess else None
self._pgroup_label.setText(cur or "")
def _on_sessions(self, sessions) -> None:
self._sess_table.setRowCount(0)
for info in sessions or []:
r = self._sess_table.rowCount()
self._sess_table.insertRow(r)
vals = [
str(getattr(info, "session", "")),
str(getattr(info, "username", "")),
"" if getattr(info, "staff", False) else "",
"" if getattr(info, "holds_baton", False) else "",
self._age(getattr(info, "last_seen_ts", None)),
"yes" if getattr(info, "close_requested", False) else "",
]
for c, v in enumerate(vals):
self._sess_table.setItem(r, c, QTableWidgetItem(v))
@staticmethod
def _age(ts) -> str:
if not ts:
return ""
import time
secs = max(0, int(time.time() - ts))
if secs < 60:
return f"{secs}s ago"
m, s = divmod(secs, 60)
if m < 60:
return f"{m}m {s}s ago"
h, m = divmod(m, 60)
return f"{h}h {m}m ago"
# ====================================================== Recovery
def _build_recovery_tab(self) -> QWidget:
w = QWidget()
lay = QVBoxLayout(w)
warn = QLabel("⚠ Recovery actions can disrupt an active session. "
"Confirmation-code actions require a staff code.")
warn.setWordWrap(True)
warn.setStyleSheet(f"color:{self._p.breakpoint};")
lay.addWidget(warn)
self._recovery_status = QLabel("")
self._recovery_status.setStyleSheet(f"color:{self._p.text_muted};")
lay.addWidget(self._recovery_status)
grid = QGridLayout()
coded = [
("Take over beamline", "take_over_beamline"),
("Free beamline", "free_beamline"),
("Recover beamline", "recover_beamline"),
("Recovery unmount", "recovery_unmount_sample"),
]
for i, (label, method) in enumerate(coded):
b = _btn(label)
b.clicked.connect(lambda _=False, m=method, lbl=label: self._coded_action(lbl, m))
grid.addWidget(b, i // 2, i % 2)
lay.addLayout(grid)
lay.addWidget(self._hsep())
lay.addWidget(QLabel("<b>Maintenance</b>"))
grid2 = QGridLayout()
plain = [
("Anneal…", self._anneal),
("Park && dry", lambda: self._call("park_and_dry")),
("TELL dry", lambda: self._call("tell_dry")),
("Toggle blower", lambda: self._call("tell_toggle_blower")),
("Resync sample", lambda: self._call("resync_sample")),
("Resync detector meta", lambda: self._call("resync_local_contact_detector_metadata")),
("Init smargon", lambda: self._call("initialise_smargon")),
("Init aerotech", lambda: self._call("initialise_aerotech")),
("Init detector", lambda: self._call("initialise_detector")),
("Take pedestal", lambda: self._call("detector_take_pedestal")),
("Cancel current op", lambda: self._call("cancel")),
]
for i, (label, slot) in enumerate(plain):
b = _btn(label); b.clicked.connect(lambda _=False, s=slot: s())
grid2.addWidget(b, i // 3, i % 3)
lay.addLayout(grid2)
lay.addStretch(1)
return w
def _coded_action(self, label: str, method: str) -> None:
if QMessageBox.question(self, label, f"{label}: are you sure?") != QMessageBox.Yes:
return
code, ok = QInputDialog.getText(self, label, "Confirmation code:",
QLineEdit.Password)
if ok and code.strip():
self._call(method, code.strip())
def _anneal(self) -> None:
secs, ok = QInputDialog.getDouble(self, "Anneal", "Time (s):", 1.0, 0.1, 60.0, 1)
if ok:
self._call("anneal", secs)
def _on_recovery_done(self, message: str) -> None:
self._recovery_status.setText(str(message))
# ====================================================== Local contact
def _build_local_contact_tab(self) -> QWidget:
w = QWidget()
lay = QVBoxLayout(w)
lay.addWidget(QLabel("<b>Simulation</b>"))
self._sim_checks = {}
simrow = QHBoxLayout()
for dev in ("tell", "bec", "detector", "aerotech", "smargon"):
cb = QCheckBox(dev)
cb.toggled.connect(
lambda on, d=dev: self._call("set_local_contact_simulation", d, on))
self._sim_checks[dev] = cb
simrow.addWidget(cb)
simrow.addStretch(1)
lay.addLayout(simrow)
lay.addWidget(QLabel("<b>Restart / initialise backend</b>"))
rrow = QGridLayout()
for i, dev in enumerate(("tell", "bec", "detector", "smargon", "aerotech")):
b = _btn(f"Restart {dev}")
b.clicked.connect(lambda _=False, d=dev: self._call("restart_local_contact_device", d))
rrow.addWidget(b, i // 3, i % 3)
lay.addLayout(rrow)
lay.addWidget(QLabel("<b>BEC</b>"))
becrow = QHBoxLayout()
for label, method in (
("Load macros", "bec_load_user_macros"),
("List macros", "bec_list_all_user_macros"),
("List devices", "bec_list_all_devices"),
("Reinit planner", "bec_reinitialise_planner_and_position_devices"),
):
b = _btn(label); b.clicked.connect(lambda _=False, m=method: self._call(m))
becrow.addWidget(b)
lay.addLayout(becrow)
self._becout = QPlainTextEdit(); self._becout.setReadOnly(True); self._becout.setMaximumHeight(120)
lay.addWidget(self._becout)
lay.addWidget(QLabel("<b>Web panels</b>"))
self._links_label = QLabel(""); self._links_label.setOpenExternalLinks(True)
self._links_label.setWordWrap(True)
lay.addWidget(self._links_label)
lay.addStretch(1)
return w
def _on_sim_state(self, state: dict) -> None:
for dev, cb in self._sim_checks.items():
if dev in (state or {}):
cb.blockSignals(True)
cb.setChecked(bool(state[dev]))
cb.blockSignals(False)
def _on_links(self, links: dict) -> None:
parts = []
for k, v in (links or {}).items():
if isinstance(v, str) and v.startswith("http"):
parts.append(f'<a href="{v}">{k}</a>')
else:
parts.append(f"{k}: {v}")
self._links_label.setText(" · ".join(parts) if parts else "")
# ====================================================== Dev help
def _build_devhelp_tab(self) -> QWidget:
w = QWidget()
lay = QVBoxLayout(w)
self._code_filter = QLineEdit(); self._code_filter.setPlaceholderText("Filter error codes…")
self._code_filter.textChanged.connect(self._filter_codes)
lay.addWidget(self._code_filter)
self._codes_table = QTableWidget(0, 1)
self._codes_table.setHorizontalHeaderLabels(["Error code"])
self._codes_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
self._codes_table.verticalHeader().setVisible(False)
lay.addWidget(self._codes_table, 1)
self._all_codes: dict = {}
lay.addWidget(QLabel("<b>Last error payload</b>"))
self._err_payload = QPlainTextEdit(); self._err_payload.setReadOnly(True)
self._err_payload.setMaximumHeight(180)
lay.addWidget(self._err_payload)
return w
def _on_error_codes(self, codes: dict) -> None:
self._all_codes = dict(codes or {})
self._filter_codes(self._code_filter.text())
def _filter_codes(self, text: str) -> None:
text = (text or "").lower()
self._codes_table.setRowCount(0)
for name in sorted(self._all_codes):
if text and text not in name.lower():
continue
r = self._codes_table.rowCount()
self._codes_table.insertRow(r)
self._codes_table.setItem(r, 0, QTableWidgetItem(name))
def _on_err_payload(self, payload: dict) -> None:
try:
self._err_payload.setPlainText(json.dumps(payload, indent=2, default=str))
except Exception:
self._err_payload.setPlainText(str(payload))
# ====================================================== helpers
@staticmethod
def _wrap(layout) -> QWidget:
wid = QWidget()
wid.setLayout(layout)
return wid
def _hsep(self) -> QLabel:
line = QLabel()
line.setFixedHeight(1)
line.setStyleSheet(f"background:{self._p.border_panel};")
return line