Annotation Processor for Main Popup Menus

Continuing from yesterday, let's generalize things even further and create annotations for our first-level popup menus, including the path to the folder containing the second-level popups. In the end, we'll be able to register all our first-level popup menus like this:

public class ScenarioMainPopupMenus {

    @MainPopupMenu(path = "Scenario",
    displayName = "Edit",
    position = 10,
    pathToSubPopupMenus = "Actions/ScenarioEditSubActions")
    private final class EditMainPopupMenu {}

    @MainPopupMenu(path = "Scenario",
    displayName = "Analyze",
    position = 20,
    pathToSubPopupMenus = "Actions/ScenarioAnalyzeSubActions")
    private final class AnalyzeMainPopupMenu {}
 
}

When run, the above (assuming you have some Actions registered in 'pathToSubPopupMenus') results in this and notice that both Actions.alwaysEnabled and Actions.context are supported, though the latter is less likely to be needed because the Node's Lookup provides the context, which is normally a single object such as, in the case below, Scenario:

OK, to get to the above point, first create a new Annotation:

package org.scenario.utils;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface MainPopupMenuAnnotation {

    String path() default "Scenario/";

    String pathToSubPopupMenus() default "Actions/SubScenarioActions";

    int position() default Integer.MAX_VALUE;

    String displayName();

}

Next, we need an Annotation processor that will take the above Annotation and apply it to any class that makes use of it. In the end, the Annotation processor should register the class as Actions in the layer file. In that way, they can be displayed in a Node's popup via the Node's getActions:

package org.scenario.utils;

import java.util.Set;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import org.openide.filesystems.annotations.LayerBuilder.File;
import org.openide.filesystems.annotations.LayerGeneratingProcessor;
import org.openide.filesystems.annotations.LayerGenerationException;
import org.openide.util.lookup.ServiceProvider;

@ServiceProvider(service = Processor.class)
@SupportedAnnotationTypes("org.scenario.utils.MainPopupMenuAnnotation")
@SupportedSourceVersion(SourceVersion.RELEASE_6)
public class MainPopupMenuActionProcessor extends LayerGeneratingProcessor {

    @Override
    protected boolean handleProcess(Set<? extends TypeElement> set, RoundEnvironment env) throws LayerGenerationException {
        Elements elements = processingEnv.getElementUtils();
        for (Element e : env.getElementsAnnotatedWith(MainPopupMenuAnnotation.class)) {
            TypeElement clazz = (TypeElement) e;
            MainPopupMenuAnnotation mpm = clazz.getAnnotation(MainPopupMenuAnnotation.class);
            String teName = elements.getBinaryName(clazz).toString();
            File f = layer(e).file(
                    "Actions/" + mpm.path() + "/" + teName.replace('.', '-') + ".instance").
                    bundlevalue("displayName", mpm.displayName()).
                    stringvalue("pathToSubPopupMenus", mpm.pathToSubPopupMenus()).
                    methodvalue("instanceCreate", "org.scenario.utils.GenericNodePopupPresenter", "create").
                    intvalue("position", mpm.position());
            f.write();
        }
        return true;
    }
 
}

Notice that the 'methodValue' attribute for the Actions created in the layer will cause the attributes in the layer to be processed in this class:

package org.scenario.utils;

import java.awt.event.ActionEvent;
import java.util.Map;
import javax.swing.*;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.util.actions.Presenter;

public final class GenericNodePopupPresenter extends AbstractAction implements Presenter.Popup {

    private final Map map;

    static GenericNodePopupPresenter create(Map m) {
        return new GenericNodePopupPresenter(m);
    }

    public GenericNodePopupPresenter(Map m) {
        this.map = m;
    }

    @Override
    public JMenuItem getPopupPresenter() {
        String mainMenuDisplayName = map.get("displayName").toString();
        JMenu mainPopupMenu = new JMenu(mainMenuDisplayName);
        FileObject subs = FileUtil.getConfigFile(map.get("pathToSubPopupMenus").toString());
        if (subs != null) {
            for (FileObject oneSub : subs.getChildren()) {
                Action a = FileUtil.getConfigObject(oneSub.getPath(), Action.class);
                mainPopupMenu.add(new JMenuItem(a));
            }
        }
        return mainPopupMenu;
    }

