feat(gui): pin status-bar operables right, stop readout width jitter
CI / lint (push) Skipped
CI / test (3.12) (push) Skipped
CI / test (3.13) (push) Skipped
CI / test-with-beamline-plugins (pxi_bec) (push) Skipped
CI / test-with-beamline-plugins (pxii_bec) (push) Skipped
CI / test-with-beamline-plugins (pxiii_bec) (push) Skipped
CI / lint (push) Skipped
CI / test (3.12) (push) Skipped
CI / test (3.13) (push) Skipped
CI / test-with-beamline-plugins (pxi_bec) (push) Skipped
CI / test-with-beamline-plugins (pxii_bec) (push) Skipped
CI / test-with-beamline-plugins (pxiii_bec) (push) Skipped
- Fast Shutter / State / p-group / Session are addPermanentWidget now: QStatusBar itself pins them to the right corner, so wrapping of the other readouts never moves them - passive readouts keep wrapping in the FlowHost on narrow windows - grow-only min-width ratchet on every readout: changing number widths used to re-flow the row each DAQ tick and jitter every neighbour Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -93,16 +93,15 @@ class StatusBar(QStatusBar):
|
||||
|
||||
self.exp_shutter_label = ValueLabel("ExpHutch Shutter", "", self)
|
||||
|
||||
# Every readout lives in ONE wrapping flow host: on narrow windows
|
||||
# QStatusBar used to clip/hide the labels outright, now they wrap to
|
||||
# extra rows and stay readable. Fixed order: passives first, then the
|
||||
# operables (shared ClickableLabel hover affordance) — Cryo | Fast
|
||||
# Shutter | State | p-group | Session at the end. The old left/right
|
||||
# split (addWidget vs addPermanentWidget) is gone with the wrap; no
|
||||
# showMessage is ever used here, so nothing hides the host.
|
||||
# TODO(cryo): passive for now, but placed with the operables because a
|
||||
# cryo operation (fill/anneal menu) is planned; when it gets a click
|
||||
# handler, swap it to a ClickableLabel so it inherits the affordance.
|
||||
# Split layout: the passive readouts wrap in a flow host (narrow
|
||||
# windows used to clip them outright), while the four operables —
|
||||
# Fast Shutter | State | p-group | Session — are addPermanentWidget
|
||||
# so QStatusBar itself pins them to the right corner at a fixed spot
|
||||
# regardless of wrapping; no showMessage is ever used here, so
|
||||
# nothing hides either side.
|
||||
# TODO(cryo): a cryo operation (fill/anneal menu) is planned; when it
|
||||
# gets a click handler, swap to ClickableLabel and move it over to
|
||||
# the permanent (right-pinned) group with the other operables.
|
||||
self.info_host = FlowHost(self)
|
||||
for widget in (
|
||||
self.message_label,
|
||||
@@ -116,14 +115,22 @@ class StatusBar(QStatusBar):
|
||||
self.tell_state_label,
|
||||
self.busy_label,
|
||||
self.cryo_label,
|
||||
self.shutter_label,
|
||||
self.state_label,
|
||||
self.pgroup_label,
|
||||
self.session_label,
|
||||
):
|
||||
self.info_host.add_widget(widget)
|
||||
self.addWidget(self.info_host, 1)
|
||||
|
||||
for widget in (self.shutter_label, self.state_label, self.pgroup_label, self.session_label):
|
||||
self.addPermanentWidget(widget)
|
||||
|
||||
def _ratchet_widths(self, *labels: QLabel) -> None:
|
||||
# ponytail: grow-only min-width ratchet — per-tick number width
|
||||
# changes used to re-flow the row and jitter every neighbour; pin
|
||||
# each label to the widest text it has shown. Never shrinks until
|
||||
# restart, which is fine for a status bar; per-label fixed widths
|
||||
# from font metrics if a pathological long value ever sticks.
|
||||
for label in labels:
|
||||
label.setMinimumWidth(max(label.minimumWidth(), label.sizeHint().width()))
|
||||
|
||||
def set_theme(self, theme: str) -> None:
|
||||
"""Adopt the theme's flag colors: recolor the connection message and
|
||||
re-render the DAQ-driven labels from the last status right away."""
|
||||
@@ -156,6 +163,7 @@ class StatusBar(QStatusBar):
|
||||
@Slot(float)
|
||||
def update_sharpness(self, val: float):
|
||||
self.sharpness.set_value(f"{val:.3f}")
|
||||
self._ratchet_widths(self.sharpness)
|
||||
|
||||
@Slot(float)
|
||||
def update_samcam_fps(self, fps: float):
|
||||
@@ -166,6 +174,7 @@ class StatusBar(QStatusBar):
|
||||
self.samcam_fps.set_value("error")
|
||||
return
|
||||
self.samcam_fps.set_value(f"{fps:.1f}")
|
||||
self._ratchet_widths(self.samcam_fps)
|
||||
|
||||
@Slot(DAQStatusModel)
|
||||
def update_daq_status(self, status: DAQStatusModel):
|
||||
@@ -284,6 +293,21 @@ class StatusBar(QStatusBar):
|
||||
|
||||
html_content_session = f"""Session: {session_flag}"""
|
||||
self.session_label.setText(html_content_session)
|
||||
|
||||
self._ratchet_widths(
|
||||
self.flux,
|
||||
self.transmission,
|
||||
self.ring_current,
|
||||
self.wvl,
|
||||
self.cryo_label,
|
||||
self.shutter_label,
|
||||
self.exp_shutter_label,
|
||||
self.tell_state_label,
|
||||
self.busy_label,
|
||||
self.state_label,
|
||||
self.pgroup_label,
|
||||
self.session_label,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Error updating DAQ status in status bar")
|
||||
|
||||
@@ -378,6 +402,7 @@ class StatusBar(QStatusBar):
|
||||
text = "Session: Vacant"
|
||||
|
||||
self.session_label.setText(text)
|
||||
self._ratchet_widths(self.session_label)
|
||||
|
||||
def show_session_menu(self, global_pos: QPoint | None = None):
|
||||
menu = QMenu(self)
|
||||
|
||||
@@ -64,22 +64,39 @@ def test_state_menu_gates_beam_location_for_non_staff(qtbot, daq_status_factory,
|
||||
assert not _state_menu_entries(non_staff)["Beam location (admin mode only)"]
|
||||
|
||||
|
||||
def test_readouts_wrap_instead_of_clipping(qtbot):
|
||||
def test_readout_width_ratchets_so_numbers_do_not_jitter(qtbot, daq_status_factory):
|
||||
bar = _bar(qtbot)
|
||||
# A wide value pins the minimum width; a narrower one must NOT shrink it,
|
||||
# otherwise every tick re-flows the row and neighbours jitter.
|
||||
bar.update_samcam_fps(1234.5)
|
||||
bar.update_sharpness(0.123)
|
||||
wide = bar.samcam_fps.minimumWidth()
|
||||
assert wide >= bar.samcam_fps.sizeHint().width()
|
||||
bar.update_samcam_fps(5.0)
|
||||
assert bar.samcam_fps.minimumWidth() == wide
|
||||
# The DAQ tick ratchets the rest, session-display path included.
|
||||
bar.update_daq_status(daq_status_factory())
|
||||
bar._update_session_display()
|
||||
assert bar.ring_current.minimumWidth() >= bar.ring_current.sizeHint().width()
|
||||
assert bar.session_label.minimumWidth() >= bar.session_label.sizeHint().width()
|
||||
|
||||
|
||||
def test_readouts_wrap_instead_of_clipping(qtbot, daq_status_factory):
|
||||
bar = _bar(qtbot)
|
||||
# Labels are empty until a DAQ tick paints them; feed one so the flow
|
||||
# measures realistic widths (empty labels never trigger a wrap).
|
||||
bar.update_daq_status(daq_status_factory())
|
||||
host = bar.info_host
|
||||
flow = host.layout()
|
||||
assert isinstance(flow, FlowLayout) # narrows Optional for the checker
|
||||
# Every readout is in the flow host, none clipped away by QStatusBar.
|
||||
for label in (
|
||||
bar.flux,
|
||||
bar.busy_label,
|
||||
bar.cryo_label,
|
||||
bar.shutter_label,
|
||||
bar.state_label,
|
||||
bar.pgroup_label,
|
||||
bar.session_label,
|
||||
):
|
||||
# Passive readouts wrap in the flow host; the four operables are
|
||||
# addPermanentWidget so QStatusBar pins them to the right corner and
|
||||
# they never move when the flow wraps.
|
||||
for label in (bar.flux, bar.busy_label, bar.cryo_label):
|
||||
assert label.parentWidget() is host
|
||||
for label in (bar.shutter_label, bar.state_label, bar.pgroup_label, bar.session_label):
|
||||
assert label.parentWidget() is bar
|
||||
assert label.parentWidget() is not host
|
||||
# Narrow width -> the flow reports a taller (multi-row) height than one
|
||||
# row, and resizing the host pins its minimum height to the wrapped
|
||||
# height so the bar grows instead of cutting labels off.
|
||||
|
||||
Reference in New Issue
Block a user