NetBeans Nodes API Tutorial

Last reviewed on 2020-12-23

This tutorial shows how to make use of some of the features of the Nodes API in NetBeans. It shows how to do the following:

  • Decorate Nodes with icons

  • Use HTML markup to enhance how Nodes are displayed

  • Create properties for display in the property sheet

  • Provide Actions from Nodes

This tutorial is intended as a follow-on to the NetBeans Selection Management Tutorial, which covers how Lookup is used in managing selection in the NetBeans windowing system, and its follow-on tutorial which demonstrates how to use the Nodes API in managing selection.

This tutorial builds on the source code created in the first tutorial and enhanced in the second tutorial. If you have not yet done these tutorials, you should do them first; or at least ensure you are familiar with the content and approach used, and download the completed result of the second tutorial.

For troubleshooting purposes, you are welcome to download the completed tutorial source code.

Creating a Node subclass

As mentioned in the previous tutorial, Nodes are presentation objects. That means that they are not a data model themselves - rather, they are a presentation layer for an underlying data model. In the Projects or Files windows in the NetBeans IDE, you can see Nodes used where the underlying data model is files on disk. In the Services window in the IDE, you can see them used where the underlying objects are configurable aspects of NetBeans runtime environment, such as available application servers and databases.

As a presentation layer, Nodes add human-friendly attributes to the objects they model. The essential ones are:

  • Display Name — a human readable, user-friendly display name

  • Description — a human readable, user-friendly description, often shown as a tooltip

  • Icon — some glyph that graphically indicates the type of object shown and possibly its state

  • Actions — actions that appear on the context menu when the node is right-clicked, which can be invoked by the user

In the preceding tutorial, you used your EventChildFactory class to create `Node`s, by calling

new AbstractNode(Children.create(new EventChildFactory(), true), Lookups.singleton(key));

and then calling setDisplayName(key.toString()) to provide a basic display name. There is much more that can be done to make your Nodes more user-friendly, which we will explore in this part of the tutorial series.

First you will need to create a Node subclass to work with, as detailed below.

  1. In the My Editor project, right click the package org.myorg.myeditor and choose New > Java Class. Name the class "EventNode" and press Enter or click Finish.

  1. Change the signature and constructors of the class as follows:

package org.myorg.myeditor;

import org.myorg.myapi.Event;
import org.openide.nodes.AbstractNode;
import org.openide.nodes.Children;
import org.openide.util.lookup.Lookups;

public class EventNode extends AbstractNode {

    public EventNode(Event obj) {
        super (Children.create(new EventChildFactory(), true), Lookups.singleton(obj));
        setDisplayName ("Event " + obj.getIndex());
    }

    public EventNode() {
        super (Children.create(new EventChildFactory(), true));
        setDisplayName ("Root");
    }

}
  1. Open MyEditor.java from the same package, in the code editor. Switch to Source view if not already selected. Remove this line in the constructor:

mgr.setRootContext(new AbstractNode(Children.create(new EventChildFactory(), true)));

Add this single line of code in place of the previous line:

mgr.setRootContext(new EventNode());
  1. Now make a similar change to the EventChildFactory class. Open it in the editor, and change its createNodeForKey method as follows:

@Override
protected Node createNodeForKey(Event key) {
    return new EventNode(key);
}

The code is now runnable, but so far all we’ve done is moved logic around. It will do exactly what it did before. The only (non-user-visible) difference you now are using a Node subclass instead of just using AbstractNode.

Enhancing Display Names with HTML

The first enhancement you will provide is an enhanced display name. The Nodes API supports a limited subset of HTML which you can use to enhance how the labels for Nodes are shown in Explorer UI components. The following tags are supported:

  • font color—font size and face settings are not supported, but color is, using standard HTML syntax

  • font style tags: b (bold), i (italics), u (underline) and s (strikethrough)

  • A limited subset of SGML entities: ", <, &, ‘, ’, “, ”, –, —, ≠, ≤, ≥, ©, ®, ™, and  

Since the data available from our Event doesn’t have anything really exciting, we’ll extend this artificial example, and decide that odd numbered Events should appear with blue text.

  1. Add the following method to EventNode:

@Override
public String getHtmlDisplayName() {
    Event obj = getLookup().lookup(Event.class);
    if ((obj != null) && (obj.getIndex() % 2 != 0)) {
        return "<font color='0000FF'>Event " + obj.getIndex() + "</font>";
    } else {
        return null;
    }
}

