This may appear to be a standard Swing tree component:
However, it's not. It is pluggable. Each of the root nodes shown above comes from a different module, while the view component is loosely coupled from its data sources:
The view module needs only the GenericNodeProvider module, because it will load implementations of the GenericNodeProvider interface via the Java extension mechanism, i.e., via META-INF/service registration entries.
So, the starting point is to create a module containing a GenericNodeProvider:
package my.node.provider;
import java.util.List;
public interface GenericNodeProvider {
List<String> getData();
}
Then, in the, in this case, three provider modules, we depend on the module providing the above generic provider, by implementing the interface, e.g:
package org.provider.city;
import java.util.ArrayList;
import java.util.List;
import my.node.provider.GenericNodeProvider;
import org.openide.util.lookup.ServiceProvider;
@ServiceProvider(service=GenericNodeProvider.class)
public class City implements GenericNodeProvider{
@Override
public List<String> getData() {
List list = new ArrayList();
list.add("Toronto");
list.add("San Francisco");
list.add("London");
return list;
}
@Override
public String toString() {
return "City";
}
}
The @ServiceProvider annotation registers the provider in the META-INF/services folder.
Next, we create a TopComponent containing a BeanTreeView and an ExplorerManager, as always in NetBeans Platform applications. This module, i.e., the view module, depends on the module containing the GenericNodeProvider, but not on any of the other modules.
Then, here's the code. Note especially the usage of Lookup.getDefault to retrieve the registered providers:
private ExplorerManager em = new ExplorerManager();
public PluggableNodeExplorerTopComponent() {
initComponents();
setName(NbBundle.getMessage(PluggableNodeExplorerTopComponent.class,
"CTL_PluggableNodeExplorerTopComponent"));
setToolTipText(NbBundle.getMessage(PluggableNodeExplorerTopComponent.class,
"HINT_PluggableNodeExplorerTopComponent"));
setLayout(new BorderLayout());
BeanTreeView btv = new BeanTreeView();
btv.setRootVisible(false);
em.setRootContext(new AbstractNode(
Children.create(new FirstLevelChildFactory(), true)));
add(btv, BorderLayout.CENTER);
associateLookup(ExplorerUtils.createLookup(em, getActionMap()));
}
private class FirstLevelChildFactory extends ChildFactory<GenericNodeProvider> {
@Override
protected boolean createKeys(List list) {
list.addAll(Lookup.getDefault().lookupAll(GenericNodeProvider.class));
return true;
}
@Override
protected Node createNodeForKey(GenericNodeProvider key) {
Node node = new AbstractNode(
Children.create(new SecondLevelChildFactory(key.getData()), true));
node.setDisplayName(key.toString());
return node;
}
}
private static class SecondLevelChildFactory extends ChildFactory<String> {
private final List key;
public SecondLevelChildFactory(List key) {
this.key = key;
}
@Override
protected boolean createKeys(List<String> list) {
list.addAll(key);
return true;
}
@Override
protected Node createNodeForKey(String key) {
Node node = new AbstractNode(Children.LEAF);
node.setDisplayName(key);
return node;
}
}
And that's all.
This feels very similar to the registration style of the Services Tab being loosely coupled from registered root notes using @ServicesTabNodeRegistration .
Would the same thing be possible for a menu?
Henk, menus are provided by actions registered in the "Actions" folder. So, this functionality already works out of the box for Actions and there's nothing you need to do for that. If you live somewhere in the Netherlands, let me know, and I'll come and introduce you to the NetBeans Platform via a few demos.
SecondLevelChildFactory should take a GenericNodeProvider, not its List<String> data - so that the data is retrieved only if and when the node is expanded.