bootstack.Tree#

class bootstack.Tree(*, nodes=None, data_source=None, parent_field='parent_id', root_value=None, label_field='name', icon_field=None, node_builder=None, order=None, selection_mode='single', show_selection_controls=False, select_on_click=True, indent=16, striped=False, show_scrollbar=True, scrollbar_variant='thin', height=None, density='default', accent=None, accent_selection=True, parent=None, **kwargs)#

Bases: PublicWidgetBase

A hierarchical tree for navigation and selection.

Tree displays nested data as expandable rows. It is a hierarchy-first widget — reach for it when the structure is the point (file trees, outlines, settings navigation, grouped pickers). For flat records with columns, sorting, filtering, and paging, use DataTable instead.

Nodes are object handles: add() returns a TreeNode that you hold and pass back to expand(), select(), remove(), and so on — there are no string ids. Each node carries a label and an optional icon (with optional open/closed variants), plus an open-ended data bag for your own attributes.

Parameters:
  • nodes (list[str | dict] | None) – Declarative initial tree — a list of node specs, where each spec is a label string or a dict like {"label": "src", "icon": "folder", "children": [...]}. Anything not a recognized display key becomes that node’s data. Mutually exclusive with data_source.

  • data_source (DataSourceProtocol | None) – A flat data source to project as a hierarchy. Each record carries a parent_field pointing at its parent’s id (an adjacency list); the tree loads one branch at a time, querying a node’s children only when it is expanded — so a huge hierarchy shows instantly. Any data-source-protocol source works. Mutually exclusive with nodes.

  • parent_field (str) – With data_source, the record field that holds each row’s parent id. Defaults to 'parent_id'.

  • root_value (Any) – With data_source, the parent_field value that marks a root node. Defaults to None (a NULL/absent parent).

  • label_field (str) – With data_source, the record field used as the node label. Defaults to 'name'. Ignored when node_builder is given.

  • icon_field (str | None) – With data_source, an optional record field whose value is used as the node icon.

  • node_builder (Callable[[dict], dict] | None) – With data_source, an optional callable taking a record and returning a node spec dict ({'label': ..., 'icon': ...}) for full control over how a record renders. Overrides label_field/icon_field.

  • order (str | Column | SortKey | Sequence[str | Column | SortKey] | None) – With data_source, the ordering applied to each sibling group — a sort key or sequence of keys ('name', '-created', a col, or a SortKey). Defaults to the source’s natural order.

  • selection_mode (SelectionMode) – Node selection behavior — 'single' keeps one highlighted node, 'multi' is click-to-toggle a set, 'none' does no highlight selection (navigation/display only). Default 'single'.

  • show_selection_controls (bool) – If True, show a per-node selection control (a checkbox in multi mode, a radio in single mode) as the visible affordance for selection — mirroring ListView and DataTable. A selected row shows both the control and the highlight wash.

  • select_on_click (bool) – If True (default), clicking a row selects it. Set False with show_selection_controls=True so that only the control selects and a row click just focuses (e.g. to drive on_activate for opening, VS Code style).

  • indent (int) – Horizontal indent per depth level, in pixels. Defaults to 16.

  • striped (bool) – If True, alternate the row background color.

  • show_scrollbar (bool) – If True (default), show the vertical scrollbar. Mousewheel scrolling works regardless.

  • scrollbar_variant (ScrollbarVariant) – Scrollbar style — 'thin' (default) for a slim bar, or 'default' for the standard rounded bar.

  • height (int | None) – Fixed height in pixels. When set, the tree maintains this height regardless of its content (so it can scroll without the parent layout providing a vertical constraint).

  • density (WidgetDensity) – Row height. Default 'default'.

  • accent (AccentToken | str | None) – Color intent token for the selection highlight. Defaults to the theme’s default color.

  • parent (Any) – Override the context-stack parent.

  • **kwargs (Any) – Layout placement options applied by the parent container — fill, expand, anchor, margin, row, column, sticky. See Layout & Spacing.

property data_source: DataSourceProtocol | None#

The backing data source, or None for a declarative tree.

