plesty.lib.ui.qt.form
Qt renderer for a device’s controls: one widget per Control.
This module makes no decisions. Which control a parameter gets, what bounds it
has and whether it may be written are settled in
controls_for(), from what the device
declared; here they only become widgets. Keeping it that way is the point of
the exercise — the mapping is testable without a display, and this file cannot
quietly invent a limit.
Two rules govern the traffic in both directions:
Only a human’s edit writes. Editors are wired to
editingFinishedandactivated— signals Qt emits for a person, not for code — so the panel’s own polling can refresh a widget without echoing a write back to the device.An editor being typed into is never overwritten. The operator entering an exposure outranks the poll that arrives mid-keystroke. Focus was the first guard and was not enough: a spin box with keyboard tracking off holds the typed text uncommitted, so a poll that Qt did not consider it focused for wiped the entry — and
Returnthen wrote the polled value back to the instrument. A key is marked on the first keystroke and released when the edit is committed.
Colours come from the theme through assets/shell.qss; none is written here.
Attributes
Classes
The scrolled body, which tells the form when its width changed. |
|
A configuration group's box, which lays its own contents out. |
|
Tells a click from a press held down, for one widget. |
|
A parameter's caption, which is also how that parameter is read. |
|
A reading, in a box of its own size whatever the reading's length. |
|
A scroll area whose wheel moves it sideways. |
|
Every control as a head and a body, in as many columns as the width allows. |
|
The widget tree for one |
Functions
|
Gather controls by configuration group, in declaration order. |
|
Return the small editor an operation argument asks for. |
|
Return the editor itself; |
|
Whether widget is showing the unset state rather than a value. |
|
Return what a widget currently holds. |
|
Show value in widget without emitting an edit. |
Module Contents
- plesty.lib.ui.qt.form._by_group(controls: list[plesty.lib.ui.device_panel.Control]) list[tuple[str, list[plesty.lib.ui.device_panel.Control]]]
Gather controls by configuration group, in declaration order.
The default group stays first whatever order it was declared in — it is the device’s own parameters and its operations, and a panel that opened with a sub-assembly’s box above them would read as belonging to that sub-assembly.
- Parameters:
controls (list[plesty.lib.ui.device_panel.Control]) – The derived controls.
- Returns:
(group, members)in the order the groups are to be shown.- Return type:
list[tuple[str, list[plesty.lib.ui.device_panel.Control]]]
- plesty.lib.ui.qt.form.MIN_COLUMN_PX = 320
- plesty.lib.ui.qt.form.MAX_COLUMNS = 4
- plesty.lib.ui.qt.form.UNBOUNDED = 1000000000000.0
- plesty.lib.ui.qt.form.INT_UNBOUNDED = 2147483647
- plesty.lib.ui.qt.form.MAX_FIELD_PX = 240
- plesty.lib.ui.qt.form.MAX_ARG_PX = 150
- plesty.lib.ui.qt.form.READOUT_PAD_PX = 6
- plesty.lib.ui.qt.form.COPY_OVER = 32
- plesty.lib.ui.qt.form.COPY_TEXT = 'Copy'
- plesty.lib.ui.qt.form.HOLD_MS = 500
- plesty.lib.ui.qt.form.REFRESH_TEXT = 'Refresh'
- plesty.lib.ui.qt.form.LIVE_TEXT = 'Live'
- plesty.lib.ui.qt.form.UNSET_TEXT = 'device default'
- plesty.lib.ui.qt.form.DECIMALS = 6
- plesty.lib.ui.qt.form.MIN_OP_COLUMN_PX = 460
- plesty.lib.ui.qt.form.OPERATIONS_TEXT = 'Operations'
- class plesty.lib.ui.qt.form._Body(parent: PySide6.QtWidgets.QWidget | None, on_resize: Any)
Bases:
PySide6.QtWidgets.QWidgetThe scrolled body, which tells the form when its width changed.
Hold on_resize, called with the new width on every resize.
- Parameters:
parent (Optional[PySide6.QtWidgets.QWidget]) – Parent widget.
on_resize (Any) – Callback taking the body’s new width.
- _on_resize
- resizeEvent(event: Any) None
Report the new width, then resize as usual.
- Parameters:
event (Any)
- Return type:
None
- class plesty.lib.ui.qt.form._GroupBox(title: str, parent: PySide6.QtWidgets.QWidget | None, on_resize: Any)
Bases:
PySide6.QtWidgets.QGroupBoxA configuration group’s box, which lays its own contents out.
It reports its own width rather than taking the panel’s. A box is narrower than the panel by the margins around it, and Qt has not resized it yet when the panel’s own resize is delivered — asked then, it answers the width it had before, so its contents were laid out one resize behind and, on the first pass, for a box that was still zero wide.
Hold on_resize, called with the box’s new width.
- Parameters:
title (str) – The group’s name, shown on the box.
parent (Optional[PySide6.QtWidgets.QWidget]) – Parent widget.
on_resize (Any) – Callback taking the box’s new width.
- _on_resize
- resizeEvent(event: Any) None
Report the new width, then resize as usual.
- Parameters:
event (Any)
- Return type:
None
- class plesty.lib.ui.qt.form._Hold(parent: PySide6.QtWidgets.QWidget, on_click: Any, on_hold: Any)
Tells a click from a press held down, for one widget.
Both mean “ask the device this”, once or over and over, so they belong on the same target rather than on a control and a separate switch beside it. The release after a hold is swallowed: a hold has already acted, and reading once more on the way up would be a stray round trip.
Watch parent’s press and release.
- Parameters:
parent (PySide6.QtWidgets.QWidget) – Widget the timer belongs to.
on_click (Any) – Called on a press released within
HOLD_MS.on_hold (Any) – Called once the press outlasts it.
- _on_click
- _on_hold
- _held = False
- _timer
- began() None
A press started.
- Return type:
None
- ended() None
A press ended; a click acts, a hold already did.
- Return type:
None
- _fired() None
- Return type:
None
- class plesty.lib.ui.qt.form._ReadLabel(text: str, parent: PySide6.QtWidgets.QWidget | None, on_click: Any, on_hold: Any)
Bases:
PySide6.QtWidgets.QLabelA parameter’s caption, which is also how that parameter is read.
Click reads it once, hold keeps reading it. The caption is the target because it is already on every row and costs no width — a read button per row is thirty buttons on a device with thirty parameters — and because the editor beside it must stay exactly what it is: somewhere to type.
Build the caption and make it answer a press.
- Parameters:
text (str) – The caption.
parent (Optional[PySide6.QtWidgets.QWidget]) – Parent widget.
on_click (Any) – Called on a click.
on_hold (Any) – Called when the press is held.
- _hold
- mousePressEvent(event: Any) None
Start timing a press.
- Parameters:
event (Any)
- Return type:
None
- mouseReleaseEvent(event: Any) None
Finish a press: a click reads once, a hold has already started.
- Parameters:
event (Any)
- Return type:
None
- class plesty.lib.ui.qt.form._Readout(parent: PySide6.QtWidgets.QWidget | None = None, cap: int | None = MAX_FIELD_PX)
Bases:
PySide6.QtWidgets.QWidgetA reading, in a box of its own size whatever the reading’s length.
A sensor’s identity or a calibration block is one long line. Shown in a plain label it set the width of its column, and through a flowing grid that is the width of the panel: one reading decided the layout for every other.
So the text lives in a fixed-height strip that scrolls sideways under the wheel, and the strip is the same size whatever is in it. Text that does not fit gets a Copy button, since reading a serial number by scrolling it past a window is not reading it.
Build the empty strip.
- Parameters:
parent (Optional[PySide6.QtWidgets.QWidget]) – Parent widget.
cap (Optional[int]) – Widest the strip may grow.
Nonelets it take what it is given, which is what an operation’s own box hands it: there the strip is the width of the box less the button and the arguments, and capping it there would waste the room the box exists to provide.
- text = ''
- _label
- _strip
- copy_button
- setText(text: str) None
Show text, offering to copy it when it is too long to read here.
- Parameters:
text (str) – The reading.
- Return type:
None
- showEvent(event: Any) None
Take the height from the label once the stylesheet has reached it.
- Parameters:
event (Any)
- Return type:
None
- _fit() None
Make the strip exactly one styled line tall.
Measured from the label rather than from the font: the sheet gives the reading its own padding and the strip a border, and a height that knows about neither clips the text it exists to show — by four pixels, which is enough to cut the descenders off a serial number.
- Return type:
None
- text_of() str
What is being shown.
- Return type:
str
- _copy() None
- Return type:
None
- class plesty.lib.ui.qt.form._WheelScroll
Bases:
PySide6.QtWidgets.QScrollAreaA scroll area whose wheel moves it sideways.
The strip is one line high, so a wheel turned over it can only mean “show me the rest of this” — and the rest of it is to the right.
- wheelEvent(event: Any) None
Scroll horizontally by whatever the wheel reported, either axis.
- Parameters:
event (Any)
- Return type:
None
- class plesty.lib.ui.qt.form._FlowForm(parent: PySide6.QtWidgets.QWidget)
Every control as a head and a body, in as many columns as the width allows.
A
QFormLayoutis one column whatever the window does, so a panel of thirty parameters is thirty rows of scrolling beside an empty half-window. This holds the same entries in a grid and re-places them when the column count changes — the widgets are moved, never rebuilt, so nothing an operator has typed and no value in flight is lost by a resize.An operation is an entry like any other: its button is the head and its result is the body, so buttons line up with each other and with the parameter captions, and results line up with the editors. Built as one opaque widget instead, an operation aligned with nothing — a grid can align cells and not the insides of them, so every button was its own width and every reading beside one started somewhere different.
Lay out inside parent.
- Parameters:
parent (PySide6.QtWidgets.QWidget)
- grid
- _entries: list[tuple[PySide6.QtWidgets.QWidget | None, PySide6.QtWidgets.QWidget, int]] = []
- _columns = 0
- add_block(widget: PySide6.QtWidgets.QWidget, span: int = 1) None
Add one entry that is a single box rather than a head and a body.
An operation is drawn as its own outlined box holding its button, its arguments and its reading together. Inside a box, alignment with the operations beside it is neither possible nor wanted: the border is what says where one operation ends and the next begins, which a row of buttons of eight different widths never did.
- Parameters:
widget (PySide6.QtWidgets.QWidget) – The box.
span (int) – Column pairs it needs.
- Return type:
None
- add_pair(head: PySide6.QtWidgets.QWidget, body: PySide6.QtWidgets.QWidget, span: int = 1) None
Add one entry: a caption and its editor, or a button and its result.
- Parameters:
head (PySide6.QtWidgets.QWidget) – What names the entry — a caption, or an operation’s button.
body (PySide6.QtWidgets.QWidget) – What it shows or takes — an editor, a result, or an operation’s arguments beside its result.
span (int) – Column pairs the body needs. An operation carrying arguments asks for two; squeezed into one it pushes every column out until the panel is wider than its window.
- Return type:
None
- clear() None
Forget every entry and empty the grid, deleting the widgets.
- Return type:
None
- relayout(columns: int) bool
Place every entry in columns columns;
Falsewhen already there.- Parameters:
columns (int) – How many head-and-body columns to use.
- Returns:
Whether anything moved.
- Return type:
bool
- _place(columns: int) None
- Parameters:
columns (int)
- Return type:
None
- class plesty.lib.ui.qt.form.DeviceForm(panel: plesty.lib.ui.device_panel.DevicePanel, parent: PySide6.QtWidgets.QWidget | None = None)
The widget tree for one
DevicePanel.The form owns no state worth reading back: the panel holds the values and tells the form what to show. It is rebuilt whole when the controls arrive, which happens once, after
describeanswers.Build the empty form.
- Parameters:
panel (plesty.lib.ui.device_panel.DevicePanel) – The panel this renders, and the object edits are reported to.
parent (Optional[PySide6.QtWidgets.QWidget]) – Parent widget.
- panel
- widget
- editors: dict[str, PySide6.QtWidgets.QWidget]
- labels: dict[str, PySide6.QtWidgets.QWidget]
- arguments: dict[str, dict[str, PySide6.QtWidgets.QWidget]]
- _outputs: dict[str, tuple[plesty.lib.ui.device_panel.Control, Ellipsis]]
- _editing: set[str]
- groups: dict[str, Any]
- banner
- control_toggle
- refresh_button
- scroll
- body
- stack
- plain
- form
- groups_holder
- _groups_stack
- ops_heading
- ops_holder
- ops
- _columns = 1
- set_controls(controls: list[plesty.lib.ui.device_panel.Control]) None
Replace the form’s rows with controls.
A device that declares configuration groups gets one box per group, titled with it. Flat, the groups are invisible: a motor’s
zeroand a detector’s sit side by side under labels that do not say which is which, and the panel is as long as the sum of them. The box is also what lets a label stay the parameter’s own name, since the group is written once above it rather than into every row.Parameters and operations alike are laid out in as many columns as the panel is wide enough for, and re-placed when it is resized.
- Parameters:
controls (list[plesty.lib.ui.device_panel.Control]) – What the device declared, already derived.
- Return type:
None
- _width_changed(width: int) None
Re-place the controls when the body’s width asks for another column.
- Parameters:
width (int)
- Return type:
None
- columns_for(width: int) int
How many label-and-field columns width has room for.
- Parameters:
width (int) – The body’s width in pixels.
- Returns:
At least one, at most
MAX_COLUMNS.- Return type:
int
- _relayout(width: int | None = None, force: bool = False) None
Lay every flow out for the current width.
Only the device’s own section is laid out from this width. Each group box reports its own — it is narrower than the panel by the margins around it, and Qt has not resized it yet when the panel’s resize arrives, so asking it here lays it out one resize behind.
- Parameters:
width (Optional[int]) – The body’s width; its current one when
None.force (bool) – Re-place even where the column count did not change.
- Return type:
None
- _group_resized(group: str, width: int) None
Lay the group group out for the width its box has now.
- Parameters:
group (str)
width (int)
- Return type:
None
- _group_form(group: str) _FlowForm
Add a titled box for the configuration group group; return its flow.
Its labels sit against the box’s left edge, unlike the form outside it. Right-aligned they hang off the box’s own title, and the wider the box grows the further the parameter names drift from the group they belong to. Against the edge, the title and the parameters under it line up.
- Parameters:
group (str)
- Return type:
- set_value(key: str, value: Any) None
Show value for control key.
A widget the operator is typing in is left alone — a reading that arrives mid-edit must not take the keyboard out of their hands.
- Parameters:
key (str) – The control’s key.
value (Any) – What the device reported.
- Return type:
None
- set_writable(writable: bool) None
Enable or disable every editor and button.
- Parameters:
writable (bool) – Whether the operator may drive the device.
- Return type:
None
- set_result(op: str, value: Any) None
Show what an operation returned, as a reading rather than a mapping.
The device declared what it returns, so the result is rendered from that: no braces, no key repeating the button that was just pressed, and a magnitude an instrument would recognise.
- Parameters:
op (str) – Operation name.
value (Any) – Its result.
- Return type:
None
- set_error(message: str) None
Show message above the form.
- Parameters:
message (str) – What failed; empty hides the banner.
- Return type:
None
- _label(control: plesty.lib.ui.device_panel.Control) PySide6.QtWidgets.QLabel
Return the caption for one control, which is also how it is read.
- Parameters:
control (plesty.lib.ui.device_panel.Control)
- Return type:
PySide6.QtWidgets.QLabel
- _toggle_live(key: str, arguments: Any = None) None
Start or stop re-reading one control, and say which it is.
- Parameters:
key (str)
arguments (Any)
- Return type:
None
- _show_label(key: str) None
Mark a control that is being re-read, and say what a press does.
- Parameters:
key (str)
- Return type:
None
- _readout(control: plesty.lib.ui.device_panel.Control) PySide6.QtWidgets.QWidget
Return a display-only field for a parameter the device reports.
- Parameters:
control (plesty.lib.ui.device_panel.Control)
- Return type:
PySide6.QtWidgets.QWidget
- _editor(control: plesty.lib.ui.device_panel.Control) PySide6.QtWidgets.QWidget
Return the editor a control asks for, wired to write when edited.
- Parameters:
control (plesty.lib.ui.device_panel.Control)
- Return type:
PySide6.QtWidgets.QWidget
- _commit(key: str, value: Any) None
Send an edited value to the device and let polling refresh it again.
Clearing the mark here and not on the write’s reply is deliberate: the panel re-reads the parameter as soon as the write returns, and that reading — whatever the instrument actually took, which may not be what was asked for — is the one that should land in the widget.
- Parameters:
key (str) – The parameter that was edited.
value (Any) – What the operator entered.
- Return type:
None
- _spin(control: plesty.lib.ui.device_panel.Control) PySide6.QtWidgets.QWidget
Return a spin box bounded by what the device declared, and no more.
- Parameters:
control (plesty.lib.ui.device_panel.Control)
- Return type:
PySide6.QtWidgets.QWidget
- _action_row(control: plesty.lib.ui.device_panel.Control) PySide6.QtWidgets.QWidget
Return one operation as a box of its own: button, arguments, reading.
Operations were laid out against the parameters’ columns, and they do not fit them: a button, a couple of arguments and a reading are not a caption and an editor. Aligned they were ragged; boxed, the border is what separates one operation from the next, and the reading takes the width the box has rather than a parameter editor’s cap.
- Parameters:
control (plesty.lib.ui.device_panel.Control)
- Return type:
PySide6.QtWidgets.QWidget
- _repeat(op: str) None
Start or stop repeating an operation with the arguments it has now.
- Parameters:
op (str)
- Return type:
None
- _run(op: str) None
Collect an operation’s arguments and ask the panel to run it.
An argument left unset is left out, so the device applies its own default. Sending the floor instead would be sending a value: a spin box always holds a number, and an untouched timeout went out as a zero-second budget the device duly timed out on.
- Parameters:
op (str) – Operation name.
- Return type:
None
- plesty.lib.ui.qt.form._argument_widget(control: plesty.lib.ui.device_panel.Control, parent: PySide6.QtWidgets.QWidget) PySide6.QtWidgets.QWidget
Return the small editor an operation argument asks for.
Capped narrower than a parameter’s editor: several sit on one row beside the button that uses them, and an uncapped row of them is what pushed the panel wider than its own window.
- Parameters:
control (plesty.lib.ui.device_panel.Control) – The derived argument control.
parent (PySide6.QtWidgets.QWidget) – Parent widget.
- Returns:
A widget holding the argument’s value.
- Return type:
PySide6.QtWidgets.QWidget
- plesty.lib.ui.qt.form._build_argument_widget(control: plesty.lib.ui.device_panel.Control, parent: PySide6.QtWidgets.QWidget) PySide6.QtWidgets.QWidget
Return the editor itself;
_argument_widget()bounds it.- Parameters:
control (plesty.lib.ui.device_panel.Control)
parent (PySide6.QtWidgets.QWidget)
- Return type:
PySide6.QtWidgets.QWidget
- plesty.lib.ui.qt.form._is_unset(widget: PySide6.QtWidgets.QWidget) bool
Whether widget is showing the unset state rather than a value.
Only a spin box built for an omittable argument has one: it is the floor, labelled
UNSET_TEXT, which Qt shows in place of the number.- Parameters:
widget (PySide6.QtWidgets.QWidget) – The argument’s editor.
- Returns:
Whether the argument should be left out of the call.
- Return type:
bool
- plesty.lib.ui.qt.form._widget_value(widget: PySide6.QtWidgets.QWidget) Any
Return what a widget currently holds.
- Parameters:
widget (PySide6.QtWidgets.QWidget) – One of the widget types this module builds.
- Returns:
The value, in the type the widget represents.
- Return type:
Any
- plesty.lib.ui.qt.form._set_widget_value(widget: PySide6.QtWidgets.QWidget, value: Any) None
Show value in widget without emitting an edit.
Signals are blocked around the change: every editor writes to the device when it changes, and a reading arriving from the device must not be sent straight back to it.
- Parameters:
widget (PySide6.QtWidgets.QWidget) – The widget to update.
value (Any) – The value to show.
- Return type:
None