Platinum Partner
netbeans

How to Make the NetBeans Platform Sensitive to Customers

In your NetBeans Platform application, you may often find yourself in the situation where you need to create actions that are sensitive to their context. A simple case in point is shown below. We have a customer application with an action invoked from the toolbar, as well as from a menu item in the main menubar and from a node in an explorer view. In the screenshot below, the action is invoked from the button with the angry face (a typical customer expression) in the toolbar, from the "Customer Details" menu item on the node in the view window, as well as from a menu item in the File menu (which you can't see below). In this case, there is a Customer object on which the action can be invoked...

...while in this situation, the UI for invoking the action is disabled (as you can see, by looking at the button in the toolbar, which is disabled, and notice that other actions are enabled in this context, since those actions relate to the editor, which is the window that is currently selected) because here we no longer have a Customer object in the context of the application, since the cursor is currently in the editor window, instead of in the view window that exposes the Customer object from the currently selected node:

So the question in this article is how to create an action such as the above. The good news is that it's really easy to do so, but you do need to be aware of the steps you need to take, which is why I am writing this article.

Take the following steps:

  1. In the New Action wizard, in the module where you want to create your action, specify that you want to create a contextually aware action that should be sensitive to Customer objects, which is part of the model in my application:



    Note: We (i.e., the NetBeans team) need to change the strings in the dialog above (i.e., in the NetBeans IDE source code), since in both cases an ActionListener will be created. I.e., when you select the "Conditionally Enabled" radiobutton, you will not get a CookieAction. Instead, you will get an ActionListener that is registered in the layer such that it is injected with context-sensitivity to Customer objects, as will be seen below.

  2. Click Next and then specify that you want to create a menu item and a toolbar button:


  3. Click Next again and choose an icon on disk, as well as specifying a class name prefix and a display name:



    Tip: Read this cool tip about icons and NetBeans Platform applications!

  4. When you complete the above wizard, you will see you have a plain old ActionListener class, which is great news since this means you can port your own ActionListeners from your own application to the NetBeans Platform without needing to rewrite them in any way. In other words, the NetBeans Platform handles ActionListeners natively and does not require you to use some special NetBeans API for creating actions. Here's the ActionListener created from the above, which has access to the current Customer object (pretty handy!):

    package org.shop.ui;

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

    public final class CustomerDetailsAction implements ActionListener {

        private final Customer context;

        public CustomerDetailsAction(Customer context) {
            this.context = context;
        }

        public void actionPerformed(ActionEvent ev) {
            // TODO use context
        }

    }

    Meanwhile, your layer.xml file has the following entries, created by the above wizard, turning your humble ActionListener into a context-sensitive action that is sensitive to Customer objects:

    <folder name="Actions">
        <folder name="Build">
            <file name="org-shop-ui-CustomerDetailsAction.instance">
                <attr name="delegate" methodvalue="org.openide.awt.Actions.inject"/>
                <attr name="displayName" bundlevalue="org.shop.ui.Bundle#CTL_CustomerDetailsAction"/>
                <attr name="iconBase" stringvalue="org/shop/ui/customer.png"/>
                <attr name="injectable" stringvalue="org.shop.ui.CustomerDetailsAction"/>
                <attr name="instanceCreate" methodvalue="org.openide.awt.Actions.context"/>
                <attr name="noIconInMenu" boolvalue="false"/>
                <attr name="selectionType" stringvalue="EXACTLY_ONE"/>
                <attr name="type" stringvalue="demo.Customer"/>
            </file>
        </folder>
    </folder>
    <folder name="Menu">
        <folder name="File">
            <file name="org-shop-ui-CustomerDetailsAction.shadow">
                <attr name="originalFile" stringvalue="Actions/Build/org-shop-ui-CustomerDetailsAction.instance"/>
                <attr name="position" intvalue="1300"/>
            </file>
        </folder>
    </folder>
    <folder name="Toolbars">
        <folder name="File">
            <file name="org-shop-ui-CustomerDetailsAction.shadow">
                <attr name="originalFile" stringvalue="Actions/Build/org-shop-ui-CustomerDetailsAction.instance"/>
            </file>
        </folder>
    </folder>

    Note: I tweaked line 11 above. By default, the type is set to "Customer", while it actually needs to be the fully-qualified name of the Customer class, which is "demo.Customer", since the Customer object is found in a package called "demo". Maybe in the New Action wizard, one should type the fully-qualified name, rather than just the name of the domain class. Need to check that. The "type" element in the layer determines the context available to the ActionListener, i.e., the currently available Customer object.

    By the way, above, in line 10, we are ensuring that the action will be disabled if more than one node is selected (thanks to choosing "User Selects One Node" in the first page of the New Action wizard), as can be seen here:


  5. Finally, let's add the action as a contextual menu item on our node. In this case, I know I have two actions in the "Actions/Build" folder, one from the current module, and the other from another one. You could also get all the actions within a particular folder, rather than specific ones, or you could get actions from different folders. Up to you.
    private class CustomerNode extends AbstractNode {

        public CustomerNode(Customer c) {
            super(Children.LEAF, Lookups.singleton(c));
            setDisplayName(c.getName());
            setShortDescription(c.getCity());
        }

        @Override
        public Action[] getActions(boolean context) {
            return new Action[]{
                Utilities.actionsForPath("Actions/Build/").get(0),
                Utilities.actionsForPath("Actions/Build/").get(1),
            };
        }

    }

And that's really all. You can use your plain old ActionListener class. The downside is you have a bunch of tags in the layer file to deal with, though (aside from the small tweak) it was all generated for you. Looking forward to an annotation for actions, so that my ActionListener can simply be decorated with an annotation that will create the necessary layer entries when the module is compiled.

Nevertheless, you now have a context-sensitive action for customer objects.

 

{{ tag }}, {{tag}},

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}
{{ parent.authors[0].realName || parent.author}}

{{ parent.authors[0].tagline || parent.tagline }}

{{ parent.views }} ViewsClicks
Tweet

{{parent.nComments}}