Wednesday Apr 09, 2014

Context-Sensitive TopComponent (Part 1)

I picked up a cool idea from a Polish developer, Dominik Cebula, recently. In the same way that the NetBeans Platform has context sensitive Actions, there should be context sensitive TopComponents.

Only if an Object of a specific type specified in the public constructor of the TopComponent is found in the Lookup should it be possible to open the TopComponent. And then the Object is available to the TopComponent, without the TopComponent needing to implement a LookupListener.

For example, below "FlightLeg Editor" and "Delay Editor" are both disabled, because no "FlightLeg" and no "Delay" is in the Lookup. Hence it doesn't make sense to open the editor, i.e., when the Object for which the editor exists is not available.

On the other hand, below a "FlightLeg" is available in the Lookup, because one of the flight legs has been selected and hence the underlying "FlightLeg" object is now in the Lookup. Therefore, the "FlightLeg Editor" menu item is enabled so that an editor can be opened for editing the selected flight leg:

In the same way, here the "Delay Editor" can be opened, because an Object of the type "Delay" is published when a DelayNode is selected:

Here is one of these TopComponents:

        preferredID = "FlightLegEditorTopComponent",
        persistenceType = TopComponent.PERSISTENCE_ALWAYS
        mode = "editor", 
        openAtStartup = false)
        category = "Window", 
        id = "")
        path = "Menu/Window")
        displayName = "#CTL_EditorAction",
        preferredID = "FlightLegEditorTopComponent"
    "CTL_EditorAction=FlightLeg Editor",
