TreeSelectionDelegate.java

package swingtree;

import org.jspecify.annotations.Nullable;
import sprouts.Tuple;
import sprouts.Var;

import javax.swing.JTree;
import javax.swing.tree.TreePath;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

/**
 *  Everything a selection change in a bound {@link JTree} has to say, handed to the
 *  {@link sprouts.Action} registered through
 *  {@link UIForTree#onSelection(sprouts.Action)}.
 *  <p>
 *  It speaks entirely in the user's own node values. The handles a bound tree keeps inside
 *  its {@link TreePath}s are an internal matter and never surface here, which is also why
 *  {@link JTree#getLastSelectedPathComponent()} is not a useful thing to call on a bound tree.
 *  <p>
 *  Note that for the common case of mirroring the selection into a view model there is no
 *  need for this delegate at all: bind a property with
 *  {@link UIForTree#withSelection(Var)} instead and the two stay in sync by themselves.
 *
 * @param <I> The identity type of the nodes, which selection paths are made of.
 * @param <N> The common node type of the tree.
 */
public final class TreeSelectionDelegate<I, N>
{
    /*
        Read once, on the UI thread, in the constructor. An action is delivered on the
        application thread, and the node handles a bound tree keeps are owned by the UI
        thread, so reading them from here later would be a race under DECOUPLED. Values,
        ids and id paths are all immutable, so a snapshot of them needs no copy.
    */
    private final JTree                      _tree;
    private final PropertyTreeModel<I, N, ?> _model;
    private final @Nullable N                _lead;
    private final @Nullable Object[]         _leadIdPath;
    private final Tuple<N>                   _selection;
    private final Tuple<N>                   _pathToLead;
    private final Tuple<I>                   _leadPath;
    private final Tuple<Tuple<I>>            _selectionPaths;

    TreeSelectionDelegate(
        JTree                      tree,
        PropertyTreeModel<I, N, ?> model,
        @Nullable TreePath         leadPath,
        @Nullable TreePath[]       selectedPaths
    ) {
        _tree           = Objects.requireNonNull(tree);
        _model          = Objects.requireNonNull(model);
        TreePath[] paths = ( selectedPaths == null ? new TreePath[0] : selectedPaths );
        Class<N> nodeType = model.conf().nodeType();
        Class<I> idType   = model.idType();

        _lead       = _nodeOf(model, leadPath);
        _leadIdPath = _idPathOf(leadPath);
        _leadPath   = model.idTupleOf(leadPath, idType);

        List<N> selected = new ArrayList<>(paths.length);
        List<Tuple<I>> selectedIds = new ArrayList<>(paths.length);
        for ( TreePath path : paths ) {
            N node = _nodeOf(model, path);
            if ( node != null )
                selected.add(node);
            selectedIds.add(model.idTupleOf(path, idType));
        }
        _selection      = Tuple.of(nodeType, selected);
        _selectionPaths = Tuple.of(Tuple.classTyped(idType), selectedIds);

        List<N> trail = new ArrayList<>();
        if ( leadPath != null )
            for ( Object component : leadPath.getPath() ) {
                Object value = model.valueOf(component);
                if ( value != null )
                    trail.add(nodeType.cast(value));
            }
        _pathToLead = Tuple.of(nodeType, trail);
    }

    private static @Nullable Object[] _idPathOf( @Nullable TreePath path ) {
        if ( path == null )
            return null;
        Object last = path.getLastPathComponent();
        return ( last instanceof TreeNodeRef ? ((TreeNodeRef) last).idPath() : null );
    }

    private static <I, N> @Nullable N _nodeOf( PropertyTreeModel<I, N, ?> model, @Nullable TreePath path ) {
        if ( path == null )
            return null;
        Object value = model.valueOf(path.getLastPathComponent());
        return ( value == null ? null : model.conf().nodeType().cast(value) );
    }

    /**
     *  The tree whose selection changed, in case you need to reach past this delegate.
     *  @return The {@link JTree} this selection belongs to.
     */
    public JTree tree() {
        return _tree;
    }

    /**
     *  The node the user just moved the selection onto, which is empty when the selection
     *  was cleared rather than moved.
     *  @return The node the selection now leads with.
     */
    public Optional<N> lead() {
        return Optional.ofNullable(_lead);
    }

    /**
     *  Every currently selected node, in the order the tree reports them. For a single
     *  selection tree this holds at most one node, and it is empty when nothing is selected.
     *  @return All selected nodes.
     */
    public Tuple<N> selection() {
        return _selection;
    }

    /**
     *  The chain of nodes from the top of the tree down to (and including) the
     *  {@link #lead()} node, which is how you learn where in the structure the selection
     *  landed. It is empty when the selection was cleared. A forest has no root, so its
     *  trails begin at the top level node the selection sits under.
     *  @return The nodes leading from the top of the tree to the selected node.
     */
    public Tuple<N> pathToLead() {
        return _pathToLead;
    }

    /**
     *  The identity of the selected position: the ids leading from the top of the tree down
     *  to the {@link #lead()} node, the root's own id first — or, in a forest, the id of the
     *  top level node it sits under. This is the same value a property bound with
     *  {@link UIForTree#withSelection(sprouts.Var)} receives, and it is empty when the
     *  selection was cleared rather than moved.
     *  <p>
     *  Where {@link #pathToLead()} answers "what is selected", this answers "which position
     *  is selected" — the question a node value cannot answer on its own, because the same
     *  value may sit in several places at once.
     *
     *  @return The ids leading to the selected node.
     */
    public Tuple<I> leadPath() {
        return _leadPath;
    }

    /**
     *  The identity of every selected position, in the order the tree reports them. This is
     *  what a property bound with {@link UIForTree#withSelectionPaths(sprouts.Var)} receives.
     *
     *  @return One path of ids per selected node.
     */
    public Tuple<Tuple<I>> selectionPaths() {
        return _selectionPaths;
    }

    /**
     *  A writable property focused on the selected node, so that an action reacting to a
     *  selection can go straight on to edit what was selected, and the edit lands in the one
     *  property the tree is bound to.
     *  <p>
     *  It is empty when nothing is selected, or when the tree was bound to a read only
     *  {@link sprouts.Val}, in which case there is nothing to write into.
     *
     *  @return A lens property onto the selected node.
     */
    public Optional<Var<N>> property() {
        Object[] idPath = _leadIdPath;
        if ( idPath == null )
            return Optional.empty();
        return Optional.ofNullable(_model.propertyFor(idPath, _lead));
    }

    @Override
    public String toString() {
        return this.getClass().getSimpleName() + "[" +
                    "lead="      + ( _lead == null ? "none" : String.valueOf(_lead) ) + ", " +
                    "selection=" + _selection.size() + " node(s)" +
                "]";
    }
}