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>
User decision: the drag-resize bars beside the Sample camera view are
not wanted - the side panels don't need resizing. Back to the plain
HBox with zoom-scaled fixed column widths; the View-menu collapse
toggles and the camera minimum-width clamp stay.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root cause of the long-standing cross-test SystemError flake
("QPushButton returned NULL", CI failing test_title_label after an
unrelated file): TitleLabel schedules
QTimer.singleShot(0, self._apply_collapsed) at construction; when the
banner is deleted before the event loop spins (what every panel test
does), the pending timer fires into the dead C++ object and poisons
whatever test runs next. Deterministic repro added as a regression
test. The receiver-context overload singleShot(0, self, ...) auto-
cancels the callback when the widget dies. Same fix for the two
deferred main-window callbacks (dock split, floating-dock enlarge).
Note for the record: the LOCAL segfault storm this afternoon was the
dev machine's disk hitting 100% full (uv cache at 15G) — Bus errors
from mmap on a full disk, unrelated to any code.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CI's basedpyright (latest from the index, newer than the local venv)
types win.daq as the real DAQWorker, so assert_not_called on the
patched-in MagicMock flagged the two new gate-test lines.
Note: the pytest job segfaults are NOT from this branch — plain
origin/main crashes 2/4 runs under PySide6 6.11.2 (what CI installs;
uv.lock pins 6.9.0 which is stable). Needs a separate dependency fix.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Why the width shrink only worked after a Playlist Mode round-trip:
Qt auto-pins a top-level window's minimum size to its layout minimum,
and returning from Playlist Mode hid the Information dock (and every
side dock), which happened to drop that layout minimum. The main view
now gets an explicit small minimum (320x240) which disables the
auto-pin outright, so the window shrinks directly and the dock
contents clip or scroll instead.
The Playlist Mode (Ctrl+6) and Automation View (Ctrl+5) prototypes
are deleted per user decision: panels, pages, the content stack (the
standard page is the only page left), menu actions, camera fan-out to
their view copies, portrait alert interceptors (now plain runtime
notification routers), and their styles (portraitRoot, compact*
cards, COMPACT_* and FONT_HERO knobs).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- center splitter: left controls | camera tabs | beamline column;
drag resizes the camera width, View menu check actions collapse
the fixed-width side panels (no on-screen buttons)
- font zoom restricted to a 100/125/150 percent ladder; the
fixed-width side boxes scale with it so zoomed text does not clip
- camera tab widget min width clamped to 1/3 of its natural minimum
- resize bars: short centered grip PNGs shared by QMainWindow
separators and QSplitter handles, single style source in styles.py
- window min width 1244 -> ~376: automation row and sample-list
chip row stop propagating their natural minimums (Ignored-width
hosts), portrait page fixed width removed (portrait mode pins the
window width itself)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The sample list's Mount/Gridscan/Screening/Rotation count columns show
only a glyph in the header to save width; hovering now shows the full
name via ToolTipRole. Text headers stay tooltip-free.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The shared "Rotation transmission" row sat in the top rows of BOTH the
Gridscan and Rotation tabs, where it read as one setting for everything
(and as nonsense in the Gridscan tab). Now each tab places its own row:
Gridscan keeps "Transmission" with the shared resolution/distance rows,
and the Rotation tab moves "Rotation transmission" below the Screening
block and its Run button, as the first row of the Rotation section -
next to the fields it actually applies to. Screening keeps its separate
"Screening transmission". No behavior change: same field, same
spreadsheet/user-value handling, only the label and the placement.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A "Currently mounted: <sample_name>" HUD line in the top-left corner of
the sample camera, driven by the DAQ status (clears when nothing is
mounted). Painted like the other camera HUDs and sized through the text
zoom, which QSS cannot reach here.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Ctrl+plus / Ctrl+minus / Ctrl+0 (and View menu entries) scale the QSS
FONT_* ladder and the application default font together; the scale is
clamped to 0.8-1.6 and persisted in QSettings appearance/font_scale.
- Resize gutters keep a 5px hover/drag region but paint only a 2px line
with a 1px shadow (hard-stop gradients per orientation).
- WCAG contrast fixes: separators were 1.0-1.2:1 against every theme
background, now >=3.25:1 (dark theme gold @70%); scrollbar handle
1.17:1 -> 3.38:1; unselected tab text on the sky gradient 2.07:1 ->
4.71:1 via TAB_IDLE_TEXT.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Count columns use glyphs instead of long headers to save width, Comment
moves ahead of the data-collection params, transmission shown as the
stored value instead of percent.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The mounted sample used to tint only the frozen "#" cell. Paint the full
row in the same blue so it is findable at a glance, and keep the theme's
own text color on it (the mid-tone blue reads fine without the dark ink
that the pastel status tints need). Repaint the whole row span on
mount/unmount, not just column 0.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Add Oscillation, Exposure, Total range and Transmission columns to the
dewar sample table, read from SampleShortInfo.aaredb_params (already
filled by spreadsheetupdater from AareDB, so no server change).
Transmission is shown as percent like the user spreadsheet.
The new columns are nullable, so the header sort key now puts None last
instead of raising on None < float.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
CI's diff-coverage gate sat at 29% because the ZMQ fixes live in code no
test reached: the subscriber's socket setup, _emit_image/notify_frame_
displayed, _recv_latest and the run() frame path, plus the main window's
frame slot. The subscriber now runs against a real inproc socket (the
drain and the in-flight cap are the fix, so a mock would prove nothing);
run() is driven with a stubbed _recv_latest that hands over one JPEG and
then stops. The busy overlay gets a check that cooling is the only blue
busy state.
The subscriber fixture takes qapp so no QImage is built before the
QApplication exists.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The sample camera drew its own gold/orange/red "Session Vacant /
Baton Requested / Guest Mode" box whenever the busy overlay had nothing
to show. Since the busy-style builder already receives the session
state, that box only ever appeared while the session was still unknown
(no status yet, or one without a session), so it flashed a different
color and shape than the yellow "Viewing mode, click here to grab the
baton" badge every known not-owned state gets.
Hoist that badge into VIEWING_MODE_STYLE and paint it from both paths;
the hover fill is shared through _badge_fill. The three MARK_TOOLTIP
constants had no other users and are dropped.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
CI resolves aarecommon 0.7.3, where DataCollectionParameters.transmission
is a 0-to-1 fraction that rejects anything above 1.0 (the percent sheets
are converted inside AareDB >= 0.83, which main already requires). The
divide-by-100 from the earlier 'percentage' fix therefore failed the
model test on CI and would have turned 20% into 0.2% at the beamline.
Pass the fraction through and pin aarecommon>=0.7.3 so the older int
percentage model can no longer be installed; relock.
Also satisfy the diff typecheck gate: basedpyright only counts instance
variables assigned in __init__, so the Database/User toggle widgets are
created there and _build_source_toggle only lays them out.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
/status cannot tell an auto centering from a manual alignment move (both
are busy + SampleAlignment, the combo the overlay deliberately hides), so
the DAQ worker emits its own auto_centering flag: True while its own
/alc/center_loop POST is in flight (the endpoint blocks until done) or
while the automation progress reports the Center step running. The
sample camera views pass it to build_busy_overlay_style, which paints a
purple AUTO CENTERING wave text like the ROBOT MOUNTING/COOLING ones.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
DataCollectionParameters.transmission is a StrictInt documented as
"positive, between 0 and 100" (aareDB's own API description of the
spreadsheet payload), while every model that consumes it wants a 0-to-1
fraction. The old "divide by 100 only when above 1.0" guess therefore
turned a spreadsheet asking for 1% into a scan at 100% transmission -
a hundredfold dose on the sample. A fraction cannot be stored in that
column at all: the model rejects 0.2.
The mapping test now builds a real DataCollectionParameters instead of a
stand-in namespace, so a renamed column fails the test rather than the
beamline. That is what let 'totalrange' through.
daq.py's spreadsheet_params and get_auto_raster_params still carry both
the old column name and the same transmission guess.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHoUkj66jxByS2ypY5h9Mn
The column is totalangle, not totalrange, so reading it raised
AttributeError four times a second in the status loop.
Reading a spreadsheet cell now goes through _spreadsheet_float, which
treats a missing column, an empty cell and a cell that does not hold a
number all as "not set" and logs the column once. aareDB owns these
names; the GUI should fall back to its defaults when one moves, not die
on the status loop it is read from.
Note that the same misspelling is still in daq.py (spreadsheet_params
and get_auto_raster_params), where getattr's default hides it: automation
has never picked up the total angle from the spreadsheet either.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHoUkj66jxByS2ypY5h9Mn
The panels kept the effective detector distance, resolution and
transmission in their own attributes, and a sample change overwrote those
from the spreadsheet whatever the Database/User toggle said. The fields
went on showing the user's numbers while the scan request carried the
database ones, so a value had to be re-entered after every mount.
The fields are now the only place a value lives. Each setting is worth,
in order: what the user typed while "User values" is selected, what the
mounted sample asks for, the panel default - one rule, ScanSettingsPanel
._setting, that each panel spells out a line at a time. Overrides are per
setting, so a setting the user did not touch keeps following the sample.
Along the way:
* DbOverrideLineEdit is gone; the panels use NumberLineEdit directly, and
the per-widget database/user bookkeeping is replaced by SampleParameters,
the one place that knows the aareDB spreadsheet column names.
* The (name, widget, converter) mapping loops with getattr/hasattr are
replaced by explicit per-field commit slots.
* The omega speed cap no longer drives one field's validator from another
field's value: it is a read-out plus a check on the pair at Run time.
* _add_row/_add_pair_row build the label/field/unit rows, which also puts
the Total angle degree sign on its own row instead of the header above.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHoUkj66jxByS2ypY5h9Mn
x=20 sits on a digit glyph with CI's Linux fonts, so the assert compared
an antialiased glyph blend against the pure background. The padding area
(border 1px + padding 6px) can never contain text on any font.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0147jE48bQUTm9AqNQApT6b2
The movestate property sat on the spinbox's inner QLineEdit, which both
themes deliberately paint transparent (QAbstractSpinBox QLineEdit rule)
- the state machine worked but no color ever showed, and the tests only
asserted the property, not the paint. The property now lives on the
spinbox itself with QAbstractSpinBox[movestate=...] added to both
themes' rules, verified by a pixel-sampling regression test that grabs
the rendered widget through the real stylesheet.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The energy setpoint applies via the Change Energy button, not Enter, so
it gets the full motor state machine instead of the typing-pending
color: neutral tracks the readback, a user edit stages pending (and is
the only thing that enables the button), the click turns moving, and
readback arrival within tol returns to neutral. New SpinMoveState in
motor_move_group.py adapts the pattern to a QDoubleSpinBox+button pair
(colors ride the movestate QSS via the spin's internal QLineEdit);
replaces the hand-rolled pending-until-click in the exp-config row and
covers the Beamline setup row that had no color at all. The server's
0.0 detector-unavailable energy is kept out of the readback feed so it
cannot clamp the spin to the range minimum.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both Set Energy rows allowed 1-30 keV, an arbitrary spinbox default;
nothing downstream validates the request (daq.change_energy forwards
straight to bec). Shared ENERGY_MIN/MAX_KEV constants now bound both
spins to 4-20 keV with a tooltip flagging the range as pending
Beamline Scientist confirmation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Values inside the Experiment configuration banner now show the same
pending color as the motor move boxes while the typed text differs from
the applied value; Enter (or focus-out) commits as before and clears it.
Opt-in via NumberLineEdit(track_pending=True) because MotorMoveGroup
owns the movestate property on its registered boxes; reuses the existing
movestate QSS, so no new styling. The Set Energy spinbox marks pending
until Change Energy is clicked, since its apply gate is the button.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The fix removed Moving from the motion-watch trigger but the unit test and code comment still described the old behavior, so pre-push failed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Auto Centering moves out of the experiment tabs to its own row between
Manual sample and Exp. Config., with an apply-after-mount checkbox that
re-runs it on every detected sample mount (guarded against firing on GUI
startup). Draw a Grid now only shows on the Raster scan tab.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Click is no longer the only way back to the "?" badge: moving the
pointer off the expanded box, or out of the widget entirely, folds it.
Checked before the interaction gate so it works in viewing mode too.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bottom-right corner shows the hovered pixel coordinates with a grey
1-2-5 scale bar above them, derived from geom.pixel_in_mm and the
current zoom. Works in viewing mode (before the interaction gate),
clears on leave, and skips drawing outside the image.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
basedpyright requires _admin_tip_anchor to exist after __init__, so the
anchor is created eagerly instead of lazily. Cover the red-tip branch and
the status-bar menu gate; the menu test swaps in a QMenu subclass with a
no-op exec because PySide method lookup ignores class-attr monkeypatches.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>