Files
eco/docs/widget_containers.md
2026-08-28 14:25:39 +02:00

137 lines
6.7 KiB
Markdown

# Widget containers ("super widgets")
`eco.widgets.containers` lets you compose a custom panel out of eco's
existing widget pieces — stack them vertically or horizontally, align each
one left/center/right, and mix any of the four widget "flavours" already
used throughout eco. It works identically whether the session is the Qt
desktop workbench or a notebook (JupyterLab/Voila) — see
{doc}`widget_views` for background on the per-item row these containers are
built from.
```python
from eco.widgets.containers import (
stack, aligned, assembly_widget, adjustable_control, detector_indicator, viewer,
)
panel = stack(
assembly_widget(some_assembly),
adjustable_control(some_assembly.motor),
detector_indicator(some_assembly.gauge),
aligned(viewer(some_camera), align="center"),
direction="vertical", align="left",
)
```
## The `_widget_*` convention
Every widget-building method an eco object offers — beyond the generic
`.widget()` entry point itself — is named `_widget_<something>`, so the
full set of widgets an object can produce is discoverable just by grepping
(or tab-completing) that prefix. Four rules tie this together:
1. **`.widget()` is the entry point.** It returns *the* widget for this
object, and any object may override `.widget()` itself directly if it
wants full control over that.
2. **Everything else is `_widget_<something>`.** `Assembly`'s own generic
property grid lives at `_widget_assembly()`; `.widget()`'s base
implementation is a thin dispatcher that returns `self._widget_assembly()`
unless the class sets `_default_widget = "_widget_<something>"` to point
at a different one (e.g. a camera's live-stream view). Either way of
customizing — overriding `.widget()`, or pointing `_default_widget` at
an alternate `_widget_*` method — is valid; `_default_widget` is just
the lower-effort option when you don't need to touch dispatch logic
itself.
3. **SVG panels follow the same pattern, for `.show()`.** Any assembly
with a dynamic SVG panel (see {doc}`widget_views`'s background on
`Assembly.show()`) implements it as `_widget_svg_panel(self, live=False,
**kwargs)` — every SVG panel, now and in the future, uses this exact
name, the same way `_widget_assembly` is Assembly's fixed name.
`show()` picks it up automatically if it exists; `show()` itself can
also be overridden outright by a subclass that needs to (e.g.
`PrepumpSystem.show()` just changes a default), same escape hatch as
`.widget()`.
4. **A `_widget_viewer` (or any other name) is just an ordinary per-device
method** — nothing in `Assembly` or `eco.widgets.containers` treats
"viewer" as a generic, defaulted concept. It only exists, and only
becomes the default, where a specific device class defines it and
points `_default_widget` at it (today: `AxisPTZStreamQt`/`CameraBasler`/
`CameraPCO`'s live-stream views). `containers.viewer()` below is a thin
pass-through to whatever `.widget()` resolves to — it doesn't assume or
require a `_widget_viewer` method to exist.
## The four widget flavours
Each of these returns a zero-argument *builder* — nothing is built until
`stack()` actually calls it, in whichever backend the stack itself resolves
to (Qt vs. notebook, decided once, at the top). A builder is never told
which backend it's building for; it figures that out the same way
`Assembly.widget()` itself does.
- {py:func}`~eco.widgets.containers.assembly_widget` — the plain
property-grid for an `Assembly` (see {doc}`widget_views`'s "Default
per-item row"): `item._widget_assembly()` if present, else
`item.widget(normal=True)` as a fallback for anything that hasn't got
around to exposing `_widget_assembly` directly. Bypasses any
`_default_widget` override either way.
- {py:func}`~eco.widgets.containers.adjustable_control` — the
tweak/enum/stop-reset control row for one `Adjustable` (or a read-only
row if given a plain `Detector` — see `detector_indicator` below),
self-polling in its own background thread. This is the exact same
per-item control logic the normal assembly grid uses
({py:class}`~eco.widgets.display_qt.DisplayQt`/
`eco.widgets.display_widget.make_assembly_widget`) — extracted into a
shared function both the grid and this builder call, not a second
implementation.
- {py:func}`~eco.widgets.containers.detector_indicator` — a read-only live
value display for one `Detector`. Literally the same builder as
`adjustable_control` (which already renders read-only for a
non-Adjustable `Detector`); a separate name purely so a call site can say
what it means. Plain label style on both backends, deliberately — see
{doc}`widget_views`'s "Indicator / gadget widgets" section for the
separate, richer Qt-only LED/gauge/dial dashboard system, which this
does not replace.
- {py:func}`~eco.widgets.containers.viewer` — any custom, purpose-built
widget as-is: `obj.widget(**kwargs)` if `obj` has one (a camera stream
viewer, an SVG panel, ...), else `obj` itself if it's already a built
widget. Nothing defaults here (see rule 4 above) — exists purely for
naming symmetry with the other three inside a `stack()` call.
A `stack()` child can also be an already-built widget instead of a
builder — useful if you built one some other way and just want to place
it.
## `stack()` and `aligned()`
```{py:function} eco.widgets.containers.stack(*children, direction="vertical", align="start")
```
`direction`: `"vertical"` (the default, a column) or `"horizontal"` (a
row). `align`: the CROSS-axis position of each child — for a vertical
stack, each child's horizontal position (`"left"`/`"center"`/`"right"`);
for a horizontal stack, its vertical position
(`"top"`/`"center"`/`"bottom"`). `"start"`/`"end"` work as backend-neutral
synonyms for left/top and right/bottom respectively.
Wrap an individual child in `aligned(child, align)` to override the
stack's own default for just that one child:
```python
stack(a, aligned(b, "center"), c, align="left") # a and c stay left, b is centered
```
**Return shape** follows eco's existing per-backend convention (nothing
new here — the same shapes `Assembly.widget()` itself already returns):
- **Qt**: an object with `.window` (a `QWidget` wrapping a
`QVBoxLayout`/`QHBoxLayout`) and `.stop()` (stops every child that has
one — same idea as `EcoDesktopApp._dock_widget_object`'s dock teardown).
Dock it into the desktop workbench, or call `.window.show()`/`.stop()`
directly.
- **Notebook**: the built `ipywidgets.VBox`/`HBox` directly — no wrapper
needed, since `eco.widgets.widget_tray.teardown_widget` already closes a
widget's whole subtree and calls `.stop()` on it and every descendant
that has one.
A stack nests: `stack(...)`'s own return value is itself a valid child of
another `stack(...)` call, on either backend.