feat: status bar splits passive left / operable right
CI / lint (push) Skipped
CI / test (3.11) (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.11) (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
Operable controls (Fast Shutter, State, p-group, Session) group right via addPermanentWidget and share one hover affordance - hand cursor + underline - now owned by ClickableLabel; ValueLabel opts out for the passive readouts, and the unwired busy label becomes a plain QLabel. Cryo sits first in the operable group but stays passive: a cryo operation is planned (TODO in status_bar.py), the placement just reserves its spot. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,34 @@ from PySide6.QtWidgets import QLabel
|
||||
class ClickableLabel(QLabel):
|
||||
clicked = Signal()
|
||||
|
||||
# Hover affordance (hand cursor + underline) marks a label as operable —
|
||||
# the one shared look for every clickable status-bar entry. ValueLabel
|
||||
# flips it off for the passive readouts it renders.
|
||||
_hover_affordance = True
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
if self._hover_affordance:
|
||||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
|
||||
def _set_underline(self, on: bool) -> None:
|
||||
# Font toggle, not QSS :hover — these labels carry rich text (colored
|
||||
# spans), and the widget font reliably underlines it; stylesheet
|
||||
# text-decoration does not reach rich-text content.
|
||||
font = self.font()
|
||||
font.setUnderline(on)
|
||||
self.setFont(font)
|
||||
|
||||
def enterEvent(self, event):
|
||||
if self._hover_affordance:
|
||||
self._set_underline(True)
|
||||
super().enterEvent(event)
|
||||
|
||||
def leaveEvent(self, event):
|
||||
if self._hover_affordance:
|
||||
self._set_underline(False)
|
||||
super().leaveEvent(event)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self.clicked.emit()
|
||||
|
||||
@@ -81,7 +81,8 @@ class StatusBar(QStatusBar):
|
||||
|
||||
self.tell_state_label = QLabel("Tell: —", self)
|
||||
|
||||
self.busy_label = ClickableLabel(parent=self)
|
||||
# Plain QLabel: "Beamline: Busy/Idle" is a passive readout, no click.
|
||||
self.busy_label = QLabel(parent=self)
|
||||
|
||||
self.session_label = ClickableLabel(parent=self)
|
||||
self.session_label.clicked.connect(self.show_session_menu)
|
||||
@@ -90,22 +91,36 @@ class StatusBar(QStatusBar):
|
||||
self.shutter_label.clicked.connect(self.show_shutter_menu)
|
||||
|
||||
self.exp_shutter_label = ValueLabel("ExpHutch Shutter", "", self)
|
||||
|
||||
# Passive readouts group left (addWidget), operable controls group
|
||||
# right (addPermanentWidget) wearing the shared ClickableLabel hover
|
||||
# affordance — fixed order Cryo | Fast Shutter | State | p-group |
|
||||
# Session. No showMessage is ever used here, so the left section is
|
||||
# never hidden by temporary messages.
|
||||
self.addWidget(self.message_label)
|
||||
self.addPermanentWidget(self.sharpness)
|
||||
self.addPermanentWidget(self.samcam_fps)
|
||||
self.addPermanentWidget(self.flux)
|
||||
self.addPermanentWidget(self.transmission)
|
||||
self.addPermanentWidget(self.ring_current)
|
||||
self.addPermanentWidget(self.wvl)
|
||||
self.addPermanentWidget(self.cryo_label)
|
||||
# Beam-path order: the hutch shutter sits upstream of the fast shutter.
|
||||
self.addPermanentWidget(self.exp_shutter_label)
|
||||
self.addPermanentWidget(self.shutter_label)
|
||||
self.addPermanentWidget(self.pgroup_label)
|
||||
self.addPermanentWidget(self.state_label)
|
||||
self.addPermanentWidget(self.tell_state_label)
|
||||
self.addPermanentWidget(self.busy_label)
|
||||
self.addPermanentWidget(self.session_label)
|
||||
for widget in (
|
||||
self.sharpness,
|
||||
self.samcam_fps,
|
||||
self.flux,
|
||||
self.transmission,
|
||||
self.ring_current,
|
||||
self.wvl,
|
||||
self.exp_shutter_label,
|
||||
self.tell_state_label,
|
||||
self.busy_label,
|
||||
):
|
||||
self.addWidget(widget)
|
||||
# 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.
|
||||
for widget in (
|
||||
self.cryo_label,
|
||||
self.shutter_label,
|
||||
self.state_label,
|
||||
self.pgroup_label,
|
||||
self.session_label,
|
||||
):
|
||||
self.addPermanentWidget(widget)
|
||||
|
||||
def set_theme(self, theme: str) -> None:
|
||||
"""Adopt the theme's flag colors: recolor the connection message and
|
||||
|
||||
@@ -4,6 +4,8 @@ from aare.gui.widgets.clickable_label import ClickableLabel
|
||||
class ValueLabel(ClickableLabel):
|
||||
# clicked signal + left-click mousePressEvent inherited from
|
||||
# ClickableLabel — this class only adds the "descr: value unit" text.
|
||||
# Passive readout: no hand cursor / hover underline.
|
||||
_hover_affordance = False
|
||||
|
||||
def __init__(self, text: str, unit: str = "", parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
"""Status bar layout contract: passive readouts left, operable controls
|
||||
right, 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.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_passives_left_operables_right(qtbot):
|
||||
bar = _bar(qtbot)
|
||||
# QStatusBar hides only the non-permanent (left) section behind a
|
||||
# temporary message — probe the grouping through that behavior.
|
||||
bar.showMessage("probe")
|
||||
assert not bar.flux.isVisible()
|
||||
assert not bar.busy_label.isVisible()
|
||||
for label in (
|
||||
bar.cryo_label,
|
||||
bar.shutter_label,
|
||||
bar.state_label,
|
||||
bar.pgroup_label,
|
||||
bar.session_label,
|
||||
):
|
||||
assert label.isVisible()
|
||||
Reference in New Issue
Block a user