property is_attached: bool#

Whether the widget is currently placed in its layout.

True while the widget occupies space in its parent; False after detach (or before it has ever been placed). A detached widget keeps its state and can be returned to the layout with attach.

property roots: list[TreeNode]#

The top-level nodes, in order.

Useful for reaching handles after declarative construction (Tree(nodes=...)), which otherwise returns no TreeNode handles.

property schedule: Schedule#

Scheduler tied to this widget’s lifetime.

All jobs are automatically cancelled when the widget is destroyed. First access creates the Schedule instance; subsequent accesses return the same instance.

Usage:

self.schedule.delay(500, callback)
self.schedule.every(1000, tick)
job = self.schedule.idle(refresh)
job.cancel()
property selection: TreeNode | list[TreeNode] | None#

The selected node(s) — node handles, in tree order.

In 'single' mode, the selected TreeNode (or None when nothing is selected). In 'multi' mode, a list of nodes (empty when nothing is selected). Each node’s data bag is at node.data. Read-only.

add(label='', *, parent=None, icon=None, open_icon=None, closed_icon=None, expanded=False, children=None, loader=None, data=None, **extra)#

Add a node and return its TreeNode handle.

Parameters:
  • label (str) – The node’s display label.

  • parent (TreeNode | None) – Parent node, or None for a root node.

  • icon (str | None) – Bootstrap icon name shown before the label.

  • open_icon (str | None) – Icon used when the node is expanded (overrides icon).

  • closed_icon (str | None) – Icon used when the node is collapsed (overrides icon).

  • expanded (bool) – Whether the node starts expanded.

  • children (list | None) – Optional child specs (same format as nodes=).

  • loader (Callable[[TreeNode], Any] | None) – Callable invoked on first expand to fetch children lazily. Receives the node; returns an iterable of child specs.

  • data (dict | None) – Initial data bag for the node.

  • **extra (Any) – Extra keywords folded into the node’s data bag.

attach(**kwargs)#

Return a detached widget to its layout, optionally moving it.

With no arguments, restores the widget to exactly where detach took it from. Any layout kwargs accepted by the original placement (e.g. fill, expand, anchor, sticky, margin) override the stored options. For stacked widgets, index= sets the position among the currently attached siblings (or pass an explicit before=/after= sibling); without one, the snapshotted position is used.

Calling attach on a widget that is already attached moves it (the kwargs are re-applied). Fires on_attach.

Parameters:

**kwargs (Any) – Layout placement options to override for this placement.

Raises:

ParentResolutionError – If the widget was never placed in a layout.

clear()#

Remove all nodes.

clear_selection()#

Clear the selection.

collapse(node)#

Collapse a node to hide its children.

collapse_all()#

Collapse every node.

deselect(node)#

Remove a node from the selection.

destroy()#

Detach the context menu and destroy the widget (cancels pending relayout/type-ahead callbacks on the internal view).

detach()#

Remove the widget from its layout without destroying it.

The widget stops occupying space but keeps its state, children, and event bindings, ready to be returned with attach. The current position is snapshotted so a plain attach() restores it exactly — for stacked siblings this is the index among the currently attached siblings, so detaching other siblings first shifts that index.

Calling detach on a widget that is already detached, or one that was never placed in a layout, does nothing. Fires on_detach.

emit(event, *, data=None)#

Fire a named event on this widget, as if it produced the event itself.

This is how a composite widget surfaces high-level activity to its listeners, and the generic counterpart to the on_*() shorthands for firing events that have no dedicated method.

Parameters:
  • event (str) – The event name, unprefixed — the same name you pass to on() or an on_<event>() shorthand (e.g. 'change', 'select').

  • data (Any) – The payload delivered to handlers. For a data-carrying event, pass the matching payload dataclass from bootstack.events — the same object an on_<event>() handler receives. Leave as None for native events (click, hover, focus, …), which carry no payload.

Example

widget.emit("change", data=bs.events.ChangeEvent(value=new_value))
expand(node)#

Expand a node to reveal its children.

expand_all()#