    @Override
    public void actionPerformed(ActionEvent ev) {
        //nothing happens here
        //because the main popup menu
        //is only a container for a sub popup menu
        //and cannot itself perform an action
    }
 
}

Finally, when the Annotation processor has done its job (i.e., when you build the module), the Annotations at the start of this blog entry will create the following in the "generated-layer.xml" file, which is in the "build" folder of your module (visible in the Files window):

<folder name="Actions">
    <folder name="Scenario">
        <file name="no-offsim-scenario-viewer-general-ScenarioMainPopupMenus$EditMainPopupMenu.instance">
            <attr name="displayName" stringvalue="Edit"/>
            <attr name="pathToSubPopupMenus" stringvalue="Actions/ScenarioEditSubActions"/>
            <attr methodvalue="org.scenario.utils.GenericNodePopupPresenter.create" name="instanceCreate"/>
            <attr intvalue="10" name="position"/>
        </file>
        <file name="no-offsim-scenario-viewer-general-ScenarioMainPopupMenus$AnalyzeMainPopupMenu.instance">
            <attr name="displayName" stringvalue="Analyze"/>
            <attr name="pathToSubPopupMenus" stringvalue="Actions/ScenarioAnalyzeSubActions"/>
            <attr methodvalue="org.scenario.utils.GenericNodePopupPresenter.create" name="instanceCreate"/>
            <attr intvalue="20" name="position"/>
        </file>
    </folder>
</folder>

Best of all, you can now simply use the New Action wizard to create and register the Actions that should be displayed as second-level popup menus within the first-level popup menus defined above. Whether your Action is defined by Actions.alwaysEnabled or Actions.context, it will be displayed within the first-level popup for which it has been registered, i.e., it needs to be registered in the appropriate "pathToSubPopupMenus" folder.

Comments:

The value of MainPopupMenu.pathToSubPopupMenus should _not_ be something starting with the "Actions/" prefix. Actions/**/*.instance (the "action pool") is intended to hold registrations of actions, not references to them which should be made separately (zero, one, or more references per declared action). So for example:

@MainPopupMenu(pathToSubPopupMenus="Scenarios/Actions/Edit", ...)
// then elsewhere:
@ActionID(category="wherever", id="...")
@ActionRegistration(...)
@ActionReference(path="Scenarios/Actions/Edit", position=100)
public class SubmenuItem implements ActionListener {
SubmenuItem(ScenarioContextThingy context) {...}
...
}

This allows SubmenuItem to potentially be used in multiple places. It also makes it easy for a branding module to add or remove usages of a submenu item without touching its actual registration. Further note that a position attribute is never wanted inside the Actions/ area (it may in fact result in a runtime warning), only in a folder of action references.

Also the target of the main annotation, e.g. 'private final class EditMainPopupMenu {}', is confused. Why a private class with no members? If you are not registering something corresponding to actual Java code, which in this case I guess you are not, it should go in package-info.java.

Proper style would really be for @MainPopupMenu to be a direct replacement for @ActionRegistration, meaning that it would have to be accompanied by an @ActionID, and would normally be accompanied also by an @ActionReference to actually add the popup menu to some other action folder. Unfortunately at the moment @ActionID and @ActionReference cannot be used on package elements, which I guess is an API defect. A workaround would be for @MainPopupMenu to be registered on, say, fields of Void type so that it is clear the annotated element is merely a placeholder:

@ActionID(...)
@MainPopupMenu(...)
@ActionReference(...)
private static final Void SOME_MENU = null;

Posted by Jesse Glick on November 10, 2011 at 12:27 AM PST #

Post a Comment:
  • HTML Syntax: NOT allowed
About

Geertjan Wielenga (@geertjanw) is a Principal Product Manager in the Oracle Developer Tools group living & working in Amsterdam. He is a Java technology enthusiast, evangelist, trainer, speaker, and writer. He blogs here daily.

The focus of this blog is mostly on NetBeans (a development tool primarily for Java programmers), with an occasional reference to NetBeans, and sometimes diverging to topics relating to NetBeans. And then there are days when NetBeans is mentioned, just for a change.

Search

Archives
« April 2014
SunMonTueWedThuFriSat
  
12
13
14
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
Today