The Nodes API controls the usage and creation of nodes, which are a variant of JavaBeans that may have adjustable property sets; provide cookies and actions; be visually displayed in the Explorer with full hierarchy support; and other features.
Nodes themselves ought not be used to hold actual data; that should be stored in a data object, or in some other appropriate storage mechanism. Rather, they provide a presentation device for existing data.
The Explorer interactions with nodes include actions that the node provides (generally available in a right-click context menu); cookie-based action enabling (so that the node selection affects the availability of system actions, like Compile); cut, copy, and paste support, as well as reordering of children, deletion, and creation of new children; displayable names and icons, which may be sensitive to the state of the node; and so on.
Importantly, nodes are not dead data - they are live components. So, actions taken in one part of the system will frequently cause open Explorer views to refresh to display the new node structure (for example, after pasting a component onto a form); and conversely, actions that seem natural to do in the Explorer will usually be accessible there through its interface and update the rest of the system accordingly (for example, deleting a toolbar button in the Environment subtree has immediate effect). These capabilities owe to the rich event notification supported by the Nodes API.
A more complex example is a Java class representing a form - this node actually has one child subtree representing the Java source hierarchy (classes, methods, and fields); and one subtree representing the AWT/Swing component hierarchy (frames, panels, buttons, etc.). Each type of subnode has its own behavior - for example, the component nodes can display Layout and Events property sheets, and if containers, can permit pasting in of components onto the form.
Children
object
itself); for the case of a leaf node, just pass in {@link org.openide.nodes.Children#LEAF Children.LEAF }.
As this class is not abstract, there are no strict requirements on what needs to be overridden. However, the following general methods you are likely to want to override:
You may set these explicitly with
{@link org.openide.nodes.Node#setName(java.lang.String) Node.setName(...) }
and
{@link org.openide.nodes.Node#setDisplayName(java.lang.String) Node.setDisplayName(...) },
or you may want to take advantage of AbstractNode
's
ability to have the display name be calculated implicitly from the
system name by means of a
{@link org.openide.nodes.AbstractNode#displayFormat format string }.
There is also a
{@link org.openide.nodes.Node#setShortDescription(java.lang.String) short description }
which is intended for things such as tool tips on the node.
AbstractNode
, for example by overriding
{@link org.openide.nodes.AbstractNode#createSheet() AbstractNode.createSheet() }
to provide the basic list of property sets desired for the
node. (You should get the sheet set you need from it, checking
whether it really exists yet, add properties to the sheet set, and
then replace it into the sheet to be sure your changes take
effect.)
Each property has a few interesting aspects to it:
Node.Property
which provide useful refinements:
AbstractNode
, handling
the system name only should suffice). This support ought to be used by
any node for which it makes sense for the user to modify the name in
the property sheet. If modifying the name should be permitted but
would need to trigger other changes, the support probably would not be
helpful (or it could be subclassed).
Naturally, common node implementation classes such as
DataNode
may automatically create a sheet with some useful properties on it;
then this sheet should generally be appended to by overriding
{@link org.openide.nodes.AbstractNode#createSheet() AbstractNode.createSheet() }
and calling the superclass method first.
For complex nodes, such as a system option controlling the appearance of an entire editor, it may be cumbersome for the user to edit individual properties, especially without getting a holistic preview. If this is the case, a customizing GUI component may be returned from {@link org.openide.nodes.Node#getCustomizer() Node.getCustomizer() }, and {@link org.openide.nodes.Node#hasCustomizer() Node.hasCustomizer() } turned on. The exact way in which the customizer will be displayed is determined by NetBeans, but typically it will be popped up in a dialog or non-modal window; it should be tied to the node's properties however appropriate.
If a full customizer is not required, individual properties may still have a custom editing style associated with them; {@link org.openide.nodes.Node.Property#getPropertyEditor() Node.Property.getPropertyEditor() } is used to look for a property editor, defaulting to the standard JavaBeans property editor for the appropriate type.
Note that the Nodes API, unlike JavaBeans, permits a specific
instance of
{@link java.beans.PropertyEditor PropertyEditor }
to be associated with the node, not just its class - so if you override
getPropertyEditor()
, it is possible to select an editor
based on the current state of the node (for example, a table may have
a completely different editor when it is bound to a SQL rowset), or to
keep an initialized editor associated with a node that may have some
UI state not kept in the node itself.
The basic data structure for managing a child list is {@link org.openide.nodes.Children Children}, which is not likely to be subclasses directly but rather used in the form of one of the support classes available for it. Note that the node must keep the same children object throughout its lifetime, and the children object is responsible for managing the addition, removal, and structure of children under it.
A simple child list may be created with {@link org.openide.nodes.Children.Array Children.Array }. You need only create it with the default constructor, and add child nodes to it (at any time, or remove them later for that matter) using {@link org.openide.nodes.Children#add(org.openide.nodes.Node[]) Children.add(...) }.
If it is desirable that the children be sorted when displayed, you can use e.g. {@link org.openide.nodes.Children.SortedArray Children.SortedArray } to do this. In this case, the comparator (i.e. sort criteria) can be changed at any time.
If the children need to be accessed based on keys, as in a hashtable, this is possible with {@link org.openide.nodes.Children.Map Children.Map } (and also {@link org.openide.nodes.Children.SortedMap Children.SortedMap }). Along similar lines, {@link org.openide.nodes.Children.Keys Children.Keys } permits clustering of the children by key, where several children may be associated with one key. This class may be especially useful when mirroring an external hierarchical system into a node hierarchy, such as Java class hierarchies, which need the children to be partitioned in a certain way (e.g. methods vs. fields).
This document will not go into the details of subclassing children
lists, since doing so is not likely to be required very
frequently - the provided support classes should handle the common
cases. If it is necessary to subclass, the documentation for
Children
should suffice.
Children.SortedArray
guarantees that your children will be properly sorted without any work
beyond providing the comparator. However, for an unsorted child list
it may be useful to provide support for directed reordering of the
children.
Generally you will want to make the children rearrangeable by the user, as well as by external code. To do so, you should implement the {@link org.openide.nodes.Index Index } cookie on your node, which exists to handle this case. This cookie provides ways for the user to move particular children around, or to undertake a complete rearrangement using a {@link org.openide.nodes.Index.Support#showIndexedCustomizer(org.openide.nodes.Index) dialog box }. There is a generic {@link org.openide.nodes.Index.Support support class} which implements the raw requirements of the cookie, but this is usually used in a more friendly form by using a special children implementation such as {@link org.openide.nodes.Index.ArrayChildren Index.ArrayChildren }. This implementation stores a list of children and makes it straightforward for the user to manipulate the order in several ways.
(If your node is actually a
DataNode
representing a data object, there are already some conventions for
attaching actions and cookies to the node, which prepopulate certain
entries based on the data loader and/or data object. The Datasystems
API describes these defaults.)
Attaching cookies to a node, so that it will be considered to
implement certain behaviors, is quite straightforward. The basic
interface for retrieving a cookie is
{@link org.openide.nodes.Node#getCookie(java.lang.Class) Node.getCookie(...) }.
However, this is abstract in Node
, and also
Node
itself does not set any policy for settings up the
cookies for a node or changing them.
Rather, if you are subclassing AbstractNode
, you may
use
{@link org.openide.nodes.AbstractNode#setCookieSet(org.openide.nodes.CookieSet) AbstractNode.setCookieSet(...) }
to specify a set of cookies to be returned by the node (and you should
merge your cookies with those provided by the superclass, as a
rule). The
{@link org.openide.nodes.CookieSet CookieSet}
is a simple container for cookies looked up by their representation
class. The AbstractNode
will then use this as an index
for implementing getCookie(...)
.
To attach actions to a node, which are listed by
{@link org.openide.nodes.Node#getActions() Node.getActions() }
(and sometimes a primary and obvious action in
{@link org.openide.nodes.Node#getDefaultAction() Node.getDefaultAction() }),
you should merge the superclass' actions into your own (if desired),
and override e.g.
{@link org.openide.nodes.AbstractNode#createActions() AbstractNode.createActions() },
which is called to set up the actions list when
getActions()
is first called.
These actions may be used by various UI components to clearly associate commands with the node, e.g. by providing them in a pop-up menu. {@link org.openide.nodes.Node#getDefaultAction() Node.getDefaultAction() } and {@link org.openide.nodes.Node#getContextActions() Node.getContextActions() } provide more refined variants of the actions list which may be appropriate for different presentations. Nodes with unusual needs for action presentation can override {@link org.openide.nodes.Node#getContextMenu() Node.getContextMenu() } to define a particular UI for this presentation.
Currently, system gives you ability to automatically install a node of your choice into (currently three) common places in the IDE:
Runtime nodes are installed in the Explorer's
Runtime hierarchy. This may be used for modules which need to
provide user-level access to some transient aspect of the module's
operation not otherwise apparent. For example, an HTTP filesystem
might want to provide a node under the Runtime displaying
information about its cache, and permitting operations such as
clearing the cache.
Nodes of such type should be placed in the UI/Runtime/ folder
using *.instance syntax,
simply specifying class of the node in question.
Root nodes are installed as roots for a whole new
hierarchy. These roots may be displayed as switchable tab panes in the
Explorer, to visually represent each root in parallel. Please do not
create a new root without a compelling UI justification.
Nodes of type Root should be
placed in the Windows/Components/ folder using *.settings syntax,
defining org.openide.explorer.ExplorerPanel type of component. Use ability of
*.settings file to specify creator method to asociate explorer panel
with your root node. Consult Winsys API,
xml layers section for details.
Session nodes, appropriate to items which are neither transient nor
project-oriented, are installed in the Tools/Options area, highest level.
Nodes of type Session should be
placed in the UI/Services/ folder, again using
*.instance syntax.
The basic definition of how settings in layers work is given in the Services API.
Creation of a usable handle is implemented in
AbstractNode
, and you should not need to override
it. However, note that a handle consists of a handle for the root node
of the target node's hierarchy together with a path (by system name)
down to the target node; so if you are creating a root node, and want
it or its children to be serializable, then you should create a
specific implementation of Node.Handle
capable of
reconstructing your root from scratch, and return it from
Node.getHandle()
.
The methods in NodeOp
such as
{@link org.openide.nodes.NodeOp#findPath(org.openide.nodes.Node,java.lang.String[]) NodeOp.findPath(...) }
may also be used for general-purpose navigation along the hierarchy,
should this be necessary.
Since most of this behavior is automatic and driven by the JavaBeans API, you need do little to use it: just create a node using {@link org.openide.nodes.BeanNode#BeanNode(java.lang.Object) new BeanNode(...) }.
Do not confuse such a bean node, which may be any sort of node that just happens to use the JavaBeans API to implement its behavior, with the specific kind of node created to represent a data object whose file is found to be a JavaBean (serialized, or as a class) - this latter type of node behaves in most respects like any other data node, and just adds a couple of features like a Customize action.
Or, you may use
{@link org.openide.nodes.AbstractNode#cloneNode() AbstractNode.cloneNode() }
to create the filter if the node does not intrinsically support
{@link java.lang.Cloneable Cloneable },
or to really clone it if it does. Note that a properly-designed node
does not actually store real data, but just provides an interface to
that data; and so it is reasonable to implement Cloneable
to provide a new node attached to the same data, if that behavior is
desired. Some nodes, such as DataNode
s, do not do this,
as such behavior would be contrary to the UI goal of having a data
node live in one place in the Repository according to the position of
the data object and primary file object.
NodeListener
, as well as several varieties of standard
property changes (since NodeListener
extends
PropertyChangeListener
): node name, parent, cookies,
property sets (i.e. the available properties, not their
values), and icons.
There are some simple node-level operations which do not need to
use data transfer.
{@link org.openide.nodes.AbstractNode#setName(java.lang.String) AbstractNode.setName(...) }
and
{@link org.openide.nodes.Node#destroy() Node.destroy() }
may simply be overridden to handle customized renames and
deletes. (Or, you could attach a NodeListener
to take
action after the fact, if that suffices.)
Supporting creation of fresh children is possible by overriding
{@link org.openide.nodes.Node#getNewTypes() Node.getNewTypes() }
to provide a list of new types of data which can be created under your
node. Each of these should implement
{@link org.openide.util.datatransfer.NewType#create() NewType.create() }
to actually create a new child. Make sure that you include
NewAction
in your
{@link org.openide.nodes.Node#getActions() list of actions }.
Certain standard subclasses of AbstractNode
(such as the
DataNode
commonly used to represent data objects) already have special
implementations of data transfer appropriate to your task (such as
actually moving a file object to a new folder), which may eliminate the need to deal
with it directly.
This flow assumes a copy-and-paste operation. Cut-and-paste is
rather similar (the source node would be destroyed rather than cloned,
typically). Also, use of AbstractNode
s is assumed;
otherwise the nodes involved would have to implement more.
The scenario is that Node B permits other nodes to be pasted into it, creating shortcuts; the user wants to create a shortcut to some arbitrary Node A.
AbstractNode
).
Note that
ExplorerUtils
provides the regular implementation of CopyAction
for any
TopComponent
.
Now, AbstractNode
's implementation of
createPasteTypes(...)
only allows one data flavor to be
accepted by the node (so-called "intelligent pastes"); this flavor is
hidden from the APIs but can be tested for in a transferable using
{@link org.openide.nodes.NodeTransfer#findPaste(java.awt.datatransfer.Transferable) NodeTransfer.findPaste(Transferable) }.
This is not the flavor that was provided by the copy, so no paste type
is created in the super method. However, Node B
in this example was specifically expecting to get copied nodes pasted
into it, so it overrode createPasteTypes(...)
like this:
public class Shortcuts extends AbstractNode { public Shortcuts () { super (new Index.ArrayChildren ()); setName ("Shortcuts"); getCookieSet ().add (ch); } protected SystemAction[] createActions () { return new SystemAction[] { SystemAction.get (ReorderAction.class), null, SystemAction.get (PasteAction.class) }; } protected void createPasteTypes(Transferable t, List ls) { final Node[] ns = NodeTransfer.nodes (t, NodeTransfer.COPY); if (ns != null) { ls.add (new PasteType () { public Transferable paste () throws IOException { Node[] nue = new Node[ns.length]; for (int i = 0; i < nue.length; i++) nue[i] = ns[i].cloneNode (); getChildren ().add (nue); return null; } }); } // Also try superclass, but give it lower priority: super.createPasteTypes(t, ls); } }Nothing is actually pasted yet. However, one paste type, that provided by Node B, has been added to the set of paste types. So, the Paste action sees that there is an option to paste, and provides a context menu item (by default labelled "Paste"), enables the toolbar button, etc.
paste()
method is actually called, making an alias of
Node A and inserting it as one of B's children. The method returns
null
, so the clipboard is left alone.
public Transferable clipboardCopy () throws IOException { Transferable deflt = super.clipboardCopy (); ExTransferable added = ExTransferable.create (deflt); added.put (new ExTransferable.Single (DataFlavor.stringFlavor) { protected Object getData () { return getDisplayName (); } }); return added; }
If the node winds up having multiple paste types available at once, NetBeans may display all of them, say in a submenu. They will be displayed in the same order as they were added.
AbstractNode
by default just looks for the secret data
flavor represented by
{@link org.openide.nodes.NodeTransfer#createPaste(org.openide.nodes.NodeTransfer.Paste) NodeTransfer.createPaste(Paste) }
and
{@link org.openide.nodes.NodeTransfer#findPaste(java.awt.datatransfer.Transferable) NodeTransfer.findPaste(Transferable) },
any part of the
system that wants to be able to paste to nodes can do so without
rewriting the node - provided it knows exactly what to do with the
target node, of course! For example, the following copy implementation
sets the display name of the target node to be the same as that of
the current node:
public Transferable clipboardCopy () throws IOException { Transferable default = super.clipboardCopy (); ExTransferable added = ExTransferable.create (default); added.put (NodeTransfer.createPaste (new NodeTransfer.Paste () { public PasteType[] types (final Node target) { return new PasteType[] { new PasteType () { public Transferable paste () throws IOException { target.setDisplayName (getDisplayName ()); // Clear the clipboard: return ExTransferable.EMPTY; } } }; } })); return added; }Of course, it would be possible to directly insert a transferable such as the one created here into the system clipboard, without needing to have
CopyAction
be
invoked on a node, if that were the desired behavior. Then the
transferable derived from NodeTransfer.createPaste
could be added directly to the system clipboard (use lookup on Clipboard
),
or added as an alternate flavor to any transferable already there.
AbstractNode
or similarly implements
clipboardCopy()
and clipboardCut()
).
ClassElement
(from java-src-model.jar)
as a cookie by way of inserting a new method with that name. Such a
convertor should work with any editor, as well as with any implementation
of the source hierarchy that provides the correct cookies. Care should be
take, however, not to override existing flavors in the
clipboard that might be more critical to users. E.g., do not add a
DataFlavor.stringFlavor
transferable if one already exists,
or some important piece of functionality may be lost. In the case of intelligent
node pastes, you could actually merge your own intelligent node paste into
an existing one (several levels of inner classes would be required!).