When the Explorer component renders the Node instance, it calls getHtmlDisplayName(). If it gets a non-null value back, then it will use the HTML string it received and a fast, lightweight HTML renderer to render it. If it is null, then it will fall back to whatever is returned by getDisplayName(). So in this implementation, any EventNode whose Event has an index not evenly divisible by 2 will have a non-null HTML display name.

  1. Run the Event Manager again and you should see the following:

nodesapi2 html display 1
There are two reasons for getDisplayName() and getHtmlDisplayName() being separate methods. First, it is an optimization; second, as you will see later, it makes it possible to compose HTML strings together, without needing to strip <html> marker tags.

You can enhance this further - in the previous tutorial, the date was included in the HTML string, and we have removed it here. So let’s make the HTML string a little more complex, and provide HTML display names for all of the nodes.

  1. Modify the getHtmlDisplayName() method as follows:

@Override
public String getHtmlDisplayName() {
    Event obj = getLookup().lookup(Event.class);
    if (obj != null) {
        return "<font color='#0000FF'>Event " + obj.getIndex() + "</font>"
                + " <font color='AAAAAA'><i>" + obj.getDate() + "</i></font>";
    } else {
        return null;
    }
}
  1. Run the Event Manager again and now you should see the following:

nodesapi2 html display 2

One minor thing we can do to improve appearance here: we are currently using hard-coded colors in your HTML. Yet the NetBeans Platform can run under various look and feels, and there’s no guarantee that your hard-coded color will not be the same as or very close to the background color of the tree or other UI component your Node appears in.

The NetBeans HTML renderer provides a minor extension to the HTML spec which makes it possible to look up colors by passing UIManager keys. The look and feel Swing is using provides a UIManager, which manages a name-value map of the colors and fonts a given look and feel uses. Most (but not all) look and feels find the colors to use for different GUI elements by calling UIManager.getColor(String), where the string key is some agreed-upon value. So by using values from UIManager, you can guarantee that you will always be producing readable text. The two keys you will use are "textText", which returns the default color for text (usually black unless using a look and feel with a dark-background theme), and "controlShadow" which should give us a color that contrasts, but not too much, with the default control background color.

  1. Modify the getHtmlDisplayName() method as follows:

@Override
public String getHtmlDisplayName() {
    Event obj = getLookup().lookup (Event.class);
    if (obj != null) {
        return "<font color='!textText'>Event " + obj.getIndex() + "</font>" +
                " <font color='!controlShadow'><i>" + obj.getDate() + "</i></font>";
    } else {
        return null;
    }
}
  1. Run the Event Manager again and now you should see the following:

nodesapi2 html display 3
You got rid of the blue color and switched to plain old black. Using the value of UIManager.getColor("textText") guarantees us text that will always be readable under any look and feel, which is valuable; also, color should be used sparingly in user interfaces, to avoid the angry fruit salad effect. If you really want to use wilder colors in your UI, the best bet is to either find a UIManager key/value pair that consistently gets what you want, or create a ModuleInstall class and derive the color from a color you can get from UIManager, or if you are sure you know the color theme of the look and feel, hard-code it on a per-look and feel basis (if ("aqua".equals(UIManager.getLookAndFeel().getID())…​).

Providing Icons

Icons, used judiciously, also enhance user interfaces. So providing 16x16 pixel icon is another way to improve the appearance of your UI. One caveat of using icons is, do not attempt to convey too much information via an icon—there are not a lot of pixels there to work with. A second caveat that applies to both icons and display names is, never use only color to distinguish a node— there are many people in the world who are colorblind.

Providing an icon is quite simple—you just load an image and set it. You will need to have a GIF or PNG file to use. If you do not have one easily available, here is one you can use:

nodesapi2 icon
  1. Copy the image linked above, or another 16x16 pixel PNG or GIF, into the same package as the MyEditor class.

In the Projects view, it should look like this:

nodesapi2 icon in project

In the Files view, it should look like this:

nodesapi2 icon in files
  1. Add the following method to the EventNode class:

@Override
public Image getIcon (int type) {
    return ImageUtilities.loadImage ("org/myorg/myeditor/icon.png");
}

Fix imports.

It is possible to have different icon sizes and styles. The possible int values (type) passed to getIcon() are constants on java.beans.BeanInfo, such as BeanInfo.ICON_COLOR_16x16. Also, while you can use the standard JDK ImageIO.read() to load your images, ImageUtilities.loadImage() is more optimized, has better caching behavior, and supports branding of images.
  1. If you run the code now, you will notice that the icon is used for some nodes but not others!

nodesapi2 icon display 1

The reason for this is that it is common to use a different icon for an unexpanded versus an expanded Node. All you need to do to fix this is to override another method. Add the following additional method to the EventNode class:

@Override
public Image getOpenedIcon(int type) {
    return getIcon(type);
}

Now if you run the Event Manager, all of the Nodes will have the correct icon, as shown below:

nodesapi2 icon display 2

Actions and Nodes

The next aspect of Nodes we will look at is Actions. A Node has a popup menu which can contain actions that the user can invoke against that Node. Any subclass of javax.swing.Action can be provided by a Node, and will show up in its popup menu. Additionally, there is the concept of presenters, which we will cover later.

First, let’s create a simple action for your nodes to provide:

  1. Override the getActions() method of EventNode as follows:

@Override
public Action[] getActions (boolean popup) {
    return new Action[] { new MyAction() };
}
  1. Now, create the MyAction class as an inner class of EventNode:

private class MyAction extends AbstractAction {

    public MyAction () {
        putValue (NAME, "Do Something");
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        Event obj = getLookup().lookup(Event.class);
        JOptionPane.showMessageDialog(null, "Hello from " + obj);
    }

}

Fix Imports. The full set of imports for EventNode.java should be:

import java.awt.Image;
import java.awt.event.ActionEvent;
import javax.swing.AbstractAction;
import javax.swing.Action;
import static javax.swing.Action.NAME;
import javax.swing.JOptionPane;
import org.myorg.myapi.Event;
import org.openide.nodes.AbstractNode;
import org.openide.nodes.Children;
import org.openide.util.ImageUtilities;
import org.openide.util.lookup.Lookups;
  1. Run the EventManager again and notice that when you right-click on a node, a menu item is shown:

nodesapi2 action display 1

When you select the menu item, the action is invoked:

nodesapi2 action display 2

Presenters

Of course, sometimes you will want to provide a submenu or checkbox menu item or some other component, other than a JMenuItem, to display in the popup menu. This is quite easy:

  1. Add to the signature of MyAction that it implements Presenter.Popup:

private class MyAction extends AbstractAction implements Presenter.Popup {

Use Fix Imports to add import org.openide.util.actions.Presenter;.

  1. Position the cursor in the class signature line of MyAction and press Alt-Enter when the lightbulb glyph appears in the margin, and accept the hint "Implement all abstract methods". Implement the newly created method getPopupPresenter() as follows:

@Override
public JMenuItem getPopupPresenter() {
    JMenu result = new JMenu("Submenu");  //remember JMenu is a subclass of JMenuItem
    result.add(new JMenuItem(this));
    result.add(new JMenuItem(this));
    return result;
}

Fix Imports again.

  1. Run the Event Manager again and notice that you now have the following:

nodesapi2 action display 3

The result is not too exciting - you now have a submenu called "Submenu" with two identical menu items which work as before. However you should get the idea of what is possible here: if you want to return a JCheckBoxMenuItem or some other kind of menu item, it is possible to do that.

Properties and the Property Sheet

The last subject we will cover in this tutorial is properties.

You are probably aware that NetBeans IDE contains a "property sheet" which can display the "properties" of a Node. The exact meaning of "properties" depends on how the Node is implemented. Properties are essentially name-value pairs which have a Java type, which are grouped in sets and shown in the property sheet - where writable properties can be edited via their property editors (see java.beans.PropertyEditor for general information about property editors).

So, built into Nodes from the ground up is the idea that a Node may have properties that can be viewed and, optionally, edited on a property sheet. Adding support for this is quite easy. There is a convenience class in the Nodes API, Sheet, which represents the entire set of properties for a Node. You can add instances of Sheet.Set to the Sheet, which represent "property sets", which appear in the property sheet as groups of properties.

  1. Override EventNode.createSheet() as follows:

@Override
protected Sheet createSheet() {
    Sheet sheet = Sheet.createDefault();
    Sheet.Set set = Sheet.createPropertiesSet();
    Event obj = getLookup().lookup(Event.class);

    try {
        Property indexProp = new PropertySupport.Reflection(obj, Integer.class, "getIndex", null);
        Property dateProp = new PropertySupport.Reflection(obj, ZonedDateTime.class, "getDate", null);

        indexProp.setName("index");
        dateProp.setName("date");

        set.put(indexProp);
        set.put(dateProp);
    } catch (NoSuchMethodException ex) {
        ErrorManager.getDefault();
    }

    sheet.put(set);
    return sheet;
}

As usual, Fix Imports.

  1. Right click the EventManager and choose Run and then, once it is started up, select Window > IDE Tools > Properties to show the NetBeans Platform Properties window.

  1. Move the selection between different nodes, and notice the property sheet updating, just as your MyViewer component does, as shown below:

nodesapi2 prop display 1

The above code makes use of a very convenient class: PropertySupport.Reflection, which may simply be passed an object, a type, and getter and setter method names, and it will create a Property object that can read (and optionally write) that property of the object in question. So you use PropertySupport.Reflection as a simple way to wire one Property object up to the getIndex() method of Event.

There is no property editor for a ZonedDateTime instance, so that doesn’t show up yet. However this is covered in detail in the next tutorial.
If you want Property objects for nearly all of the getters/setters on an underlying model object, you may want to use or subclass BeanNode, which is a full implementation of Node that can be given a random object and will try to create all the necessary properties for it (and listen for changes) via reflection (how exactly they are presented can be controlled by creating a BeanInfo for the class of the object to be represented by the node).

Caveat: Setting the name of your properties is very important. Property objects test their equality based on names. If you are adding some properties to a Sheet.Set and they seem to be disappearing, the most likely problem is that their name is not set, so putting one property in a HashSet with the same (empty) name as another is causing later added ones to displace earlier added ones.

  1. Just so we can see the date, we’ll add a method to the Event and update our Reflection call to use it.

Open org.myorg.myapi.Event in the code editor, and add the following:

public String getDateAsString() {
    return date.toString();
}
  1. Now switch back to the org.myorg.myeditor.EventNode implementation of createSheet(). Replace this line:

Property dateProp = new PropertySupport.Reflection(obj, ZonedDateTime.class, "getDate", null);

with this:

Property dateProp = new PropertySupport.Reflection(obj, String.class, "getDateAsString", null);
  1. Build and run the application. You should now see the date displayed:

nodesapi2 prop display 2

Read-Write Properties

To play with this concept further, what you really need is a read/write property. So the next step is to add some additional support to Event to make the Date property settable.

  1. Open org.myorg.myapi.Event in the code editor.

  1. Remove the final keyword from the line declaring the date field

  1. Add the following setter and property change support methods to Event:

private List listeners = Collections.synchronizedList(new LinkedList());

public void addPropertyChangeListener (PropertyChangeListener pcl) {
    listeners.add (pcl);
}

public void removePropertyChangeListener (PropertyChangeListener pcl) {
    listeners.remove (pcl);
}

private void fire(String propertyName, Object old, Object nue) {
    // Passing 0 below on purpose, so you only synchronize for one atomic call:
    PropertyChangeListener[] pcls = (PropertyChangeListener[]) listeners.toArray(new PropertyChangeListener[0]);
    for (PropertyChangeListener pcl : pcls) {
        pcl.propertyChange(new PropertyChangeEvent(this, propertyName, old, nue));
    }
}
  1. Now, within the Event, call the fire method above:

public void setDateFromString(String dateAsString) {
    String oldDate = getDateAsString();
    date = ZonedDateTime.parse(dateAsString);
    fire("date", oldDate, date);
}
  1. In EventNode.createSheet(), change the way dateProp is declared, so that it will call the setter method:

Property dateProp = new PropertySupport.Reflection(obj, String.class, "getDateAsString", "setDateFromString");
  1. Build and run the EventManager, and notice that you can now select an instance of EventNode in MyEditor and actually edit the date value, as shown below:

nodesapi2 prop display 3

However, there is still one bug in this code: when you change the Date property, you should also update the display name of your node. So you will make one more change to EventNode and have it listen for property changes on Event.

  1. Modify the signature of EventNode so that it implements java.beans.PropertyChangeListener:

public class EventNode extends AbstractNode implements java.beans.PropertyChangeListener {
  1. Placing the cursor in the signature line, accept the hint "Implement all abstract methods".

  1. Add the following line to the constructor which takes an argument of Event:

obj.addPropertyChangeListener(WeakListeners.propertyChange(this, obj));
Here you are using a utility method on org.openide.util.WeakListeners. This is a technique for avoiding memory leaks: an Event will only weakly reference its EventNode, so if the Node's parent is collapsed, the Node can be garbage collected. If the Node were still referenced in the list of listeners owned by Event, it would be a memory leak. In your case, the Node actually owns the Event, so this is not a terrible situation, but in real world programming, objects in a data model (such as files on disk) may be much longer-lived than Nodes displayed to the user. Whenever you add a listener to an object which you never explicitly remove, it is preferable to use WeakListeners to avoid memory leaks which will be quite a headache later. If you instantiate a separate listener class, though, be sure to keep a strong reference to it from the code that attaches it, otherwise it will be garbage collected almost as soon as it is added.
  1. Finally, implement the propertyChange() method:

@Override
public void propertyChange(PropertyChangeEvent evt) {
    if ("date".equals(evt.getPropertyName())) {
        this.fireDisplayNameChange(null, getDisplayName());
}
  1. Build and Run again, select a EventNode in the MyEditor window and change its Date property. Notice that the display name of the Node is now updated correctly, as shown below, where the day number is set to 25 and is now reflected both on the node and in the property sheet:

nodesapi2 prop display 4

Grouping Property Sets

You may have noticed when running Matisse, NetBeans IDE’s form editor, that there is a set of buttons at the top of the property sheet, for switching between groups of property sets.

Generally this is only advisable if you have a really large number of properties, and generally it’s not advisable for ease-of-use to have a really large number of properties. Nonetheless, if you feel you need to split out your sets of properties into groups, this is easy to accomplish.

Property has the methods getValue() and setValue(), as does PropertySet (both of them inherit this from java.beans.FeatureDescriptor). These methods can be used in certain cases, for passing ad-hoc "hints" between a given Property or PropertySet and the property sheet or certain kinds of property editor (for example, passing a default filechooser directory to an editor for java.io.File). And that is the technique by which you can specify a group name (to be displayed on a button) for one or more `PropertySet`s. In real world coding, this should be a localized string, not a hard-coded string as below:

  1. Open EventNode.java in the code editor.

  1. Modify the method createSheet() as follows (modified and added lines are highlighted):

@Override
protected Sheet createSheet() {
    Sheet sheet = Sheet.createDefault();
    Sheet.Set set = Sheet.createPropertiesSet();
    Sheet.Set set2 = Sheet.createPropertiesSet();
    set2.setDisplayName("Other");
    set2.setName("other");
    final Event obj = getLookup().lookup(Event.class);
    if (obj != null) {
        try {
            Property indexProp = new PropertySupport.Reflection(obj, Integer.class, "getIndex", null);
            Property dateProp = new PropertySupport.Reflection(obj, String.class, "getDateAsString", "setDateFromString");
            indexProp.setName("index");
            dateProp.setName("date");

            set.put(indexProp);
            set2.put(dateProp);
            set2.setValue("tabName", "Other Tab");
        } catch (NoSuchMethodException ex) {
            ErrorManager.getDefault();
        }
    }

    sheet.put(set);
    sheet.put(set2);
    return sheet;
}
  1. Run the Event Manager again, and notice that there are now buttons at the top of the property sheet, and there is one property under each, as seen here:

nodesapi2 prop display 5

General Property Sheet Caveats

If you used very early versions of NetBeans, you may recall they used the property sheet very heavily as a core element of the UI, whereas it’s not so prevalent today. The reason is simple - property sheet based UIs are not terribly user-friendly. That doesn’t mean don’t use the property sheet, but use it judiciously. If you have the option of providing a customizer with a nice GUI, such as via JavaFX, do so. Your users will thank you.

If you have an enormous number of properties on one object, try to find some overall settings that encapsulate the most probable combinations of settings. For example, think of what the settings for a tool for managing imports on a Java class can be - you can provide integers for setting the threshold number of usages of a package required for wildcard imports, the threshold number of uses of a fully qualified class name required before importing it at all, and lots of other numbers. Or you can ask yourself the question, what is the user trying to do? In this case, it’s either going to be getting rid of import statements or getting rid of fully qualified names. So probably settings of low noise, medium noise and high noise where "noise" refers to the amount of fully qualified class/package names in the edited source file would do just as well and be much easier to use. Where you can make life simpler for the user, do so.

Review of Concepts

This tutorial has introduced the following ideas:

  • Nodes are a presentation layer.

  • The display names of Nodes can be customized using a limited subset of HTML.

  • Nodes have icons, and you can provide custom icons for nodes you create.

  • Nodes have Actions; an Action which implements Presenter.Popup can provide its own component to display in a popup menu; the same is true for main menu items using Presenter.Menu , and toolbar items using Presenter.Toolbar .

  • Nodes have properties, which can be displayed on the property sheet.

Next Steps

You’ve now begun to delve into how to get more out of the property sheet in NetBeans. In the next tutorial, you will cover how to write custom editors and provide a custom inline editor for use in the property sheet.