Expand every node (loading lazy children as needed).

find(matcher)#

Return the first node matching matcher, or None.

matcher is either a predicate (TreeNode) -> bool or a col(...) condition (see Data Sources):

  • A predicate is evaluated over the materialized nodes. For a data-source-backed tree that means the currently loaded branches only — a Python callable cannot be pushed down to the source.

  • A condition is matched against each node’s data. On a declarative tree this scans every node; on a data-source-backed tree it is pushed down to the source, so it reaches nodes in unexpanded branches, and the path to the match is loaded (without expanding it) so a real TreeNode handle is returned. Call reveal() on the result to scroll it into view.

Parameters:

matcher (Callable[[TreeNode], bool] | Condition) – A (TreeNode) -> bool predicate, or a col(...) condition.

find_all(matcher)#

Return every node matching matcher.

Accepts the same predicate-or-condition matcher as find(), with the same reach (a predicate sees only materialized nodes; a condition is pushed down on a data-source-backed tree). Predicate and declarative matches come back in tree order; pushed-down condition matches come back in the source’s order.

Parameters:

matcher (Callable[[TreeNode], bool] | Condition) – A (TreeNode) -> bool predicate, or a col(...) condition.

get_clipboard()#

Return the current text contents of the system clipboard.

Returns:

The clipboard text, or an empty string when the clipboard is empty or holds non-text data.

Return type:

str

insert(index, label='', *, parent=None, **kwargs)#

Add a node at a specific position among its siblings.

Parameters:
  • index (int) – Zero-based position among siblings.

  • label (str) – The node’s display label.

  • parent (TreeNode | None) – Parent node, or None for a root node.

  • **kwargs (Any) – Same as add().

move(node, parent=None, index='end')#

Move a node to a new parent and/or position.

Parameters:
  • node (TreeNode) – The node to move.

  • parent (TreeNode | None) – New parent, or None for the root level.

  • index (int | str) – Position among the new siblings — 'end' or an integer.

on(event, handler=None)#

Bind handler to event, or return a composable Stream.

With a handler — binds immediately and returns a Subscription:

sub = widget.on("change", handler)
sub.cancel()

Without a handler — returns a Stream for operator chaining. The Tk binding is created lazily when .listen() is called:

sub = widget.on("change").debounce(300).listen(handler)
sub.cancel()
Parameters:
  • event (str) – Event name (e.g. "change", "click").

  • handler (Callable[[Any], Any] | None) – Optional callback. If omitted, a Stream is returned.

Returns:

Subscription when a handler is provided; Stream otherwise.

Return type:

Stream | Subscription

on_activate() Stream#
on_activate(handler: Callable[[TreeNode], Any]) Subscription

Fired when a node is activated (double-click or Enter).

Parameters:

handler (Callable[[TreeNode], Any] | None) – Called with the activated TreeNode. Omit to get a composable Stream instead.

Returns:

A cancellable Subscription when a handler is given, otherwise a Stream.

Return type:

Stream | Subscription

on_attach(handler=None)#

Register a callback fired when the widget enters the layout.

Fires each time the widget becomes visible in its parent — on initial placement and on every attach. Pair it with on_detach to keep per-visibility resources (timers, observers) tied to the widget’s presence on screen. The handler receives a curated Event.

Parameters:

handler (Callable[[Event], Any] | None) – Called when the widget is attached. Omit to get a composable Stream.

Returns:

A cancellable Subscription when a handler is given, otherwise a Stream.

Return type:

Stream | Subscription

on_collapse() Stream#
on_collapse(handler: Callable[[TreeNode], Any]) Subscription

Fired when a node is collapsed.

Parameters:

handler (Callable[[TreeNode], Any] | None) – Called with the collapsed TreeNode. Omit to get a composable Stream instead.

Returns:

A cancellable Subscription when a handler is given, otherwise a Stream.

Return type:

Stream | Subscription

on_destroy(handler=None)#

Register a callback fired when the widget is destroyed.

Fires once, as the widget is torn down — the place to release resources the widget owns that aren’t cleaned up automatically (file handles, observers, external subscriptions). The handler receives a curated Event.

