plesty.lib.ui.qt
PySide6 backend of the PLESTY shell.
Importing this package requires the gui extra
(uv add "plesty-lib[gui]"): PySide6 and pyqtgraph. Everything above it —
Panel, Theme,
MonitorPanel — imports without a GUI
installed, so headless hosts and CI keep working.
Submodules
Classes
The widget tree for one |
|
Draw target that renders a monitor's traces into a pyqtgraph widget. |
|
Capture a window while active; encode a video file on stop. |
|
One sub-window of the shell, wrapping a |
|
Title bar of a sub-window: state dot, title, live status, controls. |
|
Home window hosting every panel as a rearrangeable sub-window. |
Functions
|
Return the running Qt application, creating it when there is none. |
|
Create a shell hosting panels, without starting the event loop. |
|
Open a shell on panels and run it until the window is closed. |
Package Contents
- plesty.lib.ui.qt.application(name: str = 'PLESTY') PySide6.QtWidgets.QApplication
Return the running Qt application, creating it when there is none.
- Parameters:
name (str) – Application name, used by the window manager.
- Returns:
The
QApplicationinstance.- Return type:
PySide6.QtWidgets.QApplication
- plesty.lib.ui.qt.build_shell(panels: Sequence[plesty.lib.ui.panel.Panel], *, title: str = 'PLESTY', theme: plesty.lib.ui.theme.Theme | None = None, interval_ms: int = 500, layout_file: str | pathlib.Path | None = None, remember_layout: bool = True) plesty.lib.ui.qt.shell.Shell
Create a shell hosting panels, without starting the event loop.
- Parameters:
panels (Sequence[plesty.lib.ui.panel.Panel]) – Panels to add, in the order they should be docked.
title (str) – Window title and layout key.
theme (Optional[plesty.lib.ui.theme.Theme]) – Palette;
Noneloads the packaged PLESTY theme.interval_ms (int) – Panel refresh interval in milliseconds.
layout_file (Optional[str | pathlib.Path]) – Where the arrangement is stored.
remember_layout (bool) – Restore and save the arrangement.
- Returns:
The shell, ready for
start().- Return type:
- plesty.lib.ui.qt.run(panels: Sequence[plesty.lib.ui.panel.Panel], *, title: str = 'PLESTY', theme: plesty.lib.ui.theme.Theme | None = None, interval_ms: int = 500, layout_file: str | pathlib.Path | None = None, remember_layout: bool = True) int
Open a shell on panels and run it until the window is closed.
- Parameters:
panels (Sequence[plesty.lib.ui.panel.Panel]) – Panels to add, in the order they should be docked.
title (str) – Window title and layout key.
theme (Optional[plesty.lib.ui.theme.Theme]) – Palette;
Noneloads the packaged PLESTY theme.interval_ms (int) – Panel refresh interval in milliseconds.
layout_file (Optional[str | pathlib.Path]) – Where the arrangement is stored.
remember_layout (bool) – Restore and save the arrangement.
- Returns:
The Qt exit code.
- Return type:
int
- class plesty.lib.ui.qt.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
- class plesty.lib.ui.qt.PlotRenderer(theme: plesty.lib.ui.theme.Theme | None = None, parent: PySide6.QtWidgets.QWidget | None = None)
Bases:
plesty.lib.monitor.base_monitor.RendererDraw target that renders a monitor’s traces into a pyqtgraph widget.
The renderer owns its widget (
widget), so a panel builds one and hands the widget to the shell:renderer = PlotRenderer(theme) monitor.attach(renderer) # builds the plot items, replays the last data shell_panel_widget = renderer.widget
Create the plot widget.
- Parameters:
theme (Optional[plesty.lib.ui.theme.Theme]) – Palette to resolve trace colours and chrome against;
Noneloads the packaged PLESTY theme.parent (Optional[PySide6.QtWidgets.QWidget]) – Parent widget.
- theme
- widget
- plot
- caption
- _layout
- _items: dict[str, Any]
- _images: dict[str, pyqtgraph.ImageItem]
- _colorbars: dict[str, pyqtgraph.ColorBarItem]
- _texts: dict[str, tuple[str, str]]
- _names: dict[str, str]
- _named: list[str] = []
- _colormap
- _lut
- setup(monitor: plesty.lib.monitor.base_monitor.Monitor) None
Build one plot item per declared trace.
- Parameters:
monitor (plesty.lib.monitor.base_monitor.Monitor) – The monitor that was attached.
- Return type:
None
- draw(monitor: plesty.lib.monitor.base_monitor.Monitor, data: dict[str, plesty.lib.monitor.base_monitor.TraceData]) None
Feed new payloads into the plot items.
- Parameters:
monitor (plesty.lib.monitor.base_monitor.Monitor) – The monitor that produced the update.
data (dict[str, plesty.lib.monitor.base_monitor.TraceData]) – Trace key → payload.
- Return type:
None
- clear(monitor: plesty.lib.monitor.base_monitor.Monitor) None
Empty every plot item without rebuilding them.
- Parameters:
monitor (plesty.lib.monitor.base_monitor.Monitor) – The monitor whose view is being reset.
- Return type:
None
- _style_plot() None
Apply the theme to the plot background, axes, and grid.
- Return type:
None
- _style_colorbar(bar: pyqtgraph.ColorBarItem, label: str) None
Apply the theme to a colour bar and name what it measures.
- Parameters:
bar (pyqtgraph.ColorBarItem) – The bar beside an image trace.
label (str) – The trace’s label, which names the quantity the colours stand for.
- Return type:
None
- _place_legend(wanted: bool) None
Put the legend in its own column right of the plot, or take it away.
A legend floating inside the plot covers the data it names, and a corner that is empty for one frame is full for the next. In the plot item’s layout it takes its width from the plot instead. The plot item adds every named curve to
item.legend, so that is where it goes.- Parameters:
wanted (bool) – Whether the view has curves to name.
- Return type:
None
- _sync_legend() None
Show a legend entry for every named curve that has data, in declaration order.
A view declares its traces once, before any data, often more than it will fill — a slot per line it might find. An entry for a curve with nothing drawn would name a line that is not there.
- Return type:
None
- _drop_colorbars() None
Remove every colour bar from the plot item’s layout.
- Return type:
None
- _build_item(spec: plesty.lib.monitor.base_monitor.TraceSpec, index: int) None
Create the plot item declared by one trace spec.
- Parameters:
index (int)
- Return type:
None
- _draw_curve(key: str, payload: plesty.lib.monitor.base_monitor.TraceData) None
Update one line or scatter trace.
- Parameters:
key (str)
- Return type:
None
- _draw_image(key: str, payload: plesty.lib.monitor.base_monitor.TraceData, scale: str = 'linear') None
Update one image trace, scaling it onto its physical axes.
- Parameters:
key (str)
scale (str)
- Return type:
None
- _draw_text(key: str, spec: plesty.lib.monitor.base_monitor.TraceSpec, payload: plesty.lib.monitor.base_monitor.TraceData) None
Record a scalar or text trace for the caption under the plot.
The label is written in the trace’s colour, so a text that belongs to a curve — a fitted line’s result — reads as that curve’s. An empty text takes the entry out of the caption.
- Parameters:
key (str)
- Return type:
None
- _caption_html() str
Return the caption: scalar readings on one line, then each text on its own line.
- Return type:
str
- class plesty.lib.ui.qt.Recorder(widget: PySide6.QtWidgets.QWidget, path: str | pathlib.Path, *, fps: int = DEFAULT_FPS, every: int = 1)
Capture a window while active; encode a video file on stop.
shell.start_recording("run.mp4", fps=10) ... # the shell captures on every tick path = shell.stop_recording() # → run.mp4, or run.gif without ffmpeg
Prepare a recording of widget.
- Parameters:
widget (PySide6.QtWidgets.QWidget) – The window to capture — normally the shell itself.
path (str | pathlib.Path) – Destination file; the suffix is corrected to what the available encoder can actually write.
fps (int) – Frame rate of the encoded video.
every (int) – Capture one frame per every refreshes, for a run whose rows arrive faster than a video needs them.
- Raises:
ValueError – If fps or every is not positive.
- widget
- path
- fps = 10
- every = 1
- active = False
- frames: list[numpy.ndarray] = []
- _shape: tuple[int, int] | None = None
- _seen = 0
- start() None
Begin capturing; a restart discards what was captured before.
- Return type:
None
- stop() pathlib.Path | None
Stop capturing and encode what was captured.
- Returns:
The written file, or
Nonewhen nothing was captured — a recording that never saw a frame writes no file rather than an empty one.- Return type:
Optional[pathlib.Path]
- _encode() pathlib.Path
Write the captured frames, preferring MP4 and falling back to GIF.
- Return type:
pathlib.Path
- _encode_gif() pathlib.Path
Write an animated GIF, the fallback that needs only Pillow.
- Return type:
pathlib.Path
- describe() dict[str, Any]
Return what has been captured so far.
- Returns:
Whether the recorder is active, the frame count, and the target.
- Return type:
dict[str, Any]
- class plesty.lib.ui.qt.DockPanel(panel: plesty.lib.ui.panel.Panel, shell: Shell)
Bases:
PySide6.QtWidgets.QDockWidgetOne sub-window of the shell, wrapping a
Panel.Movable, floatable, and (unless the panel says otherwise) closable — the three features that let an operator rearrange the window at will.
Wrap panel into a sub-window of shell.
- Parameters:
panel (plesty.lib.ui.panel.Panel) – The panel to host.
shell (Shell) – The home window.
- panel
- title_bar
- toggle_floating() None
Pull the panel out of the home window, or drop it back in.
- Return type:
None
- refresh() None
Tick the panel and mirror its status into the title bar.
- Return type:
None
- class plesty.lib.ui.qt.PanelTitleBar(dock: DockPanel, title: str, closable: bool = True)
Bases:
PySide6.QtWidgets.QWidgetTitle bar of a sub-window: state dot, title, live status, controls.
Replaces Qt’s default dock title bar so a panel can show what it is doing without stealing plot area — the same window-chrome language as the plesty.net hero card. Mouse presses are left unhandled on purpose, so the dock widget beneath still starts a drag: this bar is the drag handle that pulls a panel out of the home window.
Build the bar for dock.
- Parameters:
dock (DockPanel) – The sub-window this bar belongs to.
title (str) – Text shown on the left.
closable (bool) – Show the close button.
- _dock
- dot
- label
- status
- _button(glyph: str, tip: str, handler: Any) PySide6.QtWidgets.QToolButton
Create one title-bar button.
- Parameters:
glyph (str)
tip (str)
handler (Any)
- Return type:
PySide6.QtWidgets.QToolButton
- set_status(text: str, tone: str = 'idle') None
Update the status line and the state dot.
- Parameters:
text (str) – Status text shown at the right of the bar.
tone (str) – State name understood by the theme (
"running","idle","error","warning").
- Return type:
None
- set_floating(floating: bool) None
Mark the bar as belonging to a floating window.
- Parameters:
floating (bool) – Whether the sub-window is currently floating.
- Return type:
None
- mouseDoubleClickEvent(event: Any) None
Toggle floating on a double click, as a title bar conventionally does.
- Parameters:
event (Any) – The Qt mouse event.
- Return type:
None
- class plesty.lib.ui.qt.Shell(title: str = 'PLESTY', *, theme: plesty.lib.ui.theme.Theme | None = None, interval_ms: int = 500, layout_file: str | pathlib.Path | None = None, remember_layout: bool = True, parent: PySide6.QtWidgets.QWidget | None = None)
Bases:
PySide6.QtWidgets.QMainWindowHome window hosting every panel as a rearrangeable sub-window.
Create the home window.
- Parameters:
title (str) – Window title, also the key of the saved layout.
theme (Optional[plesty.lib.ui.theme.Theme]) – Palette and stylesheet;
Noneloads the packaged PLESTY theme.interval_ms (int) – Refresh interval — how often every panel is ticked. 500 ms keeps a live spectrum fluent without loading the machine that is also running the measurement.
layout_file (Optional[str | pathlib.Path]) – Where to store the arrangement;
Noneuses~/.plesty/ui/<title>.json.remember_layout (bool) – Restore the stored arrangement on open and save it on close.
parent (Optional[PySide6.QtWidgets.QWidget]) – Parent widget.
- theme
- recorder: plesty.lib.ui.qt.recorder.Recorder | None = None
- last_recording: pathlib.Path | None = None
- remember_layout = True
- layout_file
- _timer
- _started = False
- _paused = False
- _pause_action: PySide6.QtGui.QAction | None = None
- add(panel: plesty.lib.ui.panel.Panel, *, area: str | None = None, tabify_with: str | None = None, split_with: str | None = None) DockPanel
Add a panel as a sub-window.
A panel joining an area that already holds one is split against the last panel there rather than dropped into the area as a whole. Only a split puts a real handle between two panels: docks that merely share an area — or worse, sit in different areas — cannot be resized against each other, which is how a panel ends up as an unadjustable strip.
- Parameters:
panel (plesty.lib.ui.panel.Panel) – The panel to host.
area (Optional[str]) – Docking area override; defaults to
panel.area.tabify_with (Optional[str]) – Name of an already-added panel to tab this one with, instead of giving it its own space.
split_with (Optional[str]) – Name of the panel to split against; defaults to the last one added to the same area.
- Returns:
The created sub-window.
- Raises:
ValueError – If the panel name is already taken, or the panel named in tabify_with / split_with is unknown.
- Return type:
- _named(name: str, action: str) DockPanel
Return an already-added sub-window by panel name.
- Raises:
ValueError – If no panel of that name was added.
- Parameters:
name (str)
action (str)
- Return type:
- _last_in_area(area: str) DockPanel | None
Return the last docked sub-window of area, if it has one.
- Parameters:
area (str)
- Return type:
Optional[DockPanel]
- apply_weights() None
Size the panels of each area in proportion to their panel weights.
Qt distributes leftover space evenly, which leaves a status strip as tall as a spectrum. Weights say what each panel is worth: a plot 3, a readout 1.
- Return type:
None
- remove(name: str) None
Remove a panel and release what it holds.
- Parameters:
name (str) – Name of the panel to remove; unknown names are ignored.
- Return type:
None
- start() None
Show the window, restore the layout, and start refreshing panels.
A stored arrangement wins over the panels’ weights — it is what the operator built last time.
- Return type:
None
- stop() None
Stop refreshing panels.
- Return type:
None
- pause() None
Hold every panel where it is until
resume().Only the refresh stops: the window still resizes, and the Panels and Layout menus still work, so a held moment can be rearranged and read.
- Return type:
None
- resume() None
Refresh the panels again after
pause().Unlike
start()it neither shows the window nor restores the layout — the arrangement made while paused is kept.- Return type:
None
- offer_pause() PySide6.QtGui.QAction
Add a Replay menu whose Pause/Resume, also on the space bar, toggles the refresh.
For a replayed run: a live one cannot be held, only stop being watched, so a live viewer does not offer it. Calling it twice returns the action already added.
- Returns:
The Pause/Resume action.
- Return type:
PySide6.QtGui.QAction
- set_status(text: str, state: str = 'idle') None
Set the shell-wide status line.
- Parameters:
text (str) – Message shown in the status bar.
state (str) – State name coloring the indicator (
"running","idle","error","warning").
- Return type:
None
- _tick() None
Refresh every panel; a failing panel never stops the others.
- Return type:
None
- start_recording(path: str | pathlib.Path, *, fps: int = 10, every: int = 1) plesty.lib.ui.qt.recorder.Recorder
Capture the window after every refresh until
stop_recording().Recording belongs to the window rather than to a view, so whatever is docked is captured — including a panel added or floated halfway through a run.
- Parameters:
path (str | pathlib.Path) – Destination file; the suffix is corrected to what the available encoder can write (MP4, else animated GIF).
fps (int) – Frame rate of the encoded video.
every (int) – Capture one frame per every refreshes.
- Returns:
The recorder, already active.
- Return type:
- stop_recording() pathlib.Path | None
Encode what was captured and stop recording.
- Returns:
The written file, or
Noneif nothing was being recorded.- Return type:
Optional[pathlib.Path]
- _toggle_recording() None
Menu handler: start a recording beside the layout, or stop one.
- Return type:
None
- save_layout(path: str | pathlib.Path | None = None) pathlib.Path
Store the current arrangement.
- Parameters:
path (Optional[str | pathlib.Path]) – Destination file;
Noneuseslayout_file.- Returns:
The path written.
- Return type:
pathlib.Path
- restore_layout(path: str | pathlib.Path | None = None) bool
Restore a stored arrangement, if one exists and still fits.
Panels that were not added this time are simply absent from the restored layout; new ones keep the placement they were added with.
- Parameters:
path (Optional[str | pathlib.Path]) – Layout file;
Noneuseslayout_file.- Returns:
Whether a layout was restored.
- Return type:
bool
- _all_docks() list[DockPanel | PySide6.QtWidgets.QDockWidget]
Return every sub-window this shell hosts, added by it or not.
panelsholds the onesadd()created, and for most shells that is all of them. A module is free to dock a widget of its own into the shell, though, and to the operator that is a panel like any other: “Hide all” that left it on screen would be answering a question nobody asked. The ones this shell added come first, so the order an operator sees is still the order they were added in.- Return type:
list[DockPanel | PySide6.QtWidgets.QDockWidget]
- reset_layout() None
Return every panel to the area it was added with, undocked from tabs.
- Return type:
None
- hide_all() None
Take every panel off the screen, leaving the window empty.
The same thing as unchecking each panel in the Panels menu, in one action: each entry there is the panel’s own visibility, so hiding a panel here unchecks it and the menu keeps saying what is on screen.
show_all()brings them all back.This is the bulk gesture an operator actually reaches for. Floating three plots into three windows to see one of them is a rearrangement; hiding the other two is the question they were asking.
- Return type:
None
- show_all() None
Put every panel back on screen, wherever it was.
Panels return to the arrangement they had, docked or floating; this says nothing about where they sit, only that they are visible. Use
reset_layout()to put them back in their declared areas.- Return type:
None
- _build_chrome() None
Create the menus and the status bar.
- Return type:
None
- closeEvent(event: Any) None
Save the arrangement and release the panels before closing.
- Parameters:
event (Any) – The Qt close event.
- Return type:
None