Class TreeConf<I,N>
- Type Parameters:
I- The identity type of the nodes, which selection paths are made of.N- The common node type of the tree, typically a sealed interface.
A JTree in SwingTree is bound to a single property holding a
deeply immutable, nested data structure. This class is how you tell the tree which
parts of that structure to zoom into:
public sealed interface FsNode extends HasId<UUID> { String name(); }
public record Dir( UUID id, String name, Tuple<FsNode> entries ) implements FsNode {}
public record Doc( UUID id, String name, String body ) implements FsNode {}
UI.tree(fileSystem, conf -> conf
.nodesOf(Dir.class, dir -> dir.children(Dir::entries).text(Dir::name))
.nodesOf(Doc.class, doc -> doc.text(Doc::name))
);
Each nodesOf(Class, Configurator) block covers one node type and reads like one
case of the switch you would otherwise write by hand, which is why this
API pairs so naturally with a sealed interface based sum type.
On identity. A tree of value objects has a problem a list of them does not:
JTree keys expansion and selection on TreePath,
which compares nodes with Object.equals(Object). Records compare by content, so
editing one leaf would invalidate every path in the tree at once. SwingTree therefore
identifies a node by its path of ids. Nodes implementing HasId supply that
id for free; for types you do not own, declare one with idOf(Function). Ids only
need to be unique among siblings, because the path disambiguates the rest.
A configuration describes node types, not the shape of the top level, so the very same
value binds a single rooted tree through UIFactoryMethods.tree(sprouts.Var, TreeConf) and a
forest of them through UIFactoryMethods.trees(sprouts.Var, TreeConf).
Instances of this class are immutable values, so every method returns a new instance instead of modifying the receiver.
-
Method Summary
Modifier and TypeMethodDescriptionDeclares what identifies a node, which is what lets expansion and selection survive an edit anywhere in the tree.leafWhenEmpty(boolean leafWhenEmpty) Decides whether a branch that currently has no children should be drawn as a leaf.Resolves a selection path back to the node it names, which is what a view bound to a selection needs in order to show anything about what is selected.Resolves a selection path of a forest back to the node it names, which is the same questionnodeAt(Object, Tuple)answers for a single rooted tree.sprouts.Tuple<N> nodesAlong(@Nullable N root, sprouts.Tuple<I> path) Resolves a selection path to every node along it, the root first and the named node last, which is what a breadcrumb trail is made of.sprouts.Tuple<N> nodesAlong(sprouts.Tuple<N> roots, sprouts.Tuple<I> path) Resolves a selection path of a forest to every node along it, the top level node first and the named node last.nodesOf(Class<B> type, Configurator<TreeNodeConf<N, B>> conf) Declares how nodes of the given type behave: whether they have children and where, how they are labelled, and what icon they carry.nodesOf(Configurator<TreeNodeConf<N, N>> conf) Declares how every node behaves, which is the right thing when the tree is homogeneous and one rule covers all of it.static <I,N extends sprouts.HasId<I>>
TreeConf<I, N> Declares the shape of a tree whose nodes carry their own identity throughHasId, which is the usual case.static <I,N> TreeConf <I, N> Declares the shape of a tree whose node types cannot implementHasId, by naming the identity type explicitly.toString()
-
Method Details
-
of
Declares the shape of a tree whose nodes carry their own identity throughHasId, which is the usual case. The result is an ordinary immutable value: bind it to a tree withUIFactoryMethods.tree(sprouts.Var, TreeConf), and ask it questions about paths withnodeAt(Object, Tuple)andnodesAlong(Object, Tuple).TreeConf<String, Packed> shape = TreeConf.of(Packed.class) .nodesOf(Box.class, it -> it.children(Box::contents).text(Box::label)) .nodesOf(Item.class, it -> it.text(Item::label));- Type Parameters:
I- The identity type of the nodes, taken from theHasIdbound.N- The common node type of the tree.- Parameters:
nodeType- The common supertype of every node in the tree.- Returns:
- A new, empty tree configuration.
-
of
Declares the shape of a tree whose node types cannot implementHasId, by naming the identity type explicitly. Their identity is then declared withidOf(Function).- Type Parameters:
I- The identity type of the nodes.N- The common node type of the tree.- Parameters:
nodeType- The common supertype of every node in the tree.idType- The type of the node identities, which selection paths are made of.- Returns:
- A new, empty tree configuration.
-
nodesOf
Declares how nodes of the given type behave: whether they have children and where, how they are labelled, and what icon they carry. Declaring a block for a type that already has one replaces the previous block.
A node whose type matches no block at all is treated as a leaf labelled by itsUI.tree(fileSystem, conf -> conf .nodesOf(Dir.class, dir -> dir .children(Dir::entries, Dir::withEntries) .text(Dir::name, Dir::withName) ) );Object.toString(), and SwingTree logs a warning naming the type, because that is almost always a forgotten case rather than an intent.Where several blocks match a node, the most specific one wins and is used on its own: a block is chosen, never merged with a more general one, in the same way one
caseof aswitchdoes not continue into another. A block which wants the label of a catch-all block above it therefore has to declare that label too.- Type Parameters:
B- The concrete node type.- Parameters:
type- The concrete node type this block applies to.conf- Configures the behaviour of nodes of that type.- Returns:
- An updated configuration.
-
nodesOf
Declares how every node behaves, which is the right thing when the tree is homogeneous and one rule covers all of it. It is exactlynodesOf(theNodeType, conf), so a more specific block declared for a subtype wins over it — and, because the winning block is used on its own, replaces it rather than adding to it. A subtype which wants what is declared here has to restate it.- Parameters:
conf- Configures the behaviour of all nodes.- Returns:
- An updated configuration.
-
idOf
Declares what identifies a node, which is what lets expansion and selection survive an edit anywhere in the tree. Node types implementingHasIdneed no such declaration; use this one for types you do not own:
Ids only have to be unique among the children of one parent..idOf( node -> node instanceof Department ? ((Department) node).id() : node )- Parameters:
id- Produces the identity of a node.- Returns:
- An updated configuration.
-
leafWhenEmpty
Decides whether a branch that currently has no children should be drawn as a leaf. SwingTree defaults tofalse, meaning the presence of achildren(..)rule alone makes a node a branch, so an empty folder still looks like a folder. Passtruefor the plainJTreebehaviour, where a node with zero children is a leaf.- Parameters:
leafWhenEmpty- True to draw childless branches as leaves.- Returns:
- An updated configuration.
-
nodeAt
Resolves a selection path back to the node it names, which is what a view bound to a selection needs in order to show anything about what is selected.A selection is a
Tupleof ids leading down from the root, because that is the only thing which identifies a position in a tree. The node living at that position is a question about the tree, and this is how you ask it.Declaring the shape as an ordinary value rather than inline is what makes that elegant, because the same value then serves twice: it builds the tree, and it answers the detail pane standing next to the tree.
Note that no listener appears anywhere above, and nothing reaches into theprivate final Var<FsNode> fileSystem = vm.zoomTo(Workspace::files, Workspace::withFiles); private final Var<Tuple<String>> selectedPath = vm.zoomTo(Workspace::opened, Workspace::withOpened); private final TreeConf<String, FsNode> shape = TreeConf.of(FsNode.class) .nodesOf(Dir.class, it -> it.children(Dir::entries).text(Dir::name)) .nodesOf(Doc.class, it -> it.text(Doc::name)); // "Which node is selected" is a function of the tree and the path, so it is derived: private final Val<FsNode> selectedNode = Viewable.of(fileSystem, selectedPath, (root, path) -> shape.nodeAt(root, path).orElse(null)); UI.panel("fill") .add("grow, w 35%", UI.scrollPane().add( UI.tree(fileSystem, shape) // the shape builds the tree... .withSelection(selectedPath) // ...which reports a path of ids in here... ) ) .add("grow, w 65%", UI.panel("fill").add(selectedNode, this::detailsOf) // ...which answers this pane );JTree. Clicking a row writes a path intoselectedPath, which makesselectedNodea different node, which swaps the view in the right hand panel — three pure functions of one property each.Use
nodeAt(Tuple, Tuple)instead where the tree was bound withUIFactoryMethods.trees(sprouts.Var, swingtree.api.Configurator), whose property holds aTupleof top level nodes rather than one root.The result is empty for the empty path (nothing is selected), for a path which no longer leads anywhere, which is what a path pointing into a since-deleted branch does, and for a root which is
null, which is what a property holding nothing is.- Parameters:
root- The root value of the tree, which is what the path is relative to, ornullwhere the property holding it is empty.path- The ids leading from the root down to the node, the root's own id first.- Returns:
- The node at that path, or an empty optional if the path names nothing.
-
nodeAt
Resolves a selection path of a forest back to the node it names, which is the same questionnodeAt(Object, Tuple)answers for a single rooted tree. A forest has no root, so the first id of the path names one of the top level nodes rather than a container above them, and this overload therefore takes the whole top level.It is the detail pane of
nodeAt(Object, Tuple)with one property widened, and nothing else about the view changes:
The one line worth pausing on is theprivate final Var<Tuple<FsNode>> projects = vm.zoomTo(Workspace::open, Workspace::withOpen); private final Var<Tuple<String>> selectedPath = vm.zoomTo(Workspace::opened, Workspace::withOpened); private final TreeConf<String, FsNode> shape = TreeConf.of(FsNode.class) .nodesOf(Dir.class, it -> it.children(Dir::entries).text(Dir::name)) .nodesOf(Doc.class, it -> it.text(Doc::name)); // The node type has to be named here, because the property no longer has it: private final Val<FsNode> selectedNode = Viewable.of(FsNode.class, projects, selectedPath, (roots, path) -> shape.nodeAt(roots, path).orElse(null)); UI.panel("fill") .add("grow, w 35%", UI.scrollPane().add( UI.trees(projects, shape) // several top level nodes, no root drawn .withSelection(selectedPath) // [ "myapp", "src", "App.java" ] ) ) .add("grow, w 65%", UI.panel("fill").add(selectedNode, this::detailsOf) );FsNode.classin the middle. The three argumentViewable.of(..)takes its result type from the first property, which here holds aTuple<FsNode>and not anFsNode, so the four argument form is the one a forest needs.- Parameters:
roots- The top level nodes of the forest, which is what the path is relative to.path- The ids leading from a top level node down to the node, its own id first.- Returns:
- The node at that path, or an empty optional if the path names nothing.
-
nodesAlong
Resolves a selection path to every node along it, the root first and the named node last, which is what a breadcrumb trail is made of. WherenodeAt(Object, Tuple)answers "what is selected", this answers "how did we get there", and a bar above the tree is the usual reason to ask:
The label reads correctly after a rename anywhere along that trail, because the trail is resolved from the ids the selection holds every time either property changes, rather than being a string somebody remembered to rebuild.private final Var<FsNode> fileSystem = vm.zoomTo(Workspace::files, Workspace::withFiles); private final Var<Tuple<String>> selectedPath = vm.zoomTo(Workspace::opened, Workspace::withOpened); private final TreeConf<String, FsNode> shape = TreeConf.of(FsNode.class) .nodesOf(Dir.class, it -> it.children(Dir::entries).text(Dir::name)) .nodesOf(Doc.class, it -> it.text(Doc::name)); private final Val<String> breadcrumb = Viewable.of(String.class, fileSystem, selectedPath, (root, path) -> shape.nodesAlong(root, path) .stream().map(FsNode::name) .collect(Collectors.joining(" > "))); UI.panel("fill, wrap 1") .add("growx", UI.label(breadcrumb)) // Workspace > src > App.java .add("grow", UI.scrollPane().add( UI.tree(fileSystem, shape).withSelection(selectedPath) ) );Use
nodesAlong(Tuple, Tuple)instead where the tree was bound withUIFactoryMethods.trees(sprouts.Var, swingtree.api.Configurator).The result is empty when the path names nothing, so a partial trail is never returned: either the whole path resolves or none of it does.
- Parameters:
root- The root value of the tree, which is what the path is relative to, ornullwhere the property holding it is empty.path- The ids leading from the root down to the node, the root's own id first.- Returns:
- Every node from the root down to the named one, or an empty tuple.
-
nodesAlong
Resolves a selection path of a forest to every node along it, the top level node first and the named node last. It isnodesAlong(Object, Tuple)in every respect but where the trail starts: a forest has no root, so its trails begin one level lower and the bar has one segment fewer.
Compare that comment with the one onprivate final Var<Tuple<FsNode>> projects = vm.zoomTo(Workspace::open, Workspace::withOpen); private final Var<Tuple<String>> selectedPath = vm.zoomTo(Workspace::opened, Workspace::withOpened); private final TreeConf<String, FsNode> shape = TreeConf.of(FsNode.class) .nodesOf(Dir.class, it -> it.children(Dir::entries).text(Dir::name)) .nodesOf(Doc.class, it -> it.text(Doc::name)); private final Val<String> breadcrumb = Viewable.of(String.class, projects, selectedPath, (roots, path) -> shape.nodesAlong(roots, path) .stream().map(FsNode::name) .collect(Collectors.joining(" > "))); UI.panel("fill, wrap 1") .add("growx", UI.label(breadcrumb)) // myapp > src > App.java .add("grow", UI.scrollPane().add( UI.trees(projects, shape).withSelection(selectedPath) ) );nodesAlong(Object, Tuple): the workspace itself is not in the trail, because in a forest it is not a node at all. Which is the whole point of binding one — there is no invented container to explain away, either on screen or inside the paths the application stores.- Parameters:
roots- The top level nodes of the forest, which is what the path is relative to.path- The ids leading from a top level node down to the node, its own id first.- Returns:
- Every node from the top level down to the named one, or an empty tuple.
-
toString
-