How do I create an Action that is automatically enabled and disabled depending on the selection?
There are several ways to do this, depending on what exactly you need. The basic problems all of the available solutions are addressing is that:
-
An action may be created and shown in a menu, toolbar or popup menu -or with no UI and only triggered via a keyboard shortcut
-
While it is visible on-screen, the selected file (or whatever) can change.
-
If it is context sensitive, it should run against the thing it was shown for not whatever is selected at the millisecond when it is actually called
-
People want to write main-menu and toolbar actions which are enabled and disabled based on what is selected - in practice this means writing an object that enables and disables itself based on a particular type — a particular class or its subclasses — being selected (each logical window in NetBeans has its own "selection"; the "global selection" is whatever is selected in whatever window currently has focus)
NetBeans allows context-sensitive actions to be registered declaratively using annotations.. In the IDE, File > New File > Module Development > Action will generate (on the first page of the wizard, specify that you want a context sensitive action):
@ActionID(...)
@ActionRegistration(...)
@ActionReference(...)
public final class SomeAction implements ActionListener {
private final List<Project> context;
public SomeAction(List<Project> context) {
this.context = context;
}
public void actionPerformed(ActionEvent ev) {
for (Project project : context) {
// TODO use project
}
}
}
which will be called if and only if one or more projects is selected. The good news is that the code is lightweight, simple and works; the bad news is that it doesn’t handle more complicated enablement logic.
If you want to add this action into a context menu of a node you have to overwrite the getActions() method as follows:
public Action[] getActions(boolean context) {
List<? extends Action> myActions =
Utilities.actionsForPath("Actions/YOUR_FOLDER");
return myActions.toArray(new Action[myActions.size()]);
}
If you need something more featureful, there are a few options, old and new:
NodeAction
NodeAction is somewhat more flexible, but requires more code to implement. It is just passed the array of activated nodes whenever that changes, and can choose to enable or disable itself as it wishes. Essentially this is just an action that automagically tracks the global Node selection.
Roll your own
The following is relatively simple and affords a way to perform whatever enablement logic you like (NodeAction
can do that too, but this might be a little more straightforward and your code doesn’t have to worry about nodes at all: DevFaqWhatIsANode).
public class FooAction extends AbstractAction implements LookupListener, ContextAwareAction {
private Lookup context;
Lookup.Result<Whatever> lkpInfo;
public FooAction() {
this(Utilities.actionsGlobalContext());
}
private FooAction(Lookup context) {
putValue(Action.NAME, NbBundle.getMessage(FooAction.class, "LBL_Action"));
this.context = context;
}
void init() {
assert SwingUtilities.isEventDispatchThread()
: "this shall be called just from AWT thread";
if (lkpInfo != null) {
return;
}
//The thing we want to listen for the presence or absence of
//on the global selection
lkpInfo = context.lookupResult(Whatever.class);
lkpInfo.addLookupListener(this);
resultChanged(null);
}
public boolean isEnabled() {
init();
return super.isEnabled();
}
public void actionPerformed(ActionEvent e) {
init();
for (Whatever instance : lkpInfo.allInstances()) {
// use it somehow...
}
}
public void resultChanged(LookupEvent ev) {
setEnabled(!lkpInfo.allInstances().isEmpty());
}
public Action createContextAwareInstance(Lookup context) {
return new FooAction(context);
}
}
Let’s register the above action and see how it works.
@ActionID(category = "foocat", id = "org.fooaction")
@ActionRegistration(displayName = "not_used", lazy = false) // displayName useless if lazy is false
@ActionReferences(
{
@ActionReference(path = "Menu/Edit", position = 100), // add an entry in the app-level edit menu
@ActionReference(path = "Shortcuts", name = "O-Z"), // action can be directly run with alt-Z
@ActionReference(path = "Actions/Popupmenu", position = 3) // add a menu entry in a contextual popup menu
})
public class FooAction extends AbstractAction implements LookupListener, ContextAwareAction {
...
When the Netbeans platform application is started:
-
a
FooAction
instance is created automatically using the no-arg constructor when the edit menu is created -
the same instance is reused and associated to the alt-Z shortcut
As there can be many actions in a platform app, the constructors must be lightweight. That’s why we start listening to the Lookup.Result
only when the menu item is about to be displayed.
To further optimize application startup time, you should register your action with lazy=true
whenever possible (see limitations in the @ActionRegistration
javadoc). In lazy instanciation mode the displayName
value (and other optional parameters) is used to build the action menu item, and the FooAction
instance is created only when the menu entry (or keyboard shortcut) is actually selected by user.
The FooAction
no-arg constructor uses the global selection context (DevFaqTrackGlobalSelection), i.e. action will be enabled only when Whatever
instances are present in the lookup of the focused TopComponent
.
FooAction
also implements ContextAwareAction
. This is used by methods Utilities.actionsForPath()
and Actions.actionsToPopup()
when building a contextual popup menu, for example:
List<? extends Action> actions = Utilities.actionsForPath("Actions/Popupmenu");
Lookup context = myEditorTopComponent.getLookup();
JPopupMenu popupmenu = Utilities.actionsToPopup(actions, context);
In our case actionsToPopup()
will find our FooAction_instance and, because it’s a ContextAwareAction
, will create a menu item based on the the return value of FooAction_instance createContextAwareInstance(context)
. The return value must be considered as a transient instance because a new FooAction
instance is created each time the popup menu is built (i.e. if the transient instance needs to listen to a shared object, you should probably use a WeakListener
).
Note that even if FooAction
does not implement ContextAwareAction
, different Utilities.actionsForPath()
calls might return different FooAction
instances if there is no strong reference on these instances.
If you need to enforce FooAction
as a singleton, it’s possible to register a static factory method instead of the class:
public class FooAction extends AbstractAction implements LookupListener {
private FooAction INSTANCE;
@ActionID(category = "foocat", id = "org.fooaction")
@ActionRegistration(displayName = "not_used", lazy = false) // displayName useless if lazy is false
@ActionReferences(
{
@ActionReference(path = "Menu/Edit", position = 100), // add an entry in the app-level edit menu
@ActionReference(path = "Shortcuts", name = "O-Z"), // action can be directly run with alt-Z
@ActionReference(path = "Actions/Popupmenu", position = 3) // add a menu entry in a contextual popup menu
})
public static FooAction getInstance() {
if (INSTANCE == null) {
INSTANCE = new FooAction();
}
return INSTANCE;
}
}
...
Legacy use of CookieAction
In many older (pre-NB 6.8) examples you may find references to CookieAction.
CookieAction is used to write actions that are sensitive to what is in the selected Node(s) Lookup. You can specify one or more classes that must be present in the selected Node's Lookup, and some other semantics about enablement.
Being an older class, under the hood it is using Node.getCookie(), so your action will
only be sensitive to things actually returned by that method - in other words, only objects that implement the marker
interface Node.Cookie
can work here.