X

Geertjan's Blog

  • April 9, 2014

Context-Sensitive TopComponent (Part 1)

Geertjan Wielenga
Product Manager

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:

@TopComponent.Description(
        preferredID = "FlightLegEditorTopComponent",
        persistenceType = TopComponent.PERSISTENCE_ALWAYS
)
@TopComponent.Registration(
        mode = "editor",
        openAtStartup = false)
@ActionID(
        category = "Window",
        id = "org.cool.viewer.FlightLegEditorTopComponent")
@ActionReference(
        path = "Menu/Window")
@ObjectTopComponent.OpenActionForObjectRegistration(
        displayName = "#CTL_EditorAction",
        preferredID = "FlightLegEditorTopComponent"
)
@NbBundle.Messages({
    "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;
    }
    @Override
    public void actionPerformed(ActionEvent e) {
        setDisplayName(fl.getName());
        open();
        requestActive();
    }
}

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 {
    @Retention(RetentionPolicy.SOURCE)
    @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:

@SupportedSourceVersion(SourceVersion.RELEASE_6)
@ServiceProvider(service = Processor.class)
public final class ObjectTopComponentProcessor extends LayerGeneratingProcessor {
    public ObjectTopComponentProcessor() {
    }
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> hash = new HashSet<String>();
        hash.add(ObjectTopComponent.OpenActionForObjectRegistration.class.getCanonicalName());
        return hash;
    }
    @Override
    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() + "/" + aid.id().replace('.', '-') + ".instance").
                        methodvalue("instanceCreate", "org.openide.windows.TopComponent", "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());
                }
                actionFile.write();
            }
        }
        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)) {
                    continue;
                }
                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
            default:
                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");
            return;
        }
        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;
                break;
            case EXECUTABLE:
                type = ((DeclaredType) ((ExecutableType) e.asType()).getReturnType()).asElement();
                break;
            default:
                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.

Join the discussion

Comments ( 2 )
  • Dominik Cebula Friday, April 11, 2014

    Geertjan, thank you for this post, this exactly covers thing that I mentioned on the training :)


  • Geertjan Friday, April 11, 2014

    Great to hear.


Please enter your name.Please provide a valid email address.Please enter a comment.CAPTCHA challenge response provided was incorrect. Please try again.