Skip to content
oldmangan edited this page Oct 8, 2026 · 6 revisions

Nested, collapsible nodes with selection, a filter box and right-click menus.

from wapyt import TreeAction, TreeConfig, TreeItem

tree = self.add_tree("nav", TreeConfig(
    items=[TreeItem(id="prod", label="Production", data={"kind": "server"}, items=[
        TreeItem(id="prod:web", label="web-01", icon="mdi-server", data={"kind": "host"}),
    ])],
    filterable=True,
    context_actions=[
        TreeAction("connect", "Connect", "mdi-lan-connect", kinds=["host"]),
        TreeAction(separator=True),
        TreeAction("delete", "Delete", "mdi-delete", danger=True, requires=["deletable"]),
    ],
))
tree.on_activate(lambda p: connect(p["node"]))
tree.on_action(lambda p: handle(p["action"], p["id"]))

Nodes

TreeItem(id, label, icon, open_icon, badge, tooltip, items, data). A tooltip always shows on hover; without one, the label shows as a tooltip only when it is cut off. A node with items is a branch, otherwise a leaf; branches default to folder icons. data travels in every event payload.

  • Use deterministic ids. Expansion is keyed by id and survives set_items(), so a reload keeps what the user opened. Persist it across sessions with get_expanded() / set_expanded(ids).
  • on_activate (double-click) fires for leaves only. A lazily loaded branch needs at least one child to render as a branch.

Context menus

TreeAction(id, label, icon, scope, kinds, requires, danger, separator):

  • scope: "any", "branch" or "leaf".
  • kinds=[...]: only on nodes whose data["kind"] is listed. For trees whose branches are different things, a server and a database say.
  • requires=[...]: only on nodes whose data["flags"] holds every name. For per-node capabilities. A node without flags shows no requires entry.
  • Separators between hidden groups, or first or last, hide themselves.
  • Action ids may repeat (two delete entries for different kinds); each is reported as its own id.
  • Right-clicking a node selects it.

The menu is a ContextMenu: ↑/↓, Home/End and Enter work, Escape closes it, Shift+F10 or the Menu key opens it from the keyboard, and focus returns to where it was.

Keyboard

Added in #63. The tree is one Tab stop. ↑/↓ move between rows, Home and End jump to the ends, → opens a folder (or moves into an open one), ← closes it (or moves to the parent), Enter opens a leaf (on_activate) or toggles a folder, Space ticks a checkbox (or selects), and Shift+F10 or the Menu key opens the right-click menu. Screen readers hear it as a tree, with each row's level, position, and open, selected and checked state. TreeConfig(label=) names it; tree.focus(node_id=None) moves focus into it.

Checkboxes

Added in #63.

tree = Tree(TreeConfig(items=items, checkboxes=True), container=cell)
tree.on_check(lambda p: save_selection(p["checked_ids"]))
  • Click a box, or press Space on a row. Clicking a box doesn't select the row.
  • By default ticking a folder ticks everything inside it, and a folder shows ticked, unticked or mixed from what is inside. check_cascade=False makes every node independent.
  • TreeItem(checked=True) starts ticked; TreeItem(checkbox=False) has no box.
  • on_check → {id, checked, checked_ids}. get_checked(leaves_only=False) lists ticked nodes in tree order (fully ticked folders too, unless leaves_only). check(id, checked=True), uncheck(id), set_checked(ids) and is_checked(id) don't fire events. Ticks survive set_items for nodes that still exist.

Drag and drop

Added in #63.

tree = Tree(TreeConfig(items=items, draggable=True), container=cell)

async def moved(p):
    if not await bff.move_node(p["id"], p["to_parent"], p["to_index"]):
        tree.move(p["id"], p["from_parent"], p["from_index"])   # put it back
tree.on_move(lambda p: asyncio.ensure_future(moved(p)))
  • Drop on the top or bottom of a row to put the node before or after it; on the middle of a folder to put it inside. Just below an open folder means "first inside it". A closed folder opens if you hover over it, the list scrolls near its edges, and Escape cancels. A node can't go into itself.
  • Keyboard: Alt+↑/↓ move the focused node among its siblings, Alt+→ puts it inside the node above, Alt+← moves it out of its folder. Screen readers hear where it went.
  • TreeItem(draggable=False) pins a node; TreeItem(droppable=False) keeps other nodes out of it; TreeConfig(drop_into_leaves=True) lets a node be dropped onto a leaf, which then becomes a folder.
  • The tree moves the node, then on_move reports {id, node, from_parent, from_index, to_parent, to_index} (parents are None at the top level). move(id, parent, index) moves a node without an event, which is how to undo a refused move. get_items() returns the current nodes for saving.
  • Dragging is paused while the filter has text.

Events

Method Payload
on_select {id, node}; both are None when the selection clears
on_activate {id, node}, leaves only
on_action {action, id, node}
on_toggle {id, expanded}
on_filter {value}
on_check {id, checked, checked_ids}
on_move {id, node, from_parent, from_index, to_parent, to_index}

Methods

set_items, get_node, get_parent_id, select, get_selected, expand, collapse, toggle, expand_all, collapse_all, get_expanded, set_expanded, set_filter, set_empty_text, get_items, move, check, uncheck, set_checked, get_checked, is_checked, focus, destroy.

TreeConfig also takes selected, expand_all, filter_placeholder, empty_text, indent, label, checkboxes, check_cascade, draggable, drop_into_leaves and extra.

Clone this wiki locally