Package swingtree

Class TreeConf<I,N>

java.lang.Object
swingtree.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.

public final class TreeConf<I,N> extends Object
Describes the shape of a tree: which node types have children, where those children live inside the node, how a node is labelled, and what identifies it.

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 Type
    Method
    Description
    Declares 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.
    nodeAt(@Nullable N root, sprouts.Tuple<I> path)
    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.
    nodeAt(sprouts.Tuple<N> roots, sprouts.Tuple<I> path)
    Resolves a selection path of a forest back to the node it names, which is the same question nodeAt(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.
    <B extends N>
    TreeConf<I,N>
    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.
    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>
    of(Class<N> nodeType)
    Declares the shape of a tree whose nodes carry their own identity through HasId, which is the usual case.
    static <I, N> TreeConf<I,N>
    of(Class<N> nodeType, Class<I> idType)
    Declares the shape of a tree whose node types cannot implement HasId, by naming the identity type explicitly.
     

    Methods inherited from class java.lang.Object

    clone, equals, finalize, getClass, hashCode, notify, notifyAll, wait, wait, wait
  • Method Details

    • of

      public static <I, N extends sprouts.HasId<I>> TreeConf<I,N> of(Class<N> nodeType)
      Declares the shape of a tree whose nodes carry their own identity through HasId, which is the usual case. The result is an ordinary immutable value: bind it to a tree with UIFactoryMethods.tree(sprouts.Var, TreeConf), and ask it questions about paths with nodeAt(Object, Tuple) and nodesAlong(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 the HasId bound.
      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

      public static <I, N> TreeConf<I,N> of(Class<N> nodeType, Class<I> idType)
      Declares the shape of a tree whose node types cannot implement HasId, by naming the identity type explicitly. Their identity is then declared with idOf(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

      public <B extends N> TreeConf<I,N> 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. Declaring a block for a type that already has one replaces the previous block.
      
        UI.tree(fileSystem, conf -> conf
            .nodesOf(Dir.class, dir -> dir
                .children(Dir::entries, Dir::withEntries)
                .text(Dir::name, Dir::withName)
            )
        );
        
      A node whose type matches no block at all is treated as a leaf labelled by its 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 case of a switch does 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

      public TreeConf<I,N> 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. It is exactly nodesOf(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

      public TreeConf<I,N> idOf(Function<N,I> id)
      Declares what identifies a node, which is what lets expansion and selection survive an edit anywhere in the tree. Node types implementing HasId need no such declaration; use this one for types you do not own:
      
        .idOf( node -> node instanceof Department ? ((Department) node).id() : node )
        
      Ids only have to be unique among the children of one parent.
      Parameters:
      id - Produces the identity of a node.
      Returns:
      An updated configuration.
    • leafWhenEmpty

      public TreeConf<I,N> leafWhenEmpty(boolean leafWhenEmpty)
      Decides whether a branch that currently has no children should be drawn as a leaf. SwingTree defaults to false, meaning the presence of a children(..) rule alone makes a node a branch, so an empty folder still looks like a folder. Pass true for the plain JTree behaviour, where a node with zero children is a leaf.
      Parameters:
      leafWhenEmpty - True to draw childless branches as leaves.
      Returns:
      An updated configuration.
    • nodeAt

      public Optional<N> nodeAt(@Nullable N root, sprouts.Tuple<I> path)
      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 Tuple of 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.

      
        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));
      
        // "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
        );
        
      Note that no listener appears anywhere above, and nothing reaches into the JTree. Clicking a row writes a path into selectedPath, which makes selectedNode a 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 with UIFactoryMethods.trees(sprouts.Var, swingtree.api.Configurator), whose property holds a Tuple of 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, or null where 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

      public Optional<N> nodeAt(sprouts.Tuple<N> roots, sprouts.Tuple<I> path)
      Resolves a selection path of a forest back to the node it names, which is the same question nodeAt(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:

      
        private 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)
        );
        
      The one line worth pausing on is the FsNode.class in the middle. The three argument Viewable.of(..) takes its result type from the first property, which here holds a Tuple<FsNode> and not an FsNode, 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

      public 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. Where nodeAt(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:
      
        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)
            )
        );
        
      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.

      Use nodesAlong(Tuple, Tuple) instead where the tree was bound with UIFactoryMethods.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, or null where 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

      public 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. It is nodesAlong(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.
      
        private 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)
            )
        );
        
      Compare that comment with the one on 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

      public String toString()
      Overrides:
      toString in class Object