Geertjan is a DZone Zone Leader and has posted 468 posts at DZone. You can read more from them at their website. View Full User Profile

How to Make the NetBeans Platform Sensitive to Customers

03.25.2010
| 14891 views |
  • submit to reddit

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.

 

Published at DZone with permission of its author, Geertjan Wielenga.

Comments

Miguel Garcia-lopez replied on Thu, 2010/03/25 - 4:56pm

Dear Geertjan, this is a great post as usual. And I agree with you annotations for actions will even improve it all.
One question: what about if selection type (see line 10 in XML layer excerpt) is not EXACTLY_ONE, but ALL, SOME or ANY? I mean, what should the code (class constructor, probably?) look like when using the different CookieAction.MODE_* selection modes?
Should the constructor take an array (Collection, maybe?) of Customer objects, for example?
Thanks as usual!

Geertjan Wielenga replied on Thu, 2010/03/25 - 6:42pm

Great question.

If you select "User May Select Multiple Nodes" in the first panel of the New Action wizard, the EXACTLY_ONE in the layer is ANY instead, with this being generated as the ActionListener:

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

public final class SomeAction implements ActionListener {

    private final List<Customer> context;

    public SomeAction(List<Customer> context) {
        this.context = context;
    }

    public void actionPerformed(ActionEvent ev) {
        for (Customer customer : context) {
            // TODO use customer
        }
    }
   
}

Don't know about ALL and SOME though. Will find out (or in the meantime you can read the NetBeans API javadoc for details)!

 

Miguel Garcia-lopez replied on Fri, 2010/03/26 - 3:51am

Thanks for the reply, the explained result is just as I would expect. Soooo handy as most NBP stuff!

Fabrizio Giudici replied on Fri, 2010/03/26 - 6:29am

This is a great improvement - and recalls me I've to run yet a big refactoring of my actions-related code, that is still bound to the API of two years ago...

Fabrizio Giudici replied on Fri, 2010/03/26 - 9:24am

Geertjan, my old code that I was referring to also provided support for SVG icons - perfectly working, but very tweaked. It could be the good moment to refactor it and given that the APIs have moved to a delegating model, it could be easier to fit it. In detail, which is the class that is processing iconBase?

Geertjan Wielenga replied on Sat, 2010/03/27 - 4:41pm

Hi Fabrizio, I don't know which class processes iconBase, why do you need to know?

Fabrizio Giudici replied on Mon, 2010/03/29 - 5:13am in response to: Geertjan Wielenga

Because as I want to fit the capability of reading a SVG icon (see my previous comment) I suppose it's there that I should work...

Peter Kirkham replied on Mon, 2010/03/29 - 10:10am

Hi Geertjan. I've been using this for a little while now and I can attest to it working successfully. Actually it was interesting you mentioned the Cookies in the dialog as I was a little puzzled about that. Nice to know it's just an artefact from NetBeans improving itself. What I'd like to know is whether it is possible to be a little more sophisticated than just enabling or disabling an action based on a class in the active node. Specifically is it possible to query a class to find out whether or not it should be context sensitive? For example to use the Customer analogy here, let's say I had a boolean field is_private in the Customer class and if true then the action should never be made available, but if false then the action should be made available if the Customer class is in the active node. Am I missing something really obvious as to how to achieve this?

Sirisha Gvls replied on Wed, 2010/06/09 - 4:49am

Hi Geertjan,

We are using netbeans platform 6.8. Previously we used filesystem api to display the nodes hierarchy. Right now we moved the data into database and showing the hierarcy. This is working fine. For these nodes and leafs we have to attach actions like Add, edit, copy,paste, cut,delete, rename and the same actions should update the nodes details in the database.


   Structure [parent Node]
       |
       -- Version
           |
             ----    Node1 [Root Node]
                      |
                       ----Multiple Nodes and leafs [ReportStructrureNode]
           |
            ----    Node2
                     |
                      ----Multipe Nodes and leafs

 

 Attached the actions by using the following code:

