How to Centralize the Management of NetBeans Platform Actions (Part 1)

When you use the Window wizard in the IDE to create a new TopComponent, you'll find an Action that looks something like this, registered in your layer.xml file, without any Java source file for the Action class being created in your module:
<folder name="Actions">
    <folder name="Window">
        <file name="org-netbeans-feedreader-FeedAction.instance">
            <attr name="instanceCreate" methodvalue="org.openide.windows.TopComponent.openAction"/>
            <attr name="component" methodvalue="org.netbeans.feedreader.FeedTopComponent.getDefault"/>
            <attr name="displayName" bundlevalue="org.netbeans.feedreader.Bundle#CTL_FeedAction"/>
            <attr name="iconBase" stringvalue="org/netbeans/feedreader/rss16.gif"/>
        </file>
    </folder>
</folder>

What's the above all about?

The starting point for understanding the above is, as always, the NetBeans API Javadoc. In the snippet above, you can see that the above Action will create an instance of "openAction", which is defined in the TopComponent class. Here's the Javadoc for the "openAction":

http://bits.netbeans.org/dev/javadoc/org-openide-windows/org/openide/windows/TopComponent.html#openAction(org.openide.windows.TopComponent,%20java.lang.String,%20java.lang.String,%20boolean)

So, the TopComponent class has, within its own definition, an openAction method that requires certain values. Those values are set in the layer file. Via an internal class (XMLMapAttr.java) the connection between the layer file and the Action is established.

So, let's now make our own Action and use it from the layer, in the same way as is done by default for the "openAction" in the TopComponent class.

  1. We'll start with the Feed Reader sample that is part of your NetBeans IDE distribution. Go to the Samples category in the New Project wizard and create the Feed Reader sample that you'll find there.

  2. Now we'll add a new module, which will provide API classes for the application. Name the module FeedReaderAPI, with code name base "org.feedreader.api". Set dependencies on Lookup, UI Utilities API, Utilities API, and Window System API.

  3. Use the "New Java Class" template to create a class named "Utils". In there, define a new "closeAction", which I simply copied and then modified from the "openAction" in the TopComponent definition:
    public class Utils {
    
        static Action closeAction(Map map) {
            return Actions.alwaysEnabled(
                    new CloseComponentAction(map),
                    (String) map.get("displayName"), // NOI18N
                    (String) map.get("iconBase"), // NOI18N
                    Boolean.TRUE.equals(map.get("noIconInMenu")) // NOI18N
                    );
        }
    
        private static class CloseComponentAction implements ActionListener {
    
            private TopComponent component;
            private final Map map;
    
            CloseComponentAction(TopComponent component) {
                assert component != null; //to diagnose #185355
                this.component = component;
                map = null;
            }
    
            CloseComponentAction(Map map) {
                this.map = map;
            }
    
            private TopComponent getTopComponent() {
                assert EventQueue.isDispatchThread();
                if (component != null) {
                    return component;
                }
                component = (TopComponent) map.get("component"); // NOI18N
                assert component != null : "Component cannot be created for " + map;
                return component;
            }
    
            @Override
            public void actionPerformed(ActionEvent e) {
                TopComponent win = getTopComponent();
                win.close();
            }
    
        }
    
    }

    See part 2 or read Jesse's critique in the comments to this blog entry: The code above is too complicated and could be simplified very easily.

    What is the "map" referred to above? You don't need to think about that, other than to know that those are the attribute values provided by the layer (in the next step), since the internal XMLMapAttr class handles the actual mapping for you. So that's why there's no place where I am (in my own code in this blog entry) using FileUtil.getConfigFile, or anything like that, since the XMLMapAttr does that under the hood, the connection being established via the "instanceCreate" attribute in the layer (as shown in the next step).

    Instead of a Utils class, you could create a FeedReaderBaseTopComponent, with a number of Actions. I.e., this FeedReaderBaseTopComponent would extend TopComponent and provide a set of Actions (and other generic features) that other TopComponents within the FeedReader application could reuse.

  4. Now open the "FeedReader" module. In the layer.xml file, register a new Action, pointing to the above "closeAction":
    <folder name="Actions">
        <folder name="Window">
            <file name="org-netbeans-feedreader-FeedAction2.instance">
                <attr name="instanceCreate" methodvalue="org.feedreader.api.Utils.closeAction"/>
                <attr name="component" methodvalue="org.netbeans.feedreader.FeedTopComponent.getDefault"/>
                <attr name="displayName" bundlevalue="org.netbeans.feedreader.Bundle#CTL_CloseFeedAction"/>
                <attr name="iconBase" stringvalue="org/netbeans/feedreader/rss16.gif"/>
            </file>
        </folder>
    </folder>

What's the benefit of all this? Well, you can, optionally, define Actions generically (e.g., "openAction" and "closeAction"), after which any module can pass any values they want to those generic Actions, from any module in the application. This approach lets you centrally manage the Actions of an application, providing consistency to the application and simplicity to its contributors.

Comments:

Thanks for this, I had pieced together that this was how the xml mechanism worked, but it's nice to read it explicitly.

Slightly orthogonal to this article, but would you consider an article showing in detail how to create context sensitive actions that use a specific lookup (not the Utilities.actionsGlobalContext()) but are still statically registered in xml? e.g. I want an action that's disabled/enabled based on the lookup of one specific TopComponent, not the currently active window.

Posted by Nick Dunn on June 28, 2010 at 10:20 PM PDT #

Hi Nick, thanks for the support. About your further question, which part of it is not already discussed here?:

http://netbeans.dzone.com/how-to-make-context-sensitive-actions

Posted by Geertjan Wielenga on June 28, 2010 at 10:30 PM PDT #

Your example is too complicated. There is no reason to ever call Actions.alwaysEnabled from Java code; it exists solely as a placeholder for Javadoc (since the variant which is actually used, taking Map, is not public). Just make Utils.closeAction return an Action impl with the right options. There is no need for a component field, nor for a constructor taking one. In short, all you really need is:

public static Action closeAction(final Map<String,?> params) {
return new AbstractAction() {
{
putValue(NAME, params.get("displayName"));
// optional (handled by Actions.connect):
putValue("iconBase", params.get("iconBase"));
putValue("noIconInMenu", params.get("noIconInMenu"));
}
public @Override void actionPerformed(ActionEvent e) {
((TopComponent) map.get("component")).close();
}
};
}

Posted by Jesse Glick on June 28, 2010 at 11:07 PM PDT #

Is this related to actions only or can be used by any \*.instance file? If so, it is an even more powerful tool!

Posted by Eduardo Costa on June 29, 2010 at 05:45 AM PDT #

Thanks for these helpful postings.

I would like to echo Nick Dunn's question. I have probably misunderstood but in the dzone article and other examples the context seems to come by default from the Utilities.actionsGlobalContext(). It is not clear what the best practice method is for using a different Lookup to provide the context.

Posted by Andrew Perry on June 29, 2010 at 11:10 AM PDT #

Thanks Jesse for the corrections. Others, hope to get to your questions, but would recommend you ask them on the dev mailing list instead.

Posted by Geertjan Wielenga on June 30, 2010 at 06:48 AM PDT #

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