Haiku BOutlineListView: Tree Structure, Visible Rows, and Safe Removal
Model hierarchical Haiku lists with BOutlineListView while keeping parent-child ownership, collapsed rows, visible indices, and removal semantics consistent.
BOutlineListView extends Haiku’s BListView with a hierarchical item model. It is a useful fit for file trees, category browsers, project outlines, and other interfaces where rows can have children and groups can be expanded or collapsed. The control owns the visual list behavior; your application still owns the domain model and must keep the two structures synchronized.
The key API distinction is between the visible list and the full outline. A collapsed branch can contain items that are still part of the outline but are not currently displayed. The ordinary CountItems() and visible selection operations answer questions about rows the user can see. The FullList... methods and FullListCountItems() operate on the full hierarchy, including collapsed descendants. Mixing indices from these two views is a common source of deleting or updating the wrong row.
Build parent-child relationships deliberately
Use ordinary AddItem() for top-level items and AddUnder(item, superItem) to insert an item one level deeper immediately after its superitem in the full list. A parent is a BListItem; it is not a separate BView. List items are lightweight model/drawing objects, which is why a list can manage many rows without creating one child view per item.
BOutlineListView* outline = new BOutlineListView("projects",
B_SINGLE_SELECTION_LIST);
BStringItem* root = new BStringItem("Project Alpha");
BStringItem* source = new BStringItem("src");
BStringItem* tests = new BStringItem("tests");
if (!outline->AddItem(root)) {
delete root;
delete source;
delete tests;
return B_ERROR;
}
if (!outline->AddUnder(source, root)) {
delete source;
delete tests;
return B_ERROR;
}
if (!outline->AddUnder(tests, root)) {
delete tests;
return B_ERROR;
}
outline->Expand(root);
The example checks insertion results and releases items that were not adopted. Verify the ownership contract for the exact insertion/removal method you use: once successfully attached, an item belongs to the list. Do not separately delete an attached item. If insertion can fail after earlier items have been added, unwind only the unattached objects and leave the successfully adopted subtree for the list to manage.
The root’s expanded state controls whether its children are visible. A child can have descendants of its own, so nested expansion should be tested at each depth. Expand() and Collapse() update the displayed outline; they do not change the application’s underlying data model. If your model filters or lazily loads children, define how that state maps onto the list and avoid presenting a branch as complete while its children have not yet been fetched.
Keep full-list and visible indices separate
The full outline is a depth-first sequence, but the displayed rows omit descendants hidden under collapsed ancestors. FullListIndexOf() and FullListItemAt() refer to that full sequence. Visible list indices can refer to another row after expansion or collapse. Never store a bare integer index as an item’s long-lived identity; retain a model key or a carefully managed item pointer and resolve its current index just before an operation.
Selection APIs require the same care. A selection reported in the visible list is meaningful in the current expansion state. A full-list selection query uses full-list indexing. If an item is selected and its ancestor collapses, it may no longer have a visible row; update the selection policy deliberately rather than assuming the selected index still names the same object. Likewise, sorting, insertion, removal, and lazy loading can all shift indices.
For application state, use stable identifiers such as a document ID or canonical model key, not row position, label text, or pointer address serialized to disk. When a message arrives from a selected row, extract that row’s model identifier, re-resolve it against the current model, and verify that the operation is still allowed. This prevents delayed commands from acting on a different row after the tree changed.
Understand removal and ownership
The API documents that RemoveItem(BListItem*) removes the item and its subitems, which are deleted as part of the operation. The overload that removes by full-list index returns the removed item, but its subitems are still removed and deleted. These are not equivalent ownership paths. Read the current API reference for the exact overload before holding pointers or attempting to reuse a removed subtree.
If a node is deleted from the domain model, remove the corresponding outline item once and invalidate any selection, expansion, or cached pointer that referred to it. If a node is moved, decide whether the move can preserve the item object safely or whether the view should remove and recreate it; do not leave a child linked under two parents. When RemoveItem() deletes a subtree, do not recursively delete its child items again from application cleanup code.
MakeEmpty() removes the full outline. It is appropriate when replacing the entire projection, but not when retaining pointers into old rows. Clear the selection and any application-side map from item pointers to model identifiers before or during teardown. A stale pointer in a side table defeats the list’s otherwise clear ownership boundary.
Reconcile model changes without corrupting the tree
A robust update path serializes structural mutations on the looper that owns the control. Worker threads should prepare plain model data and post a message; the UI handler then applies a bounded batch of inserts, removals, and label updates. Do not call view methods from arbitrary workers or hold a shared model lock while drawing can query that same model.
For a small update, compute the desired parent and child relationships first, then apply changes in an order that preserves valid parents. Insert a parent before its children. Before removing a parent, decide whether its descendants also disappear from the domain model; the list API’s subtree behavior means the visual operation may remove more than one row. For large refreshes, compare stable identifiers and apply a diff instead of clearing and rebuilding the whole view, especially if expansion state or selection should survive.
If a branch is populated lazily, distinguish “not loaded yet” from “loaded and empty.” A temporary placeholder row should not be treated as a real domain item. When a response arrives, verify the parent still exists and that the request generation is current before replacing the placeholder. This avoids reintroducing children beneath a branch the user removed or refreshed while the load was in flight.
Keep drawing and navigation in the list-item model
BListItem is responsible for row-specific state and drawing; the list view provides the surrounding control behavior. Store only the small data needed to render and identify the row. Avoid creating a BView for every item or embedding rich view layout in an item when the list’s lightweight item model is sufficient. If each row genuinely requires a complex interactive view, use a container or specialized control designed for that interaction instead of forcing a tree list to act as a view hierarchy.
Use indentation, disclosure controls, keyboard navigation, and selection state consistently. Test long labels, translated strings, large fonts, rows with different heights, and branches with many descendants. A tree that looks correct only when fully expanded or only with mouse input is not a reliable navigation control.
Test collapsed descendants and teardown
Exercise an empty outline, one root, multiple nesting levels, collapsed and expanded parents, selection under collapse, insertion under an expanded parent, and removal of a parent with descendants. Confirm that CountItems() and FullListCountItems() differ as expected when branches are collapsed, and verify that every stored pointer is invalidated when the owning item is removed.
Also test failures during incremental insertion, refresh while a branch is loading, and teardown while queued model-update messages are pending. Log model IDs and structural operations rather than relying only on row indices. The tree remains correct when its visible projection, full hierarchy, and domain model have explicit boundaries, and when indices are treated as temporary coordinates rather than identities.
Related:
- Haiku BListView: Item Lifetime, Selection, and Stable Model Updates
- Haiku BScrollView: Viewport Geometry, Scrollbar Ranges, and Target Ownership
Sources: