All docs
Docs/ Building Tools/ Contributing UI with @tool.view
Building Tools

Contributing UI with @tool.view

Use @tool.view for editor tabs and sidebars, and declared viewer extensions for HUDs and overlays.

TL;DRA @tool.view factory returns a View. location selects an editor tab or sidebar; editor instance policy, entity acceptance and binding cardinality are explicit. Viewer HUDs and overlays use ViewerExtensionSpec.surfaces through tool.viewer_extension. The old overlay placement and opens_for helper are removed.

A tool can contribute a sidebar control form or an editor data view with @tool.view. Its factory returns a View: a class with param() fields, @action methods and optional data rendering.

  • location="sidebar" docks one surface in the shell.
  • location="editor" declares an editor tab and is the default.
  • Content inside a viewer uses tool.viewer_extension(...) with ViewerExtensionSpec.surfaces.

The View interaction model is independent of GTK. A native drawing shell can use GTK behind the supported shell seam; ordinary form and data-view authors do not need to import it.

Usage#

A sidebar is a View subclass with fields and actions. The factory receives its host context:

Python
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()

The mounted factory injects self.ctx. An action reads typed fields directly and can use self.ctx.ui.notify(...) for notifications. Tests and other front ends invoke the same action through view.invoke("run").

A sidebar @tool.view is one docked surface. It uses the single-instance policy and does not accept editor entity-binding options. For additional native sidebar layout or delivery contracts, use the separately declared @tool.sidebar_view seam.

At mount time the sidebar becomes a SidebarViewContribution. This and the editor's MainViewType are declared exports of lsdtools.extend; the old ViewContribution type is removed. See Params & actions for ordinary View controls.

Overlay (a HUD)#

The previous @tool.view(location="overlay", dock=..., view_kind=...) API is removed. Tool.view rejects that location.

Declare viewer content with tool.viewer_extension(spec), where spec is a ViewerExtensionSpec containing ViewerSurfaceSpec entries. These types, ViewerSurfaceKind and package-qualified OwnedContributionId values come from lsdtools.extend. A declaration identifies its target viewer_kinds, surface kind, region and build factory. For example, a toolbar is a ViewerSurfaceKind.TOOLBAR surface in an available viewer region such as viewer.tools.

Use the viewer extension's declared placement and lifecycle rather than mounting an extra editor View over the canvas. Its package ownership is validated when the Tool is mounted; declaring a factory does not run it during API inventory. See the extension reference for the complete surface contract.

Editor (a data view)#

An editor factory may receive (ctx, entity=None) and returns a View, such as one from lsdtools.views. Declare how the tab accepts and binds entities explicitly:

Python
from lsdtools import Entity, Tool, views

tool = Tool("mine")

@tool.view(
    "mine.report",
    title="Grade report",
    location="editor",
    instance_policy="per_entity",
    binding_cardinality="one",
    accepts_entity=lambda entity: isinstance(entity, Entity),
)
def report_view(ctx, entity=None):
    return views.table(title="Grades").watch(entity)

This example accepts Entity objects; use a more selective predicate when a view requires one domain type or output contract. A second factory argument alone does not imply a per-entity view.

Option Meaning
instance_policy single, reusable or per_entity declares instance reuse.
accepts_entity Predicate deciding whether an entity is accepted by this view kind.
binding_cardinality none, one or many declares entity-binding capacity.
default_open Explicit activation-time opening signal; default false.

Editor declarations use the same MainViewType contract as native main views. The host assigns each live instance its exact ctx.view_id. A @tool.view factory must return a View; returning an arbitrary GTK widget is a mount error.

Receiving an entity's output — watch#

watch(entity) connects the View's data to an entity and refreshes it after runs. It returns the View, so views.table(...).watch(entity) can be the factory's result. Watching an already-run entity pulls its output immediately.

Instance creation, entity acceptance and data observation are separate choices. The old opens_for argument and opens_for_type helper are removed; migrate matching to accepts_entity and declare the instance/binding policy. default_open=True is the explicit way to request activation-time opening of an editor view.

Custom views#

For a representation the built-in kinds cannot express, subclass View with its own kind, feed(data) and to_text() / to_image(). Register the native drawing shell through lsdtools.views.set_view_shell:

Python
from lsdtools.views import set_view_shell

set_view_shell("sparkline", build_sparkline_shell)  # build_sparkline_shell(view) -> Gtk.Widget

The complete View subclass and native shell belong together; see Custom views. For native main-view factories, use @tool.main_view and its explicit host/view-id contract.

LearnCommands & actions · UI overview · Naming: which is which

APITool · Context · Extension facade

ExamplesA dockable panel · A complete package

Frequently asked questions

What location values does @tool.view accept?

editor and sidebar only. An overlay belongs to a declared viewer extension. Passing location=overlay raises an error.

Does an entity argument make a view per-entity automatically?

No. Declare instance_policy, accepts_entity and binding_cardinality explicitly. default_open controls activation-time opening for editor views.

By LSD Team · Last updated Sep 09, 2026 Ask a question View as Markdown
Type to search every doc, guide, and tutorial.
↑↓ navigate openesc close