plesty.lib.ui.qt.shell ====================== .. py:module:: plesty.lib.ui.qt.shell .. autoapi-nested-parse:: The PLESTY shell: one home window hosting rearrangeable sub-windows. The shell is a home window with no fixed content of its own. Every :class:`~plesty.lib.ui.panel.Panel` becomes a sub-window the operator owns: drag it to another edge, drop it onto another panel to tab the two together, drag the handle between two panels to change how the space is split, drag it out of the window entirely to get a free-floating window on a second screen, and drag it back in when the bench gets crowded. Closed panels come back from the Panels menu; the arrangement is saved on exit and restored next time, so each rig keeps the layout its operator built. .. code-block:: python shell = Shell("Demo", interval_ms=500) shell.add(spectrum_panel, area="top") shell.add(map_panel, area="top") shell.add(power_panel, area="top") shell.start_recording("run.mp4") # optional: the run as a video shell.start() Everything visual comes from :class:`~plesty.lib.ui.theme.Theme` — the shell holds no colour of its own. Attributes ---------- .. autoapisummary:: plesty.lib.ui.qt.shell._AREAS plesty.lib.ui.qt.shell._ORIENTATIONS plesty.lib.ui.qt.shell.LAYOUT_VERSION Classes ------- .. autoapisummary:: plesty.lib.ui.qt.shell.PanelTitleBar plesty.lib.ui.qt.shell.DockPanel plesty.lib.ui.qt.shell.Shell Functions --------- .. autoapisummary:: plesty.lib.ui.qt.shell._slug plesty.lib.ui.qt.shell._repolish Module Contents --------------- .. py:data:: _AREAS .. py:data:: _ORIENTATIONS .. py:data:: LAYOUT_VERSION :value: 1 .. py:function:: _slug(text: str) -> str Return a filesystem-safe slug for a shell title. .. py:function:: _repolish(widget: PySide6.QtWidgets.QWidget) -> None Re-apply the stylesheet after a dynamic property changed. .. 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:: 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:: 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.