From ActionListener to CallableSystemAction

In 6.5, when you create an "Always Enabled" action, the "New Action" wizard gives you this Java code:
package org.demo.module;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public final class SomeAction implements ActionListener {

    public void actionPerformed(ActionEvent e) {
        // TODO implement action body
    }
}

Very nice... because you're using plain old Java code. The ActionListener is one of the JDK's classes, unlike the Action classes that the NetBeans Platform provides. In previous releases, you would get a CallableSystemAction instead, which is one of those NetBeans Platform classes.

To make it possible for the NetBeans Platform to hook the above class into the NetBeans action system, the following entries are added to the layer.xml file, by the same "New Action" wizard that created the above Java class:



<filesystem>
    <folder name="Actions">
        <folder name="Build">
            <file name="org-demo-module-SomeAction.instance">
                <attr name="SystemFileSystem.localizingBundle" stringvalue="org.demo.module.Bundle"/>
                <attr name="delegate" newvalue="org.demo.module.SomeAction"/>
                <attr name="displayName" bundlevalue="org.demo.module.Bundle#CTL_SomeAction"/>
                <attr name="instanceCreate" methodvalue="org.openide.awt.Actions.alwaysEnabled"/>
                <attr name="noIconInMenu" boolvalue="false"/>
            </file>
        </folder>
    </folder>
    <folder name="Menu">
        <folder name="File">
            <file name="org-demo-module-SomeAction.shadow">
                <attr name="originalFile" stringvalue="Actions/Build/org-demo-module-SomeAction.instance"/>
                <attr name="position" intvalue="0"/>
            </file>
        </folder>
    </folder>
</filesystem>

Take careful note of the section that is in bold above! There you can see how the ActionListener implementation is hooked into the NetBeans action system. For example, the "instanceCreate" attribute causes a NetBeans Platform action to be created for your ActionListener! So, even though you're using plain old Java, the entries above (generated automatically by the wizard) cause the ActionListener to be treated as a NetBeans Platform class.

However... the NetBeans Platform classes are much more powerful than the ActionListener class. For example, if you use the CallableSystemAction class, you can (in bad pseudo code, any way) do something like this:

@Override
public void setEnabled(boolean enabled) {
    if (hell freezes over){
        enable this menu item
    } else {
        not
    }
}

In other words, the NetBeans Platform action classes provide built-in support for enabling/disabling themselves. Therefore, you're quite likely to want to 'upgrade' your ActionListener to a CallableSystemAction. So, what must one do to transform one's ActionListener into a CallableSystemAction? First, change the class signature so that you're extending "CallableSystemAction", instead of implementing "ActionListener". The Utilities API, which provides the "CallableSystemAction" class, is already set as one of the module's dependencies when you complete the "New Action" wizard, so there are no additional dependencies you need to set. You'll find you need to do something with a bunch of methods that come from the CallableSystemAction class and/or its ancestors.

Next... you need to tweak the layer.xml file so that the bits in bold above are removed. You'll be left with this:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE filesystem PUBLIC "-//NetBeans//DTD Filesystem 1.2//EN" "http://www.netbeans.org/dtds/filesystem-1_2.dtd">
<filesystem>
    <folder name="Actions">
        <folder name="Build">
            <file name="org-demo-module-SomeAction.instance"/>
        </folder>
    </folder>
    <folder name="Menu">
        <folder name="File">
            <file name="org-demo-module-SomeAction.shadow">
                <attr name="originalFile" stringvalue="Actions/Build/org-demo-module-SomeAction.instance"/>
                <attr name="position" intvalue="0"/>
            </file>
        </folder>
    </folder>
</filesystem>

Or, if you have the display name in the localizing bundle, you might have this instead:

    <folder name="Actions">
        <folder name="Build">
            <file name="org-demo-module-SomeAction.instance">
                <attr name="SystemFileSystem.localizingBundle" stringvalue="org.demo.module.Bundle"/>
                <attr name="displayName" bundlevalue="org.demo.module.Bundle#CTL_SomeAction"/>
            </file>
        </folder>
    </folder>