Parameters:

handler (Callable[[Event], Any] | None) – Called as the widget is destroyed. Omit to get a composable Stream.

Returns:

A cancellable Subscription when a handler is given, otherwise a Stream.

Return type:

Stream | Subscription

on_detach(handler=None)#

Register a callback fired when the widget leaves the layout.

Fires each time the widget stops occupying space in its parent — on detach and when an ancestor hides it. Pair it with on_attach to release per-visibility resources. The handler receives a curated Event.

Parameters:

handler (Callable[[Event], Any] | None) – Called when the widget is detached. Omit to get a composable Stream.

Returns:

A cancellable Subscription when a handler is given, otherwise a Stream.

Return type:

Stream | Subscription

on_expand() Stream#
on_expand(handler: Callable[[TreeNode], Any]) Subscription

Fired when a node is expanded.

Parameters:

handler (Callable[[TreeNode], Any] | None) – Called with the expanded TreeNode. Omit to get a composable Stream instead.

Returns:

A cancellable Subscription when a handler is given, otherwise a Stream.

Return type:

Stream | Subscription

on_right_click() Stream#
on_right_click(handler: Callable[[dict[str, Any]], Any]) Subscription

Fired on a right-click.

Parameters:

handler (Callable[[dict[str, Any]], Any] | None) – Called with a dict carrying node, x_root, and y_root — enough to position a context menu. Omit to get a composable Stream instead.

Returns:

A cancellable Subscription when a handler is given, otherwise a Stream.

Return type:

Stream | Subscription

on_select() Stream#
on_select(handler: Callable[[TreeSelectionEvent], Any]) Subscription

Fired when the set of selected nodes changes.

Parameters:

handler (Callable[[TreeSelectionEvent], Any] | None) – Called with a TreeSelectionEvent carrying nodes (the full selection, in tree order). Omit to get a composable Stream instead.

Returns:

A cancellable Subscription when a handler is given, otherwise a Stream.

Return type:

Stream | Subscription

refresh()#

Reload a data-source-backed tree from its source.

Re-queries the roots and discards loaded branches, so the tree reflects records that were inserted, updated, or deleted in the source. Selection and expansion state are reset. A no-op for a declarative tree.

reload_children(node)#

Refresh a lazy node’s children — drop them and re-fetch via its loader. If node is expanded the children reload immediately; otherwise they reload on the next expand. A no-op for non-lazy nodes.

Parameters:

node (TreeNode) – The lazy node (created with loader=) to refresh.

remove(node)#

Remove a node (and its descendants).

Parameters:

node (TreeNode) – The node to remove.

reveal(node)#

Expand ancestors and scroll so node is visible.

select(node)#

Select a node (replaces in single mode, adds in multi).

select_all()#

Select every node. Only effective when selection_mode='multi'.

set_clipboard(text)#

Replace the system clipboard contents with text.

Parameters:

text (str) – The text to place on the clipboard.

set_context_menu(builder, *, min_width=150, density='default')#

Attach a per-node context menu.

On right-click, builder(node, menu) is called with the right-clicked TreeNode and a fresh (emptied) ContextMenu; populate it with menu.add_item(...) / add_check_item(...) / add_separator() etc. (capture node in your callbacks). The menu is then shown at the cursor. Because it is rebuilt on every right-click, the items can depend on the node. If the builder adds no items, nothing is shown. Pass None to remove the menu.

Example:

def build(node, menu):
    menu.add_item("Rename", on_click=lambda: rename(node))
    menu.add_item("Delete", icon="trash", on_click=lambda: tree.remove(node))
tree.set_context_menu(build)
Parameters:
  • builder (Callable[[TreeNode, Any], Any] | None) – Callable taking (node, menu) and populating menu, or None to detach.

  • min_width (int) – Minimum menu width in pixels.

  • density (WidgetDensity) – Menu item density — 'default' or 'compact'.

toggle(node)#

Expand the node if collapsed, or collapse it if expanded.

walk()#

Yield every node depth-first, including collapsed ones.