How do I "decorate" nodes that come from another module (i.e. add icons, actions)?

Apache NetBeans Wiki Index

Note: These pages are being reviewed.

Say you have a reference to the root of a tree of Node instances, and you want to add icons or actions to those nodes. First, what you do not do is call setDisplayName or any other setter on that Node (unless you created it - the point here is that it is rude and can have bad side effects to call setters on random Nodes somebody else created - setters in APIs are bugs - the fact that Node has them is a historical artifact, not proper design).

If you own the component that will display the Nodes, this sort of thing is very easily done by subclassing FilterNode and overriding the appropriate methods (e.g. getActions(), getIcon(), etc.), wrapping the original node inside your FilterNode. Now let’s say that the Node you want to decorate builds out its children in a lazy fashion, that is, only when the user expands the tree in some tree view. How would you decorate that node and all of its children, without traversing the entire tree and effectively undoing the benefits of the lazy population of the tree?

Fortunately, while this sounds rather challenging, it turns out to be surprisingly easy and simple to achieve. The trick is to subclass the FilterNode.Children class and override the copyNode() method. Below is a short example:

class NodeProxy extends FilterNode {

    public NodeProxy(Node original) {
        super(original, new ProxyChildren(original));
    }

    // add your specialized behavior here...
}

class ProxyChildren extends FilterNode.Children {

    public ProxyChildren(Node owner) {
        super(owner);
    }

    protected Node copyNode(Node original) {
        return new NodeProxy(original);
    }
}

As you can see, NodeProxy is intended to wrap around another Node and provide some additional appearance or behavioral changes (e.g. different icons or actions). The fun part is the ProxyChildren class. While very short and simple, it provides that critical ability for our NodeProxy to act as a decorator for not only the root node, but all of its children, and their children, and so on, without having to traverse the entire tree at once.

While FilterNode should NOT be used to insert additional nodes at the beginning or end of the list (see its JavaDoc), it can be easily used to filter out some of the children nodes. For instance, this refinement of ProxyChildren overrides the createNodes() method and conditionally selects the children nodes by submitting them to a custom accept() method:

   class ProxyChildren extends FilterNode.Children {

        public ProxyChildren (Node owner)  {
            super(owner);
          }

        @Override
        protected Node copyNode (Node original){
            return new NodeProxy(original);
          }

        @Override
        protected Node[] createNodes (Object object) {
            List<Node> result = new ArrayList<Node>();

            for (Node node : super.createNodes(object)) {
                if (accept(node)) {
                    result.add(node);
                  }
              }

            return result.toArray(new Node[0]);
          }

        private boolean accept (Node node) {
            // ...
          }
      }

Below a complete example of a FileFilteredNode that can be used to show a file hierarchy where only a subset of files is shown, selected by means of the standard java.io.FileFilter class:

class FileFilteredNode extends FilterNode {

    static class FileFilteredChildren extends FilterNode.Children {
        private final FileFilter fileFilter;

        public FileFilteredChildren (Node owner, FileFilter fileFilter) {
            super(owner);
            this.fileFilter = fileFilter;
          }

        @Override
        protected Node copyNode (Node original) {
            return new FileFilteredNode(original, fileFilter);
          }

        @Override
        protected Node[] createNodes (Object object) {
            List<Node> result = new ArrayList<Node>();

            for (Node node : super.createNodes(object)) {
                DataObject dataObject = (DataObject)node.getLookup().lookup(DataObject.class);

                if (dataObject != null) {
                    FileObject fileObject = dataObject.getPrimaryFile();
                    File file = FileUtil.toFile(fileObject);

                    if (fileFilter.accept(file)) {
                        result.add(node);
                      }
                  }
              }

            return result.toArray(new Node[result.size()]);
          }
      }

    public FileFilteredNode (Node original, FileFilter fileFilter) {
        super(original, new FileFilteredChildren(original, fileFilter));
      }
  }

Note that if you’re showing the filtered nodes in a tree view according to the code above, you might find expansion handles on leaf nodes. This thread from the dev@openide list discusses some solutions to this problem.