And now install your module. Your ActionListener is now a CallableSystemAction.

Comments:

Switching to CallableSystemAction is bad advice. To make enablement dynamic, just implement javax.swing.Action rather than only ActionListener. You can call setEnabled, but it is better style to override isEnabled and fire property changes when needed. (Overriding setEnabled makes no sense at all.)

If you need the ability to make the action disabled before it is ever used, then you need to use the old style of registration, in which case the action is loaded during platform startup. In most cases it would suffice for dynamic enablement checks to occur only after the menu item is invoked at least once, which minimizes startup overhead. In this case, leave the layer untouched (i.e. use Actions.alwaysEnabled) and just implement Action.

Anyway, if you are using old-style registration (whether of a CallableSystemAction or a general Action), SystemFileSystem.localizingBundle and displayName attributes would be ignored and should be deleted. Just the plain instance file suffices to load the object; all display information is taken from Java methods.

Posted by Jesse Glick on December 02, 2008 at 01:31 AM PST #

So CallableSystemAction should never be used?

Posted by Geertjan Wielenga on December 02, 2008 at 01:35 AM PST #

Right, I don't think there is any remaining reason to use CallableSystemAction. We are still working on a convenient replacement for CookieAction (see issue #153442). Eventually we want to deprecate SystemAction subclasses generally.

Posted by Jesse Glick on December 02, 2008 at 01:42 AM PST #

Very cool.

Posted by Geertjan Wielenga on December 02, 2008 at 01:56 AM PST #

What then is the replacement of SystemActions.get(FooAction.class) as a type-safe way to get an instance of a given action class (in opposite to use Lookup.forPath("..."))

An what is here referred to as old-style/new-style of registration?

Posted by Frank-Michael Moser on December 18, 2008 at 12:06 AM PST #

There is no type-safe way to get an instance of an action class except SystemAction.get. However, it is unusual for new code to need to explicitly retrieve an action singleton.

Do not do so just to update action state; the action should instead listen to a model object which enables communication among subsystems.

Code just needing to create a context menu should define the menu items in a layer and then use Lookups.forPath; it need not know anything about the resulting menu items (indeed some may be contributed by other modules). Even if you do want to explicitly create a certain kind of (non-SystemAction) action for a context menu, there is no need to obtain a singleton instance; just create a fresh instance, which will anyway normally be a ContextAwareAction.

Posted by Jesse Glick on December 18, 2008 at 12:28 AM PST #

I liked the singleton instance for actions with complex listener code, and I prefer the type-safe way, as refactoring does not go well together with Lookup.forPath(...). So I guess that I will end up with utility methods mediating between the "SystemAction.get" and "Lookup.forPath" approaches.

However, you might also want to throw a word on the second part of my question about old-style vs. new-style of registration.

Posted by Frank-Michael Moser on December 18, 2008 at 06:39 PM PST #

Thanks for the posts. If I understand correctly,CallableSystemAction is being deprecated. So I attempted creating a "LogoutAction" extending AbstractAction and having the "attr" values set for the action in layer.xml. Works fine -except I would like the menu item to be initially disabled.

It functions as expected (initially disabled) if I remove all the "attr" values for the action in layer.xml, but then I also lose the the icons associated with the action. As soon as I add back an attr, I revert again to initially enabled.

Anyone know how I should associate icons and name to an action based on an AbstractAction? Perhaps this is a case where I really should continue on with CallableSystemAction?

Thanks

Posted by Glenn Heinze on January 04, 2009 at 08:24 AM PST #

@Glenn Heinze:

Yes this is really a problem with the wrapper created by allwaysEnabled. The wrapper is an internal action with the state isEnabled == true. If netbeans shows this (wrapper) action your real delegate action is not created. Only after the first actionPerformed, the wrapper creates e real instance and delegates the isEnabled state. So it's impossible to create a Action with <attr name="instanceCreate" methodvalue="org.openide.awt.Actions.alwaysEnabled"/> and a first disabled state...

br, josh.

Posted by Aljoscha Rittner on July 22, 2009 at 07:32 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
« March 2015
SunMonTueWedThuFriSat
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
    
       
Today