All docs
Contributing UI with @tool.view
One decorator, @tool.view, contributes UI. It always returns a View; location= (sidebar / overlay / editor) is only where the View lands. Nothing imports GTK.
Beyond nodes, a tool can contribute its own UI. There is one decorator — @tool.view — and it
always returns one class, a View. location= only chooses where that View
lands, and a View never imports GTK:
location="sidebar"— docks it as an input form in the shell (informally a panel);location="overlay"— floats it over the 3D viewer as a HUD;location="editor"— an editor tab that represents data (the default).
These are placements of the one View, not three types. See the UI overview
for the whole picture, and naming for how these words map to the View class.
Usage#
A sidebar view is just a View — a subclass with param() fields and @action methods. @tool.view
decorates a factory (ctx) -> View that returns an instance; the desktop docks it:
from lsdtools import Tool, View, action, param
tool = Tool("mine")
class MineControls(View):
count = param(int, default=5, min=1, label="Count")
@action(label="Run", style="suggested")
def run(self):
self.ctx.ui.notify(f"running with {self.count}")
@tool.view("mine.controls", title="Mine controls", location="sidebar")
def controls(ctx):
return MineControls()
# → a "Mine controls" view docks in the sidebar
An action handler reads the fields as typed attributes (self.count) — there is no values dict to
keep in sync. On mount the view's self.ctx is injected, so a handler can reach
self.ctx.ui.notify(...), self.ctx.engine, and the rest of the Context
without any wiring.
Sidebar (params + actions)#
@tool.view(view_id, title=None, *, location="sidebar", icon=None) decorates a factory that
returns a View. location places it ("sidebar", "editor", …). Because a panel is just a
View, the same object renders in the desktop, prints in the console, and is driven by tests through
view.invoke("run") — one dispatch entry for every front-end. See Params & actions for the
full View authoring guide.
Overlay (a HUD)#
@tool.view(view_id, title=None, *, location="overlay", dock="top", view_kind="viewer3d", icon=None)
is a HUD over a viewport. It returns a View too — typically an action-only one — docked at
dock (top/bottom/left/right) over the viewport named by view_kind:
class MineHud(View):
@action(label="Fit view", style="suggested")
def fit(self):
self.ctx.ui.notify("fit view")
@tool.view("mine.hud", location="overlay", dock="top", view_kind="viewer3d")
def hud(ctx):
return MineHud()
The desktop reads the dock / view_kind hints and mounts the view's actions as a toolbar over the
viewer.
Editor (a data view)#
@tool.view(view_id, title=None, *, location="editor", icon=None, menu=None) contributes a data
view: the decorated factory is (ctx, entity=None) -> View. A View with a
kind is a representation of data — a table, chart, or flowchart from lsdtools.views, or a
custom one:
from lsdtools import Tool, views
tool = Tool("mine")
@tool.view("mine.report", title="Grade report", location="editor")
def report_view(ctx, entity=None):
return views.chart(kind="histogram", title="Grades").watch(entity) # follows the entity
# → an editor tab titled "Grade report" that refreshes when the entity re-runs
The factory returns the View; the front-end resolves its shell — the desktop draws the GTK control,
a headless host uses to_text() / to_image(). Returning a raw GTK widget is a mount error: a
package's only path to GTK is a custom view's shell.
Receiving an entity's output — watch#
A data-view factory is (ctx, entity=None) -> View: the two-argument signature is what marks it a
per-entity view, and the desktop opens it for a matching entity (via opens_for=). The view gets
that entity's data through one verb — watch(entity) — and refreshes on
every re-run. You never hand-feed it; the framework wires the bus for you:
@tool.view("mine.grades", title="Grades", location="editor")
def grades(ctx, entity=None):
return views.table(title="Grades").watch(entity) # holds the entity's output; auto-refreshes
watch returns the view, so views.table(...).watch(entity) reads as one phrase. Any order works —
watching an entity that has already run pulls its output immediately. A deliver step that returns a
ViewerPayload is routed to the same watching views. Pass opens_for=predicate
to @tool.view (see the opens_for_type helper in lsdtools.advanced) to have the desktop open this
view on demand for a matching entity; otherwise it is mounted once. A View (no entity) that wants
live data can still self.ctx.engine.get("name") and watch it from an @action.
Custom views#
For a representation the built-in kinds can't express, subclass View with its
own kind, feed(data), and to_text() / to_image(). Its GTK drawing shell registers through the
one sanctioned seam — set_view_shell from lsdtools.views:
from lsdtools.views import set_view_shell
set_view_shell("sparkline", build_sparkline_shell) # build_sparkline_shell(view) -> a Gtk.Widget
The full walkthrough — the View subclass and its shell — is in
Custom views.
Related#
Learn — Commands & actions · UI overview · Naming: which is which
Examples — A dockable panel · A complete package
Frequently asked questions
What do the different location= values mean?
location= only chooses where the one View lands. "sidebar" docks it as an input form in the shell (informally a panel); "overlay" floats it over a viewport as a HUD (with dock="top"/"bottom"/etc. over the viewport named by view_kind); "editor" (the default) is an editor tab. Same View class every time — there is no separate panel or overlay type.
When does a view show data instead of controls?
A View with no kind renders as a form — param() fields plus @action buttons. A View with a kind (table/chart/flowchart/custom) represents data via to_text()/to_image(). It is one class either way; the kind is what turns controls into a data representation, independent of location=.