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 (pull_request) Failing after 59s
CI / test (3.12) (pull_request) Successful in 1m4s
CI / test (3.14) (pull_request) Successful in 1m5s
CI / test (3.13) (pull_request) Successful in 1m10s
CI / test-with-beamline-plugins (pxii_bec) (pull_request) Successful in 1m12s
CI / test-with-beamline-plugins (pxi_bec) (pull_request) Successful in 1m14s
CI / test-with-beamline-plugins (pxiii_bec) (pull_request) Successful in 1m25s
CI / test-with-coverage (pull_request) Successful in 1m38s
CI / coverage-analysis (pull_request) Successful in 3s
All readouts (sharpness ... Cryo | Fast Shutter | State | p-group | Session) now live in one FlowLayout host: when the window gets narrow the bar grows extra rows and every value stays readable, where QStatusBar used to clip the permanent group and hide the rest. FlowLayout is the canonical Qt example trimmed down (Qt ships no built-in); the host pins its minimum height to the wrapped height because QStatusBar ignores heightForWidth. The old left/right addWidget vs addPermanentWidget split is gone with the wrap. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
98 lines
3.8 KiB
Python
98 lines
3.8 KiB
Python
"""Status bar layout contract: every readout lives in one wrapping flow
|
|
host (narrow windows wrap instead of clipping), and every operable label
|
|
wears the shared hover affordance."""
|
|
|
|
from aarecommon.models.models import TokenData
|
|
from PySide6.QtCore import QEvent, QPointF, Qt
|
|
from PySide6.QtGui import QEnterEvent
|
|
|
|
from aare.gui.widgets.flow_layout import FlowLayout
|
|
from aare.gui.widgets.status_bar import StatusBar
|
|
|
|
|
|
def _bar(qtbot):
|
|
bar = StatusBar(token=TokenData(sub="u", staff=True, pgroups=["p1"], session=1))
|
|
qtbot.addWidget(bar)
|
|
bar.show()
|
|
return bar
|
|
|
|
|
|
def test_operables_share_the_hover_affordance(qtbot):
|
|
bar = _bar(qtbot)
|
|
for label in (bar.shutter_label, bar.state_label, bar.pgroup_label, bar.session_label):
|
|
assert label.cursor().shape() == Qt.CursorShape.PointingHandCursor
|
|
|
|
# Cryo is grouped with the operables but stays passive for now (TODO in
|
|
# status_bar.py); the plain readouts are passive too.
|
|
for label in (bar.cryo_label, bar.flux, bar.busy_label):
|
|
assert label.cursor().shape() != Qt.CursorShape.PointingHandCursor
|
|
|
|
# Hover underlines, leaving restores.
|
|
enter = QEnterEvent(QPointF(1, 1), QPointF(1, 1), QPointF(1, 1))
|
|
bar.state_label.enterEvent(enter)
|
|
assert bar.state_label.font().underline()
|
|
bar.state_label.leaveEvent(QEvent(QEvent.Type.Leave))
|
|
assert not bar.state_label.font().underline()
|
|
|
|
|
|
def test_state_menu_gates_beam_location_for_non_staff(qtbot, daq_status_factory, monkeypatch):
|
|
from aarecommon.models.models import BeamlineStateEnum
|
|
from PySide6.QtWidgets import QMenu
|
|
|
|
import aare.gui.widgets.status_bar as status_bar_module
|
|
|
|
# exec() would block on a real popup, and PySide's method lookup ignores
|
|
# a class-attribute monkeypatch — swap in a subclass instead. The menu
|
|
# stays inspectable as a child of the bar afterwards.
|
|
class _NoExecMenu(QMenu):
|
|
def exec(self): # pyright: ignore[reportIncompatibleMethodOverride]
|
|
return None
|
|
|
|
monkeypatch.setattr(status_bar_module, "QMenu", _NoExecMenu)
|
|
|
|
def _state_menu_entries(bar):
|
|
bar._status = daq_status_factory(state=BeamlineStateEnum.SampleAlignment)
|
|
bar.show_state_menu()
|
|
# Plain data, not QAction refs: the menu (and its actions) only
|
|
# lives until the next GC pass once show_state_menu returns.
|
|
return {a.text(): a.isEnabled() for a in bar.findChildren(QMenu)[-1].actions()}
|
|
|
|
assert _state_menu_entries(_bar(qtbot))["Beam location"]
|
|
|
|
non_staff = StatusBar(token=TokenData(sub="u", staff=False, pgroups=["p1"], session=1))
|
|
qtbot.addWidget(non_staff)
|
|
assert not _state_menu_entries(non_staff)["Beam location (admin mode only)"]
|
|
|
|
|
|
def test_readouts_wrap_instead_of_clipping(qtbot):
|
|
bar = _bar(qtbot)
|
|
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,
|
|
):
|
|
assert label.parentWidget() is 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.
|
|
one_row = flow.heightForWidth(100_000)
|
|
wrapped = flow.heightForWidth(200)
|
|
assert wrapped > one_row
|
|
host.resize(200, wrapped)
|
|
assert host.minimumHeight() >= wrapped
|
|
# Layout bookkeeping the canonical example demands.
|
|
assert flow.count() > 0
|
|
assert flow.itemAt(0) is not None
|
|
assert flow.itemAt(9999) is None
|
|
taken = flow.takeAt(flow.count() - 1)
|
|
assert taken is not None
|
|
assert flow.takeAt(9999) is None
|