public Action[] getActions(boolean context) {
    Action[] result = new Action[] { new AddAction(), new EditAction()};
    return result;
}

We tried one approach for these action events with the following code:

 private final class EditAction extends AbstractAction implements ActionListener{

        public EditAction() {
            putValue(Action.NAME, "Edit");
        }

        public void actionPerformed(ActionEvent e) {
            int response = JOptionPane.showConfirmDialog(null, "Are you sure to want to edit "+ reportStructureItem.getNodeName() + "?");
            ReportStructureItem obj =(ReportStructureItem)getLookup().lookup(ReportStructureItem.class);
            editNode(obj);   
            EditCookie cookie = (EditCookie) getCookie(EditCookie.class);
        }

}

Actual Result what we have to show is when ever we click on "edit" action it has to get the xml content from the database and show in the report panel. The following code "editNode" will fetch the xml content from the database.

But for me edit is showing in the disabled form. Even i updated in the layer.xml. 

// Edit the node
    public void editNode(ReportStructureItem obj) {
        ReportStructureAdapter reportStructureAdapter = new ReportStructureAdapter();
        try {
            reportStructureAdapter.editItem(obj);
        } catch (DBException ex) {
            System.out.println("ex = " + ex.getMessage());
        }
       
    }

 Please do guide me how i would have to show the xml content in the report panel when i click on edit action.

 

Thanks

-Sirisha

Sirisha Gvls replied on Wed, 2010/06/09 - 4:49am

Hi Geertjan,

We are using netbeans platform 6.8. Previously we used filesystem api to display the nodes hierarchy. Right now we moved the data into database and showing the hierarcy. This is working fine. For these nodes and leafs we have to attach actions like Add, edit, copy,paste, cut,delete, rename and the same actions should update the nodes details in the database.


   Structure [parent Node]
       |
       -- Version
           |
             ----    Node1 [Root Node]
                      |
                       ----Multiple Nodes and leafs [ReportStructrureNode]
           |
            ----    Node2
                     |
                      ----Multipe Nodes and leafs

 

 Attached the actions by using the following code:

public Action[] getActions(boolean context) {
    Action[] result = new Action[] { new AddAction(), new EditAction()};
    return result;
}

We tried one approach for these action events with the following code:

 private final class EditAction extends AbstractAction implements ActionListener{

        public EditAction() {
            putValue(Action.NAME, "Edit");
        }

        public void actionPerformed(ActionEvent e) {
            int response = JOptionPane.showConfirmDialog(null, "Are you sure to want to edit "+ reportStructureItem.getNodeName() + "?");
            ReportStructureItem obj =(ReportStructureItem)getLookup().lookup(ReportStructureItem.class);
            editNode(obj);   
            EditCookie cookie = (EditCookie) getCookie(EditCookie.class);
        }

}

Actual Result what we have to show is when ever we click on "edit" action it has to get the xml content from the database and show in the report panel. And if i do any modifications in the xml, it has to save in the database. The following code "editNode" will fetch the xml content from the database.

But for me edit is showing in the disabled form. Even i updated in the layer.xml. 

// Edit the node
    public void editNode(ReportStructureItem obj) {
        ReportStructureAdapter reportStructureAdapter = new ReportStructureAdapter();
        try {
            reportStructureAdapter.editItem(obj);
        } catch (DBException ex) {
            System.out.println("ex = " + ex.getMessage());
        }
       
    }

 Please do guide me how i would have to show the xml content in the report panel when i click on edit action.

 

Thanks

-Sirisha

Nicholas Dunn replied on Thu, 2010/12/09 - 3:09pm

Hi, Just want to make a brief comment. In your screenshot you show just the name Customer. This results in a layer entry with the "type" value of Customer, which will fail at run time. You need to ensure that the layer entry has a type value equal to the fully qualified class name (demo.Customer). (Your xml is correct, but I think you should highlight this).

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.