plesty.lib.ui.qt ================ .. py:module:: plesty.lib.ui.qt .. autoapi-nested-parse:: PySide6 backend of the PLESTY shell. Importing this package requires the ``gui`` extra (``uv add "plesty-lib[gui]"``): PySide6 and pyqtgraph. Everything above it — :class:`~plesty.lib.ui.panel.Panel`, :class:`~plesty.lib.ui.theme.Theme`, :class:`~plesty.lib.ui.monitor_panel.MonitorPanel` — imports without a GUI installed, so headless hosts and CI keep working. Submodules ---------- .. toctree:: :maxdepth: 1 /reference/plesty/lib/ui/qt/app/index /reference/plesty/lib/ui/qt/plot/index /reference/plesty/lib/ui/qt/recorder/index /reference/plesty/lib/ui/qt/shell/index Classes ------- .. autoapisummary:: plesty.lib.ui.qt.PlotRenderer plesty.lib.ui.qt.Recorder plesty.lib.ui.qt.DockPanel plesty.lib.ui.qt.PanelTitleBar plesty.lib.ui.qt.Shell Functions --------- .. autoapisummary:: plesty.lib.ui.qt.application plesty.lib.ui.qt.build_shell plesty.lib.ui.qt.run Package Contents ---------------- .. py:function:: application(name: str = 'PLESTY') -> PySide6.QtWidgets.QApplication Return the running Qt application, creating it when there is none. :param name: Application name, used by the window manager. :returns: The :class:`QApplication` instance. .. py:function:: build_shell(panels: Sequence[plesty.lib.ui.panel.Panel], *, title: str = 'PLESTY', theme: Optional[plesty.lib.ui.theme.Theme] = None, interval_ms: int = 500, layout_file: Optional[str | pathlib.Path] = None, remember_layout: bool = True) -> plesty.lib.ui.qt.shell.Shell Create a shell hosting *panels*, without starting the event loop. :param panels: Panels to add, in the order they should be docked. :param title: Window title and layout key. :param theme: Palette; ``None`` loads the packaged PLESTY theme. :param interval_ms: Panel refresh interval in milliseconds. :param layout_file: Where the arrangement is stored. :param remember_layout: Restore and save the arrangement. :returns: The shell, ready for :meth:`~plesty.lib.ui.qt.shell.Shell.start`. .. py:function:: run(panels: Sequence[plesty.lib.ui.panel.Panel], *, title: str = 'PLESTY', theme: Optional[plesty.lib.ui.theme.Theme] = None, interval_ms: int = 500, layout_file: Optional[str | pathlib.Path] = None, remember_layout: bool = True) -> int Open a shell on *panels* and run it until the window is closed. :param panels: Panels to add, in the order they should be docked. :param title: Window title and layout key. :param theme: Palette; ``None`` loads the packaged PLESTY theme. :param interval_ms: Panel refresh interval in milliseconds. :param layout_file: Where the arrangement is stored. :param remember_layout: Restore and save the arrangement. :returns: The Qt exit code. .. py:class:: PlotRenderer(theme: Optional[plesty.lib.ui.theme.Theme] = None, parent: Optional[PySide6.QtWidgets.QWidget] = None) Bases: :py:obj:`plesty.lib.monitor.base_monitor.Renderer` Draw target that renders a monitor's traces into a pyqtgraph widget. The renderer owns its widget (:attr:`widget`), so a panel builds one and hands the widget to the shell: .. code-block:: python renderer = PlotRenderer(theme) monitor.attach(renderer) # builds the plot items, replays the last data shell_panel_widget = renderer.widget Create the plot widget. :param theme: Palette to resolve trace colours and chrome against; ``None`` loads the packaged PLESTY theme. :param parent: Parent widget. .. py:attribute:: theme .. py:attribute:: widget .. py:attribute:: plot .. py:attribute:: caption .. py:attribute:: _items :type: dict[str, Any] .. py:attribute:: _images :type: dict[str, pyqtgraph.ImageItem] .. py:attribute:: _texts :type: dict[str, str] .. py:attribute:: _lut .. py:method:: setup(monitor: plesty.lib.monitor.base_monitor.Monitor) -> None Build one plot item per declared trace. :param monitor: The monitor that was attached. .. py:method:: 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. :param monitor: The monitor that produced the update. :param data: Trace key → payload. .. py:method:: clear(monitor: plesty.lib.monitor.base_monitor.Monitor) -> None Empty every plot item without rebuilding them. :param monitor: The monitor whose view is being reset. .. py:method:: _style_plot() -> None Apply the theme to the plot background, axes, and grid. .. py:method:: _build_item(spec: plesty.lib.monitor.base_monitor.TraceSpec, index: int) -> None Create the plot item declared by one trace spec. .. py:method:: _draw_curve(key: str, payload: plesty.lib.monitor.base_monitor.TraceData) -> None Update one line or scatter trace. .. py:method:: _draw_image(key: str, payload: plesty.lib.monitor.base_monitor.TraceData) -> None Update one image trace, scaling it onto its physical axes. .. py:method:: _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 line under the plot. .. py:class:: 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. .. code-block:: python 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*. :param widget: The window to capture — normally the shell itself. :param path: Destination file; the suffix is corrected to what the available encoder can actually write. :param fps: Frame rate of the encoded video. :param every: 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. .. py:attribute:: widget .. py:attribute:: path .. py:attribute:: fps :value: 10 .. py:attribute:: every :value: 1 .. py:attribute:: active :value: False .. py:attribute:: frames :type: list[numpy.ndarray] :value: [] .. py:attribute:: _shape :type: Optional[tuple[int, int]] :value: None .. py:attribute:: _seen :value: 0 .. py:method:: start() -> None Begin capturing; a restart discards what was captured before. .. py:method:: capture() -> None Capture one frame, honouring :attr:`every`; ignored when inactive. .. py:method:: stop() -> Optional[pathlib.Path] Stop capturing and encode what was captured. :returns: The written file, or ``None`` when nothing was captured — a recording that never saw a frame writes no file rather than an empty one. .. py:method:: _encode() -> pathlib.Path Write the captured frames, preferring MP4 and falling back to GIF. .. py:method:: _encode_gif() -> pathlib.Path Write an animated GIF, the fallback that needs only Pillow. .. py:method:: describe() -> dict[str, Any] Return what has been captured so far. :returns: Whether the recorder is active, the frame count, and the target. .. py:class:: DockPanel(panel: plesty.lib.ui.panel.Panel, shell: Shell) Bases: :py:obj:`PySide6.QtWidgets.QDockWidget` One sub-window of the shell, wrapping a :class:`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*. :param panel: The panel to host. :param shell: The home window. .. py:attribute:: panel .. py:attribute:: title_bar .. py:method:: toggle_floating() -> None Pull the panel out of the home window, or drop it back in. .. py:method:: refresh() -> None Tick the panel and mirror its status into the title bar. .. py:class:: PanelTitleBar(dock: DockPanel, title: str, closable: bool = True) Bases: :py:obj:`PySide6.QtWidgets.QWidget` Title 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*. :param dock: The sub-window this bar belongs to. :param title: Text shown on the left. :param closable: Show the close button. .. py:attribute:: _dock .. py:attribute:: dot .. py:attribute:: label .. py:attribute:: status .. py:method:: _button(glyph: str, tip: str, handler: Any) -> PySide6.QtWidgets.QToolButton Create one title-bar button. .. py:method:: set_status(text: str, tone: str = 'idle') -> None Update the status line and the state dot. :param text: Status text shown at the right of the bar. :param tone: State name understood by the theme (``"running"``, ``"idle"``, ``"error"``, ``"warning"``). .. py:method:: set_floating(floating: bool) -> None Mark the bar as belonging to a floating window. :param floating: Whether the sub-window is currently floating. .. py:method:: mouseDoubleClickEvent(event: Any) -> None Toggle floating on a double click, as a title bar conventionally does. :param event: The Qt mouse event. .. py:class:: Shell(title: str = 'PLESTY', *, theme: Optional[plesty.lib.ui.theme.Theme] = None, interval_ms: int = 500, layout_file: Optional[str | pathlib.Path] = None, remember_layout: bool = True, parent: Optional[PySide6.QtWidgets.QWidget] = None) Bases: :py:obj:`PySide6.QtWidgets.QMainWindow` Home window hosting every panel as a rearrangeable sub-window. Create the home window. :param title: Window title, also the key of the saved layout. :param theme: Palette and stylesheet; ``None`` loads the packaged PLESTY theme. :param interval_ms: 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. :param layout_file: Where to store the arrangement; ``None`` uses ``~/.plesty/ui/.json``. :param remember_layout: Restore the stored arrangement on open and save it on close. :param parent: Parent widget. .. py:attribute:: theme .. py:attribute:: panels :type: dict[str, DockPanel] .. py:attribute:: recorder :type: Optional[plesty.lib.ui.qt.recorder.Recorder] :value: None .. py:attribute:: last_recording :type: Optional[pathlib.Path] :value: None .. py:attribute:: remember_layout :value: True .. py:attribute:: layout_file .. py:attribute:: _timer .. py:method:: add(panel: plesty.lib.ui.panel.Panel, *, area: Optional[str] = None, tabify_with: Optional[str] = None, split_with: Optional[str] = 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. :param panel: The panel to host. :param area: Docking area override; defaults to ``panel.area``. :param tabify_with: Name of an already-added panel to tab this one with, instead of giving it its own space. :param split_with: 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. .. py:method:: _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. .. py:method:: _last_in_area(area: str) -> Optional[DockPanel] Return the last docked sub-window of *area*, if it has one. .. py:method:: 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. .. py:method:: remove(name: str) -> None Remove a panel and release what it holds. :param name: Name of the panel to remove; unknown names are ignored. .. py:method:: 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. .. py:method:: stop() -> None Stop refreshing panels. .. py:method:: set_status(text: str, state: str = 'idle') -> None Set the shell-wide status line. :param text: Message shown in the status bar. :param state: State name coloring the indicator (``"running"``, ``"idle"``, ``"error"``, ``"warning"``). .. py:method:: _tick() -> None Refresh every panel; a failing panel never stops the others. .. py:method:: 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 :meth:`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. :param path: Destination file; the suffix is corrected to what the available encoder can write (MP4, else animated GIF). :param fps: Frame rate of the encoded video. :param every: Capture one frame per *every* refreshes. :returns: The recorder, already active. .. py:method:: stop_recording() -> Optional[pathlib.Path] Encode what was captured and stop recording. :returns: The written file, or ``None`` if nothing was being recorded. .. py:method:: _toggle_recording() -> None Menu handler: start a recording beside the layout, or stop one. .. py:method:: save_layout(path: Optional[str | pathlib.Path] = None) -> pathlib.Path Store the current arrangement. :param path: Destination file; ``None`` uses :attr:`layout_file`. :returns: The path written. .. py:method:: restore_layout(path: Optional[str | pathlib.Path] = 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. :param path: Layout file; ``None`` uses :attr:`layout_file`. :returns: Whether a layout was restored. .. py:method:: reset_layout() -> None Return every panel to the area it was added with, undocked from tabs. .. py:method:: _build_chrome() -> None Create the menus and the status bar. .. py:method:: closeEvent(event: Any) -> None Save the arrangement and release the panels before closing. :param event: The Qt close event.