Action System Migration

Migration Guide

Before starting to migrate the Action implementation manually you can use a tool which will give you a starting point for the migration.
The class com.bc.ceres.nbmgen.NbCodeGenTool in snap-deploy will create a class template for each action implemented in BEAM for you. It compiles the information from the module.xml and the implementation class into the template.

This tool can have three parameters.

  1. The path to the project to process
  2. Switch to let the tool run in dry-mode
  3. The base output path. The structure of the input project is retained (optional)

BEAM actions extend the class ExecCommand. This is not possible in SNAP any more. Instead you simply extend AbstractAction or implement an ActionListener. When you need a simple action which has no complex enablement logic you can use the ActionListener option. For actions which shall only be enabled if a certain object is select you can put the object which needs to be selected as a parameter to the constructor.

public class MyAction extends AbstractAction {

    public MyAction(SelectedObject obj) {
    }
}

If you need a more fine grained context sensitivity or multiple objects shall be selected before the action is enabled you should implement the interface ContextAwareAction. A typical implementation would look like the following

@ActionRegistration(displayName = "Compare Bands", lazy = false)
@NbBundle.Messages({"CTL_CompareBandActionName=Compare Bands"})
public class CompareBandAction extends AbstractAction implements ContextAwareAction, LookupListener {
    private final Lookup lkp;
    private final Lookup.Result<Band> result;
    
    public CompareBandAction() {
        this(Utilities.actionsGlobalContext());
    }

    public CompareBandAction(Lookup lkp) {
        super(Bundle.CTL_CompareBandActionName());
        this.lkp = lkp;
        result = lkp.lookupResult(Band.class);
        result.addLookupListener(WeakListeners.create(LookupListener.class, this, result));
        setEnabled(false);
    }
    
    @Override
    public Action createContextAwareInstance(Lookup lkp) {
        return new CompareBandAction(lkp);
    }
    
    @Override
    public void actionPerformed(ActionEvent e) {
	    // Do the action stuff
    }

    @Override
    public void resultChanged(LookupEvent le) {
        setEnabled(result.allInstances().size() >= 2);
    }
}

Note

The Annotation @NbBundle.Messages only works if the package where the action is implemented is not yet defined in an other module.

Having the package  org.esa.snap.rcp.actions in snap-rcp and using the same package in a second module the Bundle key will not be found by NetBeans. Probably because it first finds the bundle property file of snap-rcp but the key for the action is not specified there.

 

HowTos

Defining a Group of Actions

In the Nodes API is a method available (getActions(boolean context)) in order to define the context menu. But providing here a defined set of actions is not good because it cant be extended by plugins. Instead it is better to define a new group.
For the context menu of bands a group named 'Context/Product/Band', or speaking in the System FileSystem language a new 'Folder', has been defined. This group can either be referenced from the System FileSystem or from the ActionReference annotation in the Java code.

ActionReference Example
...
@ActionReference(path = "Context/Product/Band", position = 100)
public class OpenImageViewAction extends AbstractAction {
...
}
System FileSystem Example
<folder name="Context">
    <folder name="Product">
        <folder name="Band">
            <file name="org-openide-actions-CopyAction.shadow">
                <attr name="originalFile" stringvalue="Actions/Edit/org-openide-actions-CopyAction.instance"/>
                <attr name="position" intvalue="505"/>
            </file>
		</folder>
    </folder>
</folder>

The PNode and PNNode classes use the org.esa.snap.rcp.nodes.PNNodeSupport#getContextActions(...) implementation. It loads  the actions of this group, either defined in the layer.xml or by an annotation by using the utilities method Utilities.actionsForPath(String).

Retrieve Context Menu Actions
@Override
public Action[] getActions(boolean context) {
    ArrayList<Action> actions = new ArrayList<>(Utilities.actionsForPath("Context/Product/Band"));
    return actions.toArray(new Action[actions.size()]);
}

Referencing System Actions in System FileSystem

In order to define System Actions in the layer.xml it is necessary to know their ID. The easiest way to find this is to use the NetBeans IDE. Within a NetBeans module expand in the Projects Explorer the 'Important Files' folder. There you can find the XML Layer (probably only if the module has already contribution to the System FileSystem. One of the elements '<this layer>' contains only the definitions of this module but the other 'this layer in context' contains also the inherited definition. In this file you can find the IDs and other settings of the system actions. For example the cut action:

Cut Action in System FileSystem
<folder name="Actions">
    <folder name="Edit">
        <attr name="SystemFileSystem.localizingBundle" stringvalue="org.netbeans.core.ui.resources.Bundle"/>
        <file name="org-openide-actions-CutAction.instance">
            <attr name="instanceCreate" methodvalue="org.openide.awt.Actions.callback"/>
            <attr name="key" stringvalue="cut-to-clipboard"/>
            <attr name="iconBase" stringvalue="org/openide/resources/actions/cut.gif"/>
            <attr name="displayName" bundlevalue="org.openide.actions.Bundle#Cut"/>
        </file>
    </folder>
</folder>

Retrieving a Single Action

There is a utility method for obtaining a single action by its ID. The method Action.forID(String category, String ID) can be used for example to get the preferred action of a Node.

NewAction and NewType API

The NewAction class, if added to e.g. the context menu of your Node, lets you add actions for creating sub-nodes.

Additionally to the NewAction in the context menu the getNewTypes() must be implemented. This method returns an array of NewType implementations. Each of them defines a way of creating a new sub-node. Actually everything can be done in the implementations but they are intended for adding new sub-nodes. Because of this intention an "Add " prefix is added to a single menu item text. If there are more then one NewType implementations they are grouped under a "Add" menu item.

For more details see Geertjan's Blog

Creating a Sub-Menu

In order to group action which belong together in a sub-menu you just need to adapt the @ActionReference annotation.

@ActionReference(path = "Menu/Tools/My Special Group", position = 100)

Each part of the path makes up a sub-menu. If it does does not already exist it is created. The action annotated with the afore mentioned annotation will be placed in a sub-menu with the label "My Special Group". The same applies for the layer.xml. If one or more actions are wrapped into a folder element a sub-menu is created.

 

Sub-Menu in layer.xml
<folder name="Menu">

    <folder name="Tools">
        <folder name="My Special Group">
            <file name="org-openide-actions-CutAction.shadow">
                <attr name="originalFile"
 stringvalue="Actions/Edit/org-openide-actions-CutAction.instance"/>
                <attr name="position" intvalue="510"/>
            </file>
        </folder>
    </folder>
</folder>

Unfortunately this seems not to work for context menus.
The only way to add sub-menus to the context menu of a node I have found in the NetBeans Forum. However the solution described there is more than 3 years old and not very NetBeanish.
I've send a question to the mailing list and waiting for an answer.

Resources