public class FlightLegEditorTopComponent extends ObjectTopComponent implements ActionListener{
    private FlightLeg fl;
    //no-arg constructor is required:
    private FlightLegEditorTopComponent() {}
    public FlightLegEditorTopComponent(FlightLeg fl) {
        this.fl = fl;
    public void actionPerformed(ActionEvent e) {

In the above, notice there is a public constructor that receives the domain object "FlightLeg" (i.e., for an airline-type application). Also, there's a new annotation up there, "@ObjectTopComponent.OpenActionForObjectRegistration".

Here's what that annotation looks like (copied and then simply renamed from "@TopComponent.OpenActionRegistration"):

public class ObjectTopComponent extends TopComponent {
    @Target({ ElementType.TYPE, ElementType.METHOD })
    public static @interface OpenActionForObjectRegistration {
        String displayName();
        String preferredID() default "";

However, the annotation above is processed in such a way that a context-sensitive Action is created that uses the type in the constructor of the TopComponent:

@ServiceProvider(service = Processor.class)
public final class ObjectTopComponentProcessor extends LayerGeneratingProcessor {
    public ObjectTopComponentProcessor() {
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> hash = new HashSet<String>();
        return hash;
    protected boolean handleProcess(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) throws LayerGenerationException {
        for (Element e : roundEnv.getElementsAnnotatedWith(ObjectTopComponent.OpenActionForObjectRegistration.class)) {
            ObjectTopComponent.OpenActionForObjectRegistration reg = e.getAnnotation(ObjectTopComponent.OpenActionForObjectRegistration.class);
            assert reg != null;
            Description info = findInfo(e);
            ActionID aid = e.getAnnotation(ActionID.class);
            if (aid != null) {
                File actionFile = layer(e).
                        file("Actions/" + aid.category() + "/" +'.', '-') + ".instance").
                        methodvalue("instanceCreate", "", "openAction");
                actionFile.instanceAttribute("component", TopComponent.class, reg, null);
                if (reg.preferredID().length() > 0) {
                    actionFile.stringvalue("preferredID", reg.preferredID());
                generateContext(e, actionFile);
                actionFile.bundlevalue("displayName", reg.displayName(), reg, "displayName");
                if (info != null && info.iconBase().length() > 0) {
                    actionFile.stringvalue("iconBase", info.iconBase());
        return true;
    private void generateContext(Element e, File f) throws LayerGenerationException {
        ExecutableElement ee = null;
        ExecutableElement candidate = null;
        for (ExecutableElement element : ElementFilter.constructorsIn(e.getEnclosedElements())) {
            if (element.getKind() == ElementKind.CONSTRUCTOR) {
                candidate = element;
                if (!element.getModifiers().contains(Modifier.PUBLIC)) {
                if (ee != null) {
                    throw new LayerGenerationException("Only one public constructor allowed", e, processingEnv, null); // NOI18N
                ee = element;
        if (ee == null || ee.getParameters().size() != 1) {
            if (candidate != null) {
                throw new LayerGenerationException("Constructor has to be public with one argument", candidate);
            throw new LayerGenerationException("Constructor must have one argument", ee);
        VariableElement ve = (VariableElement) ee.getParameters().get(0);
        TypeMirror ctorType = ve.asType();
        switch (ctorType.getKind()) {
            case ARRAY:
                String elemType = ((ArrayType) ctorType).getComponentType().toString();
                throw new LayerGenerationException("Use List<" + elemType + "> rather than " + elemType + "[] in constructor", e, processingEnv, null);
            case DECLARED:
                break; // good
                throw new LayerGenerationException("Must use SomeType (or List<SomeType>) in constructor, not " + ctorType.getKind());
        DeclaredType dt = (DeclaredType) ctorType;
        String dtName = processingEnv.getElementUtils().getBinaryName((TypeElement) dt.asElement()).toString();
        if ("java.util.List".equals(dtName)) {
            if (dt.getTypeArguments().isEmpty()) {
                throw new LayerGenerationException("Use List<SomeType>", ee);
            f.stringvalue("type", binaryName(dt.getTypeArguments().get(0)));
            f.methodvalue("delegate", "org.openide.awt.Actions", "inject");
            f.stringvalue("injectable", processingEnv.getElementUtils().getBinaryName((TypeElement) e).toString());
            f.stringvalue("selectionType", "ANY");
            f.methodvalue("instanceCreate", "org.openide.awt.Actions", "context");
        if (!dt.getTypeArguments().isEmpty()) {
            throw new LayerGenerationException("No type parameters allowed in ", ee);
        f.stringvalue("type", binaryName(ctorType));
        f.methodvalue("delegate", "org.openide.awt.Actions", "inject");
        f.stringvalue("injectable", processingEnv.getElementUtils().getBinaryName((TypeElement) e).toString());
        f.stringvalue("selectionType", "EXACTLY_ONE");
        f.methodvalue("instanceCreate", "org.openide.awt.Actions", "context");
    private String binaryName(TypeMirror t) {
        Element e = processingEnv.getTypeUtils().asElement(t);
        if (e != null && (e.getKind().isClass() || e.getKind().isInterface())) {
            return processingEnv.getElementUtils().getBinaryName((TypeElement) e).toString();
        } else {
            return t.toString(); // fallback - might not always be right
    private Description findInfo(Element e) throws LayerGenerationException {
        Element type;
        switch (e.asType().getKind()) {
            case DECLARED:
                type = e;
            case EXECUTABLE:
                type = ((DeclaredType) ((ExecutableType) e.asType()).getReturnType()).asElement();
                throw new LayerGenerationException("" + e.asType().getKind(), e);
        TopComponent.Description info = type.getAnnotation(TopComponent.Description.class);
        return info;

The above processor is a combination of the TopComponentProcessor and the ActionProcessor in the NetBeans Platform.

And now you have a layer generating processor that creates a context sensitive Action for opening a TopComponent. If no Object of the type specified in the constructor of the TopComponent is in the Lookup, the Action will be disabled. If an Object of the specified type is available, the Action is enabled and immediately available to the TopComponent as soon as it is opened.

That's an example of a view that is bound to a model. Useful for editors that need to be created for one or more specific Objects. Data binding for TopComponents, hurray.


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.


« April 2014 »