UIForTree.java
package swingtree;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sprouts.Action;
import sprouts.From;
import sprouts.Tuple;
import sprouts.Val;
import sprouts.Var;
import swingtree.api.Configurator;
import swingtree.api.IconDeclaration;
import javax.swing.Icon;
import javax.swing.JTree;
import javax.swing.ToolTipManager;
import javax.swing.tree.DefaultTreeCellEditor;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.TreeCellEditor;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import java.awt.Component;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.EventObject;
import java.util.List;
import java.util.Objects;
/**
* A SwingTree builder node designed for configuring {@link JTree} instances.
* <p>
* The recommended way to get one is {@link UI#tree(Var, Configurator)}, which binds the
* tree to a single property holding a deeply immutable, nested data structure and a
* {@link TreeConf} saying which parts of it to zoom into:
* <pre>{@code
* UI.tree(fileSystem, conf -> conf
* .nodesOf(Dir.class, dir -> dir
* .children(Dir::entries, Dir::withEntries)
* .text(Dir::name, Dir::withName)
* )
* .nodesOf(Doc.class, doc -> doc.text(Doc::name))
* )
* .withSelection(selectedNode);
* }</pre>
* Where the data has no single root but several top level nodes,
* {@link UI#trees(Var, Configurator)} binds a {@code Var<Tuple<N>>} of them instead. The
* builder returned is this same one, and everything below configures both forms alike.
* <p>
* <b>Please take a look at the <a href="https://globaltcad.github.io/swing-tree/">living swing-tree documentation</a>
* where you can browse a large collection of examples demonstrating how to use the API of this class.</b>
*
* @param <I> The identity type of the nodes, which selection paths are made of.
* @param <N> The common type of the nodes of the tree managed by this builder.
* @param <T> The type of the {@link JTree} instance which will be managed by this builder.
*/
public final class UIForTree<I, N, T extends JTree> extends UIForAnySwing<UIForTree<I, N, T>, T>
{
private static final Logger log = LoggerFactory.getLogger(UIForTree.class);
private final BuilderState<T> _state;
private final @Nullable PropertyTreeModel<I, N, ?> _model;
UIForTree( BuilderState<T> state, @Nullable PropertyTreeModel<I, N, ?> model ) {
_state = Objects.requireNonNull(state);
_model = model;
}
@Override
protected BuilderState<T> _state() {
return _state;
}
@Override
protected UIForTree<I, N, T> _newBuilderWithState( BuilderState<T> newState ) {
return new UIForTree<>(newState, _model);
}
/**
* Installs a plain Swing {@link TreeModel}, which is the escape hatch for a tree whose
* contents genuinely do not live in a property, or which already has a model you want
* to keep. A tree installed like this takes no part in any of the property binding this
* builder otherwise offers.
* <p>
* Installing one on a tree built by {@code UI.tree(rootProperty, ..)} replaces the bound
* model, which leaves the binding with nothing to drive; SwingTree says so in the log
* rather than leaving you to work out why the tree stopped following the property. Build
* the tree with {@link UI#of(JTree)} instead when the contents are not a property.
*
* @param model The {@link TreeModel} to install.
* @return This builder node, to allow for method chaining.
*/
public final UIForTree<I, N, T> withModel( TreeModel model ) {
Objects.requireNonNull(model, "model");
PropertyTreeModel<I, N, ?> bound = _model;
return _with( thisComponent -> {
if ( bound != null ) {
log.warn(SwingTree.get().logMarker(),
"Replacing the bound model of a tree with a plain '{}'. The tree will no " +
"longer follow the property it was built from. Use 'UI.of(someTree)' if " +
"the contents of this tree are not supposed to live in a property.",
model.getClass().getSimpleName());
thisComponent.setCellRenderer(new DefaultTreeCellRenderer());
thisComponent.setEditable(false);
}
thisComponent.setModel(model);
})
._this();
}
/**
* Binds the selected position of the tree to a mutable property holding a
* <b>path of ids</b>, which keeps the two in sync in both directions: selecting a node
* writes the path leading to it into the property, and assigning a path selects the
* node it names, opening every branch above it and scrolling it into view.
* <pre>{@code
* Var<Tuple<String>> selectedPath = vm.zoomTo(Move::selectedPath, Move::withSelectedPath);
*
* UI.tree(move, conf -> ..).withSelection(selectedPath);
* // nothing selected -> Tuple.of(String.class)
* // the root -> ["move"]
* // the kettle -> ["move", "kitchen", "appliances", "kettle"]
* }</pre>
* <b>A selection is a position, and only a path names a position.</b> A node value
* cannot: the same value may sit in several places at once, and a tree only requires
* its ids to be unique <i>among siblings</i>, so two folders may each hold a
* {@code notes.txt}. A path of ids has nothing to be ambiguous about, needs no search
* to resolve, and carries no data which could contradict the tree.
* <p>
* The empty tuple means nothing is selected, so there is no null to handle. To get at
* the node a path names — for a detail view, say — ask the configuration:
* {@link TreeConf#nodeAt(Object, Tuple)} and {@link TreeConf#nodesAlong(Object, Tuple)}.
* <p>
* On a tree bound through {@link UI#trees(Var, Configurator)} the first id names a top
* level node rather than a root, so its paths are one element shorter and no path at all
* names the forest. Resolve them with {@link TreeConf#nodeAt(Tuple, Tuple)}.
* <p>
* Binding a single path this way also puts the tree into single selection mode. Use
* {@link #withSelectionPaths(Var)} for a tree the user may select several nodes in.
*
* @param selection A property holding the ids leading to the selected node.
* @return This builder node, to allow for method chaining.
*/
public final UIForTree<I, N, T> withSelection( Var<Tuple<I>> selection ) {
Objects.requireNonNull(selection, "selection");
PropertyTreeModel<I, N, ?> model = _requireBoundModel("withSelection(Var)");
if ( model == null )
return _this();
return _with( thisComponent -> {
thisComponent.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
Runnable writeBack = () -> {
Tuple<I> path = model.idTupleOf(
thisComponent.getSelectionPath(),
_idTypeOf(selection, model)
);
_runInApp(() -> selection.set(From.VIEW, path));
};
thisComponent.addTreeSelectionListener( event -> {
if ( model.isRestructuring() )
return; // The tree is mid rebuild, so its selection means nothing yet.
writeBack.run();
});
model.onAfterRestructure(writeBack);
})
._withOnShow( selection, (thisComponent, path) -> {
_selectPaths(thisComponent, model, _asPaths(path));
})
._with( thisComponent -> {
Tuple<I> initial = selection.orElseNull();
if ( initial != null && !initial.isEmpty() )
_selectPaths(thisComponent, model, _asPaths(initial));
})
._this();
}
/**
* Binds the selected position of the tree to a read only property holding a path of
* ids, so that the tree follows the property but a selection made by the user is not
* written back. See {@link #withSelection(Var)} for why a selection is a path.
*
* @param selection A read only property holding the ids leading to the selected node.
* @return This builder node, to allow for method chaining.
*/
public final UIForTree<I, N, T> withSelection( Val<Tuple<I>> selection ) {
Objects.requireNonNull(selection, "selection");
PropertyTreeModel<I, N, ?> model = _requireBoundModel("withSelection(Val)");
if ( model == null )
return _this();
return _with( thisComponent -> {
thisComponent.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
})
._withOnShow( selection, (thisComponent, path) -> {
_selectPaths(thisComponent, model, _asPaths(path));
})
._with( thisComponent -> {
Tuple<I> initial = selection.orElseNull();
if ( initial != null && !initial.isEmpty() )
_selectPaths(thisComponent, model, _asPaths(initial));
})
._this();
}
/**
* Binds every selected position of the tree to a mutable property holding one path of
* ids per selected node, which also puts the tree into a mode where the user may
* select any number of nodes. The two stay in sync in both directions, just like
* {@link #withSelection(Var)}.
*
* @param selection A property holding one path of ids per selected node.
* @return This builder node, to allow for method chaining.
*/
public final UIForTree<I, N, T> withSelectionPaths( Var<Tuple<Tuple<I>>> selection ) {
Objects.requireNonNull(selection, "selection");
PropertyTreeModel<I, N, ?> model = _requireBoundModel("withSelectionPaths(Var)");
if ( model == null )
return _this();
return _with( thisComponent -> {
thisComponent.getSelectionModel().setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
Runnable writeBack = () -> {
Class<I> idType = _idTypeOfNested(selection, model);
TreePath[] paths = thisComponent.getSelectionPaths();
List<Tuple<I>> asIds = new ArrayList<>();
if ( paths != null )
for ( TreePath path : paths )
asIds.add(model.idTupleOf(path, idType));
Tuple<Tuple<I>> selected = Tuple.of(Tuple.classTyped(idType), asIds);
_runInApp(() -> selection.set(From.VIEW, selected));
};
thisComponent.addTreeSelectionListener( event -> {
if ( model.isRestructuring() )
return; // The tree is mid rebuild, so its selection means nothing yet.
writeBack.run();
});
model.onAfterRestructure(writeBack);
})
._withOnShow( selection, (thisComponent, paths) -> {
_selectPaths(thisComponent, model, _asPathList(paths));
})
._with( thisComponent -> {
Tuple<Tuple<I>> initial = selection.orElseNull();
if ( initial != null && !initial.isEmpty() )
_selectPaths(thisComponent, model, _asPathList(initial));
})
._this();
}
/**
* Registers an action which is invoked whenever the selection of the tree changes.
* The action receives a {@link TreeSelectionDelegate} speaking in your own node values,
* so there is never a reason to reach into the tree's own path objects.
* <pre>{@code
* UI.tree(fileSystem, conf -> ..)
* .onSelection( it -> it.lead().ifPresent(node -> open(node)) );
* }</pre>
* For the common case of mirroring the selection into a view model, prefer
* {@link #withSelection(Var)}, which needs no action at all.
*
* @param action The action to invoke when the selection changes.
* @return This builder node, to allow for method chaining.
*/
public final UIForTree<I, N, T> onSelection( Action<TreeSelectionDelegate<I, N>> action ) {
Objects.requireNonNull(action, "action");
PropertyTreeModel<I, N, ?> model = _requireBoundModel("onSelection(..)");
if ( model == null )
return _this();
return _with( thisComponent -> {
Runnable notify = () -> {
// Captured on the UI thread, delivered on the application thread:
TreeSelectionDelegate<I, N> delegate = new TreeSelectionDelegate<>(
thisComponent, model,
thisComponent.getSelectionPath(),
thisComponent.getSelectionPaths()
);
_runInApp(() -> {
try {
action.accept(delegate);
} catch (Exception e) {
log.error(SwingTree.get().logMarker(),
"Error occurred while processing a tree selection event.", e);
}
});
};
thisComponent.addTreeSelectionListener( event -> {
if ( model.isRestructuring() )
return; // The tree is mid rebuild, so its selection means nothing yet.
notify.run();
});
model.onAfterRestructure(notify);
})
._this();
}
/**
* Decides whether the root node of the bound structure is drawn as a row of its own.
* Hiding it also turns on the handles of the level below, so that what is now the first
* visible level can still be opened.
* <p>
* A tree bound through {@link UI#trees(Var, Configurator)} has no root node, so there is
* nothing here to show: showing it is ignored and reported in the log, and hiding it was
* already the case.
*
* @param rootVisible True to show the root node.
* @return This builder node, to allow for method chaining.
*/
public final UIForTree<I, N, T> withRootVisible( boolean rootVisible ) {
return _with( thisComponent -> {
_applyRootVisible(thisComponent, rootVisible, "withRootVisible(boolean)");
if ( !rootVisible )
thisComponent.setShowsRootHandles(true);
})
._this();
}
/**
* Binds the visibility of the root node to a property, so that the tree can fold its
* root away and back in response to application state. A tree bound through
* {@link UI#trees(Var, Configurator)} has no root node, so see
* {@link #withRootVisible(boolean)} for what happens there.
*
* @param rootVisible A property telling whether the root node should be shown.
* @return This builder node, to allow for method chaining.
*/
public final UIForTree<I, N, T> isRootVisibleIf( Val<Boolean> rootVisible ) {
Objects.requireNonNull(rootVisible, "rootVisible");
return _withOnShow( rootVisible, (thisComponent, visible) -> {
_applyRootVisible(thisComponent, Boolean.TRUE.equals(visible), "isRootVisibleIf(Val)");
})
._with( thisComponent -> {
_applyRootVisible(
thisComponent, Boolean.TRUE.equals(rootVisible.orElseNull()), "isRootVisibleIf(Val)"
);
})
._this();
}
private void _applyRootVisible( JTree tree, boolean rootVisible, String method ) {
PropertyTreeModel<I, N, ?> model = _model;
if ( rootVisible && model != null && model.isForest() ) {
log.warn(SwingTree.get().logMarker(),
"Ignoring '{}' on a tree bound to a tuple of top level nodes, which has no root " +
"node to show. Bind it through 'UI.tree(rootProperty, ..)' if it should have one.",
method);
return;
}
tree.setRootVisible(rootVisible);
}
/**
* Decides whether the handles which expand and collapse a branch are drawn next to the
* top level nodes as well. Hiding the root turns these on, because they are then the only
* handles there are — and turning them off again on a tree with no visible root leaves
* the user nothing to click, so only do that where expansion is driven from code.
*
* @param showsRootHandles True to draw handles next to the top level nodes.
* @return This builder node, to allow for method chaining.
*/
public final UIForTree<I, N, T> withRootHandlesVisible( boolean showsRootHandles ) {
return _with( thisComponent -> {
thisComponent.setShowsRootHandles(showsRootHandles);
})
._this();
}
/**
* Sets the height of every row of the tree, in the same scale independent developer
* pixels the rest of SwingTree speaks in. Pass {@code 0} to let each row take the
* height its own cell view asks for.
*
* @param rowHeight The height of a row.
* @return This builder node, to allow for method chaining.
*/
public final UIForTree<I, N, T> withRowHeight( int rowHeight ) {
return _with( thisComponent -> {
thisComponent.setRowHeight(rowHeight <= 0 ? rowHeight : UI.scale(rowHeight));
})
._this();
}
/**
* Expands every branch down to the given depth, once, at the point this builder method
* runs, where a depth of {@code 1} opens the topmost visible level, {@code 2} the level
* below that as well, and so on. This is a convenience for the initial view only:
* expansion the user performs afterwards is untouched by it, and so is everything the
* bound property grows later.
* <p>
* Depth is counted from what is on screen, so a root hidden with
* {@link #withRootVisible(boolean)} — and the absent root of a forest — does not count
* as a level. Call this <i>after</i> hiding the root, so that it knows.
*
* @param depth How many visible levels to expand.
* @return This builder node, to allow for method chaining.
*/
public final UIForTree<I, N, T> withInitialExpansionDepth( int depth ) {
return _with( thisComponent -> {
_expandToDepth(thisComponent, depth);
})
._this();
}
/**
* Use this to build a cell renderer covering several node types at once, through the
* same fluent API the list, combo box and table components expose:
* <pre>{@code
* UI.tree(fileSystem, conf -> ..)
* .withCells( it -> it
* .when(Dir.class).asText( cell -> cell.entry().map(Dir::name).orElse("") )
* .when(Doc.class).asComponent( cell -> myFancyRow(cell) )
* );
* }</pre>
* Note that for a label and an icon there is no need to come here at all: declare them
* as part of the node's own rule with {@link TreeNodeConf#text(java.util.function.Function)}
* and {@link TreeNodeConf#icon(java.util.function.Function)}. Reach for this method when
* a node needs a view a label cannot give it.
* <p>
* The two mix freely: a node type this builder says nothing about keeps the label, icon
* and tool tip declared in its own rule, so covering one type here does not oblige you
* to cover the rest.
*
* @param renderBuilder Configures the cell renderer.
* @param <V> The type of the node values being rendered.
* @return This builder node, to allow for method chaining.
*/
public final <V extends N> UIForTree<I, N, T> withCells(
Configurator<CellBuilder<T, V>> renderBuilder
) {
Objects.requireNonNull(renderBuilder, "renderBuilder");
CellBuilder builder = CellBuilder.forTree(Object.class);
try {
builder = renderBuilder.configure(builder);
} catch (Exception e) {
log.error(SwingTree.get().logMarker(), "Error while building the cells of a tree.", e);
return _this();
}
Objects.requireNonNull(builder);
PropertyTreeModel<I, N, ?> model = _model;
/*
The tree's own renderer goes in as the fallback, so a node type no 'when(..)'
clause covers keeps the rules declared for it instead of falling to 'toString()'.
*/
TreeCellRenderer renderer = builder.getForTree(
model == null ? null : new DefaultBoundRenderer<>(model)
);
return withCellRenderer(renderer);
}
/**
* Configures a single cell view used for every node of the tree, which is the shorter
* form of {@link #withCells(Configurator)} when there is no need to distinguish between
* node types.
*
* @param cellConfigurator Configures the cell view.
* @param <V> The type of the node values being rendered.
* @return This builder node, to allow for method chaining.
*/
public final <V extends N> UIForTree<I, N, T> withCell(
Configurator<CellConf<T, V>> cellConfigurator
) {
Objects.requireNonNull(cellConfigurator, "cellConfigurator");
return withCells( it -> it.when((Class) Object.class).as(cellConfigurator) );
}
/**
* Installs a plain Swing {@link TreeCellRenderer}. A bound tree keeps internal handles
* inside its paths, so the renderer is wrapped in one which unwraps them first: what
* reaches your renderer is always your own node value.
* <p>
* A renderer installed this way answers for <i>every</i> node, so the {@code text(..)},
* {@code icon(..)} and {@code toolTip(..)} rules declared per node type no longer apply.
* Use {@link #withCells(Configurator)} to cover only some node types and leave the rest
* to their own rules.
*
* @param renderer The renderer to paint the nodes with.
* @return This builder node, to allow for method chaining.
*/
public final UIForTree<I, N, T> withCellRenderer( TreeCellRenderer renderer ) {
Objects.requireNonNull(renderer, "renderer");
PropertyTreeModel<I, N, ?> model = _model;
return _with( thisComponent -> {
thisComponent.setCellRenderer(
model == null ? renderer : new UnwrappingRenderer(model, renderer)
);
})
._this();
}
/**
* Installs a plain Swing {@link TreeCellEditor}, replacing the in place rename the
* {@link TreeNodeConf#text(java.util.function.Function, java.util.function.BiFunction)}
* rule installs by itself.
*
* @param editor The editor to edit the nodes with.
* @return This builder node, to allow for method chaining.
*/
public final UIForTree<I, N, T> withCellEditor( TreeCellEditor editor ) {
Objects.requireNonNull(editor, "editor");
return _with( thisComponent -> {
thisComponent.setCellEditor(editor);
})
._this();
}
// -------------------------------------------------------------- Internals
static <I, N, T extends JTree> UIForTree<I, N, T> _bind(
BuilderState<T> state, Val<N> root, TreeConf<I, N> conf, boolean writable
) {
return _bindTo(state, root, TreeRoots.single(conf.nodeType()), conf, writable);
}
static <I, N, T extends JTree> UIForTree<I, N, T> _bindForest(
BuilderState<T> state, Val<Tuple<N>> roots, TreeConf<I, N> conf, boolean writable
) {
return _bindTo(state, roots, TreeRoots.forest(conf.nodeType()), conf, writable);
}
private static <I, N, R, T extends JTree> UIForTree<I, N, T> _bindTo(
BuilderState<T> state, Val<R> bound, TreeRoots<N, R> roots, TreeConf<I, N> conf, boolean writable
) {
PropertyTreeModel<I, N, R> model = new PropertyTreeModel<>(
bound, roots, conf, writable, state.eventProcessor()
);
UIForTree<I, N, T> builder = new UIForTree<>(state, model);
return builder
._with( thisComponent -> {
model.attachTo(thisComponent);
thisComponent.setModel(model);
thisComponent.setCellRenderer(new DefaultBoundRenderer<>(model));
ToolTipManager.sharedInstance().registerComponent(thisComponent);
if ( model.isForest() ) {
// Nothing sits above the top level, and its handles are the only ones there are:
thisComponent.setRootVisible(false);
thisComponent.setShowsRootHandles(true);
}
if ( model.isWritable() && conf.hasRenamableRule() ) {
thisComponent.setEditable(true);
thisComponent.setCellEditor(new BoundCellEditor<>(
thisComponent, (DefaultTreeCellRenderer) thisComponent.getCellRenderer(), model
));
}
})
._withOnShow( bound, (thisComponent, newValue) -> {
model.applyNewValue(newValue);
})
._this();
}
private @Nullable PropertyTreeModel<I, N, ?> _requireBoundModel( String method ) {
PropertyTreeModel<I, N, ?> model = _model;
if ( model == null )
log.warn(SwingTree.get().logMarker(),
"Ignoring '{}' on a tree which is not bound to a property. Create the tree through " +
"'UI.tree(rootProperty, conf -> ..)' to be able to use it.", method);
return model;
}
/**
* Taken from the value the property already holds, because a {@link Tuple} compares its
* element type as part of its equality: a tuple of the wrong type would never compare
* equal to what the property holds, so every write would echo back forever.
*/
private Class<I> _idTypeOf( Val<Tuple<I>> selection, PropertyTreeModel<I, N, ?> model ) {
Tuple<I> current = selection.orElseNull();
return ( current == null ? model.idType() : current.type() );
}
private Class<I> _idTypeOfNested( Val<Tuple<Tuple<I>>> selection, PropertyTreeModel<I, N, ?> model ) {
Tuple<Tuple<I>> current = selection.orElseNull();
if ( current != null && !current.isEmpty() )
return current.get(0).type();
return model.idType();
}
private List<Tuple<I>> _asPaths( @Nullable Tuple<I> path ) {
List<Tuple<I>> paths = new ArrayList<>(1);
if ( path != null && !path.isEmpty() )
paths.add(path);
return paths;
}
private List<Tuple<I>> _asPathList( @Nullable Tuple<Tuple<I>> paths ) {
List<Tuple<I>> result = new ArrayList<>();
if ( paths != null )
for ( int i = 0; i < paths.size(); i++ ) {
Tuple<I> path = paths.get(i);
if ( path != null && !path.isEmpty() )
result.add(path);
}
return result;
}
/**
* A path which no longer names anything simply takes no part in the selection,
* rather than clearing it.
*/
private void _selectPaths( JTree tree, PropertyTreeModel<I, N, ?> model, List<Tuple<I>> idPaths ) {
if ( idPaths.isEmpty() ) {
tree.clearSelection();
return;
}
List<TreePath> resolved = new ArrayList<>(idPaths.size());
for ( Tuple<I> idPath : idPaths ) {
TreePath path = model.pathForIdTuple(idPath);
if ( path != null )
resolved.add(path);
}
if ( resolved.isEmpty() ) {
tree.clearSelection();
return;
}
for ( TreePath path : resolved )
tree.makeVisible(path);
tree.setSelectionPaths(resolved.toArray(new TreePath[0]));
tree.scrollPathToVisible(resolved.get(0));
}
private static void _expandToDepth( JTree tree, int depth ) {
if ( depth <= 0 )
return;
// A root nobody can see is no level of its own, so it does not count towards the depth:
int invisibleLevels = ( tree.isRootVisible() ? 0 : 1 );
// Expanding a row adds rows below it, so the count is re-read on every step:
for ( int row = 0; row < tree.getRowCount(); row++ ) {
TreePath path = tree.getPathForRow(row);
if ( path != null && path.getPathCount() - invisibleLevels <= depth )
tree.expandRow(row);
}
}
/**
* Unwraps the node handle and renders the node by the rules declared for its type.
* @param <I> The identity type of the nodes, which selection paths are made of.
* @param <N> The common node type of the tree.
*/
private static final class DefaultBoundRenderer<I, N> extends DefaultTreeCellRenderer
{
private final PropertyTreeModel<I, N, ?> _model;
DefaultBoundRenderer( PropertyTreeModel<I, N, ?> model ) {
_model = model;
}
@Override
public Component getTreeCellRendererComponent(
JTree tree, Object value, boolean selected,
boolean expanded, boolean leaf, int row, boolean hasFocus
) {
Object node = _model.valueOf(value);
TreeNodeConf<N, ?> rule = _model.ruleOf(value);
String text = null;
IconDeclaration iconDeclaration = null;
String toolTip = null;
if ( rule != null && node != null ) {
try {
text = rule.textOf(node);
iconDeclaration = rule.iconOf(node);
toolTip = rule.toolTipOf(node);
} catch (Exception e) {
log.error(SwingTree.get().logMarker(),
"Failed to render a tree node of type '{}'.",
node.getClass().getSimpleName(), e);
}
}
if ( text == null )
text = String.valueOf(node);
Component view = super.getTreeCellRendererComponent(
tree, text, selected, expanded, leaf, row, hasFocus
);
// After the super call, which picks a leaf/open/closed icon of its own every time:
if ( iconDeclaration != null ) {
Icon icon = iconDeclaration.find().orElse(null);
if ( icon != null )
setIcon(icon);
}
setToolTipText(toolTip);
return view;
}
}
/** Keeps a user supplied renderer from ever seeing the internal node handles. */
private static final class UnwrappingRenderer implements TreeCellRenderer
{
private final PropertyTreeModel<?, ?, ?> _model;
private final TreeCellRenderer _delegate;
UnwrappingRenderer( PropertyTreeModel<?, ?, ?> model, TreeCellRenderer delegate ) {
_model = model;
_delegate = delegate;
}
@Override
public Component getTreeCellRendererComponent(
JTree tree, Object value, boolean selected,
boolean expanded, boolean leaf, int row, boolean hasFocus
) {
return _delegate.getTreeCellRendererComponent(
tree, _model.valueOf(value), selected, expanded, leaf, row, hasFocus
);
}
}
/**
* Differs from the plain Swing editor in two places: it opens showing what the node's
* {@code text(..)} rule produces rather than its {@code toString()}, and it refuses to
* open on a node the user pointed at whose type declared no wither.
*/
private static final class BoundCellEditor<I, N> extends DefaultTreeCellEditor
{
private final PropertyTreeModel<I, N, ?> _model;
BoundCellEditor( JTree tree, DefaultTreeCellRenderer renderer, PropertyTreeModel<I, N, ?> model ) {
super(tree, renderer);
_model = model;
}
@Override
public Component getTreeCellEditorComponent(
JTree tree, Object value, boolean isSelected, boolean expanded, boolean leaf, int row
) {
Object node = _model.valueOf(value);
TreeNodeConf<N, ?> rule = _model.ruleOf(value);
String text = ( rule == null || node == null ? null : rule.textOf(node) );
return super.getTreeCellEditorComponent(
tree, text == null ? String.valueOf(node) : text, isSelected, expanded, leaf, row
);
}
@Override
public boolean isCellEditable( EventObject event ) {
if ( !super.isCellEditable(event) )
return false;
TreePath path = _pathTargetedBy(event);
if ( path == null )
return true;
TreeNodeConf<N, ?> rule = _model.ruleOf(path.getLastPathComponent());
return rule != null && rule.isRenamable();
}
private @Nullable TreePath _pathTargetedBy( @Nullable EventObject event ) {
if ( event instanceof MouseEvent && event.getSource() instanceof JTree ) {
MouseEvent click = (MouseEvent) event;
TreePath clicked = ((JTree) event.getSource())
.getPathForLocation(click.getX(), click.getY());
if ( clicked != null )
return clicked;
}
return lastPath;
}
}
}