Superclass Sensitive Actions
- by Geertjan
I've created a small piece of functionality that enables you to create actions for Java classes in the IDE. When the user right-clicks on a Java class, they will see one or more actions depending on the superclass of the selected class.
To explain this visually, here I have "BlaTopComponent.java". I right-click on its node in the Projects window and I see "This is a TopComponent":
Indeed, when you look at the source code of "BlaTopComponent.java", you'll see that it implements the TopComponent class. Next, in the screenshot below, you see that I have right-click a different class. In this case, there's an action available because the selected class implements the ActionListener class.
Then, take a look at this one. Here both TopComponent and ActionListener are superclasses of the current class, hence both the actions are available to be invoked:
Finally, here's a class that subclasses neither TopComponent nor ActionListener, hence neither of the actions that I created for doing something that relates to TopComponents or ActionListeners is available, since those actions are irrelevant in this context:
How does this work? Well, it's a combination of my blog entries "Generic Node Popup Registration Solution" and "Showing an Action on a TopComponent Node".
The cool part is that the definition of the two actions that you see above is remarkably trivial:
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JOptionPane;
import org.openide.loaders.DataObject;
import org.openide.util.Utilities;
public class TopComponentSensitiveAction implements ActionListener {
private final DataObject context;
public TopComponentSensitiveAction() {
context = Utilities.actionsGlobalContext().lookup(DataObject.class);
}
@Override
public void actionPerformed(ActionEvent ev) {
//Do something with the context:
JOptionPane.showMessageDialog(null, "TopComponent: " +
context.getNodeDelegate().getDisplayName());
}
}
The above is the action that will be available if you right-click a Java class that extends TopComponent. This, in turn, is the action that will be available if you right-click a Java class that implements ActionListener:
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JOptionPane;
import org.openide.loaders.DataObject;
import org.openide.util.Utilities;
public class ActionListenerSensitiveAction implements ActionListener {
private final DataObject context;
public ActionListenerSensitiveAction() {
context = Utilities.actionsGlobalContext().lookup(DataObject.class);
}
@Override
public void actionPerformed(ActionEvent ev) {
//Do something with the context:
JOptionPane.showMessageDialog(null, "ActionListener: " +
context.getNodeDelegate().getDisplayName());
}
}
Indeed, the classes, at this stage are the same. But, depending on what I want to do with TopComponents or ActionListeners, I now have a starting point, which includes access to the DataObject, from where I can get down into the source code, as shown here.
This is how the two ActionListeners that you see defined above are registered in the layer, which could ultimately be done via annotations on the ActionListeners, of course:
<folder name="Actions">
<folder name="Tools">
<file name="org-netbeans-sbas-impl-TopComponentSensitiveAction.instance">
<attr stringvalue="This is a TopComponent" name="displayName"/>
<attr name="instanceCreate"
methodvalue="org.netbeans.sbas.SuperclassSensitiveAction.create"/>
<attr name="type" stringvalue="org.openide.windows.TopComponent"/>
<attr name="delegate"
newvalue="org.netbeans.sbas.impl.TopComponentSensitiveAction"/>
</file>
<file name="org-netbeans-sbas-impl-ActionListenerSensitiveAction.instance">
<attr stringvalue="This is an ActionListener" name="displayName"/>
<attr name="instanceCreate"
methodvalue="org.netbeans.sbas.SuperclassSensitiveAction.create"/>
<attr name="type" stringvalue="java.awt.event.ActionListener"/>
<attr name="delegate"
newvalue="org.netbeans.sbas.impl.ActionListenerSensitiveAction"/>
</file>
</folder>
</folder>
<folder name="Loaders">
<folder name="text">
<folder name="x-java">
<folder name="Actions">
<file name="org-netbeans-sbas-impl-TopComponentSensitiveAction.shadow">
<attr name="originalFile"
stringvalue="Actions/Tools/org-netbeans-sbas-impl-TopComponentSensitiveAction.instance"/>
<attr intvalue="150" name="position"/>
</file>
<file name="org-netbeans-sbas-impl-ActionListenerSensitiveAction.shadow">
<attr name="originalFile"
stringvalue="Actions/Tools/org-netbeans-sbas-impl-ActionListenerSensitiveAction.instance"/>
<attr intvalue="160" name="position"/>
</file>
</folder>
</folder>
</folder>
</folder>
The most important parts of the layer registration are the lines that are highlighted above. Those lines connect the layer to the generic action that delegates back to the action listeners defined above, as follows:
public final class SuperclassSensitiveAction extends AbstractAction implements ContextAwareAction {
private final Map map;
//This method is called from the layer, via "instanceCreate",
//magically receiving a map, which contains all the attributes
//that are defined in the layer for the file:
static SuperclassSensitiveAction create(Map map) {
return new SuperclassSensitiveAction(Utilities.actionsGlobalContext(), map);
}
public SuperclassSensitiveAction(Lookup context, Map m) {
super(m.get("displayName").toString());
this.map = m;
String superclass = m.get("type").toString();
//Enable the menu item only if
//we're dealing with a class of type superclass:
JavaSource javaSource = JavaSource.forFileObject(
context.lookup(DataObject.class).getPrimaryFile());
try {
javaSource.runUserActionTask(new ScanTask(this, superclass), true);
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
//Hide the menu item if it isn't enabled:
putValue(DynamicMenuContent.HIDE_WHEN_DISABLED, true);
}
@Override
public void actionPerformed(ActionEvent ev) {
ActionListener delegatedAction = (ActionListener)map.get("delegate");
delegatedAction.actionPerformed(ev);
}
@Override
public Action createContextAwareInstance(Lookup actionContext) {
return new SuperclassSensitiveAction(actionContext, map);
}
private class ScanTask implements Task<CompilationController> {
private SuperclassSensitiveAction action = null;
private String superclass;
private ScanTask(SuperclassSensitiveAction action, String superclass) {
this.action = action;
this.superclass = superclass;
}
@Override
public void run(final CompilationController info) throws Exception {
info.toPhase(Phase.ELEMENTS_RESOLVED);
new EnableIfGivenSuperclassMatches(info, action, superclass).scan(
info.getCompilationUnit(), null);
}
}
private static class EnableIfGivenSuperclassMatches extends TreePathScanner<Void, Void> {
private CompilationInfo info;
private final AbstractAction action;
private final String superclassName;
public EnableIfGivenSuperclassMatches(CompilationInfo info,
AbstractAction action,
String superclassName) {
this.info = info;
this.action = action;
this.superclassName = superclassName;
}
@Override
public Void visitClass(ClassTree t, Void v) {
Element el = info.getTrees().getElement(getCurrentPath());
if (el != null) {
TypeElement te = (TypeElement) el;
List<? extends TypeMirror> interfaces = te.getInterfaces();
if (te.getSuperclass().toString().equals(superclassName)) {
action.setEnabled(true);
} else {
action.setEnabled(false);
}
for (TypeMirror typeMirror : interfaces) {
if (typeMirror.toString().equals(superclassName)){
action.setEnabled(true);
}
}
}
return null;
}
}
}
This is a pretty cool solution and, as you can see, very generic. Create a new ActionListener, register it in the layer so that it maps to the generic class above, and make sure to set the type attribute, which defines the superclass to which the action should be sensitive.