TreeNodeRef.java

package swingtree;

import org.jspecify.annotations.Nullable;

import javax.swing.tree.TreePath;
import java.util.Arrays;

/**
 *  The handle a {@link javax.swing.JTree} holds inside its
 *  {@link javax.swing.tree.TreePath}s when it is bound to a property.
 *  <p>
 *  Its identity is the <b>path of ids</b> leading to the node, never the node's content.
 *  A {@link javax.swing.tree.TreePath} compares by the nodes it contains, and value objects
 *  compare by content, so identifying a node by its value would make renaming one leaf
 *  invalidate every path in the tree at once and collapse everything the user had opened.
 *  <p>
 *  A forest has one handle more than it has nodes: {@link #ofForest()} builds the invisible
 *  one sitting above the top level, whose id path is empty because no id names it and whose
 *  value is {@code null} because it wraps no node of the user's. Its children then get id
 *  paths of length one, and everything below them follows without a special case.
 *  <p>
 *  Value, parent and index ride along in mutable fields written only by
 *  {@link PropertyTreeModel} and only on the UI thread. They take no part in
 *  {@link #equals(Object)}: two handles for the same path <i>are</i> the same node,
 *  whatever either of them currently holds.
 */
final class TreeNodeRef
{
    private static final Object[] NO_IDS = new Object[0];

    private final Object[]              _idPath;
    private final int                   _hash;
    private final @Nullable TreeNodeRef _parent;
    private final int                   _depth;
    private @Nullable Object            _value; // UI thread owned, never part of identity.
    private int                         _index;

    static TreeNodeRef ofRoot( @Nullable Object value, Object id ) {
        return new TreeNodeRef(new Object[]{id}, null, value, 0);
    }

    static TreeNodeRef ofForest() {
        return new TreeNodeRef(NO_IDS, null, null, 0);
    }

    private TreeNodeRef( Object[] idPath, @Nullable TreeNodeRef parent, @Nullable Object value, int index ) {
        _idPath = idPath;
        _hash   = Arrays.hashCode(idPath);
        _parent = parent;
        _depth  = ( parent == null ? 0 : parent._depth + 1 );
        _value  = value;
        _index  = index;
    }

    TreeNodeRef child( Object childId, @Nullable Object childValue, int index ) {
        Object[] childPath = Arrays.copyOf(_idPath, _idPath.length + 1);
        childPath[_idPath.length] = childId;
        return new TreeNodeRef(childPath, this, childValue, index);
    }

    @Nullable Object value() {
        return _value;
    }

    void updateValue( @Nullable Object value ) {
        _value = value;
    }

    @Nullable TreeNodeRef parent() {
        return _parent;
    }

    int index() {
        return _index;
    }

    void updateIndex( int index ) {
        _index = index;
    }

    /** Shared by reference with every caller, so it must never be mutated. */
    Object[] idPath() {
        return _idPath;
    }

    /**
     *  Walked along the parent chain rather than sized from the id path, because a forest
     *  handle contributes a component to every path below it without contributing an id.
     */
    TreePath path() {
        Object[] chain = new Object[_depth + 1];
        TreeNodeRef current = this;
        for ( int i = _depth; i >= 0; i-- ) {
            if ( current == null )
                throw new IllegalStateException();
            chain[i] = current;
            current = current._parent;
        }
        return new TreePath(chain);
    }

    @Override
    public boolean equals( @Nullable Object other ) {
        if ( other == this )
            return true;
        if ( !(other instanceof TreeNodeRef) )
            return false;
        TreeNodeRef that = (TreeNodeRef) other;
        return this._hash == that._hash && Arrays.equals(this._idPath, that._idPath);
    }

    @Override
    public int hashCode() {
        return _hash;
    }

    /**
     *  Forwards to the node, because
     *  {@link javax.swing.JTree#convertValueToText(Object, boolean, boolean, boolean, int, boolean)}
     *  falls back to this when no renderer has anything better to say.
     */
    @Override
    public String toString() {
        return String.valueOf(_value);
    }
}