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

Let's say we have a general TopComponent, which we want to share between multiple different modules. I.e., each module will provide its own Action. When the Action is invoked, e.g., from a menu item, a JPanel provided by the module that provides the Action will be added to the TopComponent, once the previous content in the TopComponent has been removed.

Creating a generic Action providing the above functionality, following Jesse's corrections in the comment to yesterday's blog entry, you would do the following:

public class Utils {

    public static Action addPanelAction(final Map<String, ?> params) {
        return new AddPanelAction(params);
    }

    private static class AddPanelAction extends AbstractAction {

        Map<String, ?> params;

        public AddPanelAction(Map<String, ?> params) {
            this.params = params;
            putValue(NAME, params.get("displayName"));
            putValue("iconBase", params.get("iconBase"));
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            JPanel panel = (JPanel) params.get("panel");
            GeneralTopComponent tc = GeneralTopComponent.findInstance();
            tc.removeAll();
            tc.add(panel, BorderLayout.CENTER);
            tc.open();
            tc.requestActive();
        }

    }

}

So, we need to send a map of parameters to the above Action. In the map, there'll be various values, including the JPanel that should be added to the TopComponent.

And this is how the map of parameters is defined, i.e., in the layer of the modules that want to integrate with the above generic Action:

<folder name="Actions">
    <folder name="Window">
        <file name="org-netbeans-feedreader-NameAndAddress.instance">
            <attr name="instanceCreate" methodvalue="org.feedreader.api.Utils.addPanelAction"/>
            <attr name="panel" newvalue="org.netbeans.feedreader.NameAndAddressPanel"/>
            <attr name="displayName" bundlevalue="org.netbeans.feedreader.Bundle#CTL_NameAndAddressPanel"/>
            <attr name="iconBase" stringvalue="org/netbeans/feedreader/NameAndAddressPanel.gif"/>
        </file>
        <file name="org-netbeans-feedreader-CityAndState.instance">
            <attr name="instanceCreate" methodvalue="org.feedreader.api.Utils.addPanelAction"/>
            <attr name="panel" newvalue="org.netbeans.feedreader.CityAndStatePanel"/>
            <attr name="displayName" bundlevalue="org.netbeans.feedreader.Bundle#CTL_CityAndStatePanel"/>
            <attr name="iconBase" stringvalue="org/netbeans/feedreader/CityAndStatePanel.gif"/>
        </file>
    </folder>
</folder>

In the above case, we have one module that provides two Actions. Each Action defines the parameters that the generic Action requires. One of these parameters is "panel". Note that here we use "newvalue" instead of "methodvalue", which we used yesterday. Yesterday, we wanted a specific method to be invoked in a TopComponent; today we want to instantiate the JPanel, i.e., call its constructor. That's why we use "newvalue", to create a new instance of the JPanel.

That's all. Now we can register the above Actions in one of the other folders, such as "Menu", so that they can be invoked from the menubar.

Comments:

You need only cast to Component, not JPanel specifically.

It is interesting to note that in a setup like this, NameAndAddressPanel (or whatever) is not instantiated, nor is its class loaded, unless and until the action is called. That is because the params Map is a special lazy implementation that only reads file attributes when asked, and file objects coming from a layer do not load newvalue/methodvalue values until asked. This is a good thing, because it means that you do not need to worry about creating potentially expensive Swing panels when they are not needed. The only thing that is created eagerly (i.e. during menu initialization) would be the two AddPanelAction instances.

It is actually possible to avoid loading even the AddPanelAction class unless and until one of these actions is invoked. You can use the usual Actions.alwaysEnabled factory from the layer, and your own factory method returning ActionListener from the delegate attribute; your factory would still take a params Map but only interpret the panel key, as the others are handled by alwaysEnabled.

One possible catch with the panel attribute in your example: the layer file object will in general cache attributes once loaded (a GC might clear the cache if I recall correctly), so successive calls to the same AddPanelAction may run with the same JPanel. In this case that is not a concern since there is only one GeneralTopComponent. If however you were displaying a new TopComponent each time the action were called, you would get an error calling it twice in a row since a component may be added to only one container at a time. The solution to this is straightforward: make the panel parameter be a factory for a component rather than the component itself.

Example using all these hints:

<folder name="Actions">
<folder name="Window">
<file name="gui1.instance">
<attr name="instanceCreate" methodvalue="org.openide.awt.Actions.alwaysEnabled"/>
<attr name="displayName" bundlevalue="testactionfactories.Bundle#label1"/>
<attr name="noIconInMenu" boolvalue="true"/>
<attr name="delegate" methodvalue="testactionfactories.AddPanelAction.create"/>
<attr name="panel" methodvalue="testactionfactories.GUI.create1"/>
</file>
<!-- for others, just change panel attr -->
</folder>
</folder>
public class AddPanelAction implements ActionListener {
private final Map<String,?> params;
private AddPanelAction(Map<String,?> params) {
this.params = params;
}
public static ActionListener create(Map<String,?> params) {
return new AddPanelAction(params);
}
public @Override void actionPerformed(ActionEvent e) {
Component panel;
try {
panel = (Component) ((Callable<?>) params.get("panel")).call();
} catch (Exception ex) {
Exceptions.printStackTrace(ex);
return;
}
GeneralTopComponent tc = GeneralTopComponent.findInstance();
tc.removeAll();
tc.add(panel, BorderLayout.CENTER);
tc.open();
tc.requestActive();
}
}
public class GUI {
public static Callable<Component> create1() {
return new Callable<Component>() {
public @Override Component call() throws Exception {
return new JLabel("Hello #1");
}
};
}
// etc.
}

Posted by Jesse Glick on June 30, 2010 at 08:44 AM PDT #

Hi Jesse. Sorry the indentation of your code snippet got mangled again. Why not post into your own blog instead, which would give you more control over content? The info you provide is too valuable to be this mangled...

Posted by Geertjan Wielenga on June 30, 2010 at 02:20 PM 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
17
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
Today