Thursday Apr 29, 2010

Keyboard Shortcut Card for NetBeans Macophiles!

Good news for Mac users of NetBeans IDE. In 6.9, when you're using the Mac and go to Help | Keyboard Shortcuts Card, you will get a Mac-specific keyboard shortcut card:

Hurray!

Wednesday Apr 28, 2010

XML Editor in Source View of MultiView Component (Part 1)

You want the source view of your multiview editor to display an XML file in the NetBeans XML Editor? Nothing simpler—take the following steps:

  1. Download and install this plugin from the Plugin Portal.

  2. Go to the New Project dialog, find the "Abc MultiView Sample" in Samples | NetBeans Modules and complete the wizard.

  3. In the Projects window, open "AbcResolver.xml" and replace its content with the following:
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE MIME-resolver PUBLIC "-//NetBeans//DTD MIME Resolver 1.0//EN" 
    "http://www.netbeans.org/dtds/mime-resolver-1_0.dtd">
    <MIME-resolver>
        <file>
            <ext name="xml"/>
            <resolver mime="text/x-abc+xml">
                <xml-rule>
                    <element name="bla"/>
                </xml-rule>
            </resolver>
        </file>
    </MIME-resolver>

  4. Then go to the layer.xml file and change "x-abc" to to "x-abc+xml", in line 6 and in line 70.

  5. Somewhere on your hard drive, create a file with ".xml" as the file extension and then define its content as follows:
    <bla name="hello" default="task1">
        <description>Describes tasks and related jobs.</description>
        <task1 default="job1">
           <name>blabla1</name>
           <job1>hmmm1</job1>
           <job2>hmmm2</job2>
        </task1>
        <task2 default="job1">
           <name>blabla2</name>
           <job1>hmmm3</job1>
           <job2>hmmm4</job2>
        </task2>
    </bla>

  6. Run the application, go to the Favorites window, open the file you created above and then you should see this in the Text view:

You have now modified the sample module so that it supports an XML file by opening it in the NetBeans XML Editor, which wasn't needed by the non-XML file that the sample supports by default. Learn from the source code and apply to your own scenario. Stir briskly and eat while hot.

In other news. Now read part 2 of this series!

Tuesday Apr 27, 2010

Movie: Interview with Gunnar Reinseth, NetBeans Platform Expert in Norway!

While in Oslo recently, Toni Epple and I interviewed Gunnar Reinseth from Exie, which is based in Oslo. Gunnar and his team are no strangers to the NetBeans Platform—they created Office LAF for the NetBeans Platform and integrated EMF into their application.

We recorded the discussion we had together, i.e., Gunnar, Toni, and myself, and here it is in two parts, both are under 10 minutes each. The topics discussed include, of course, NetBeans Platform, but also modularity, GlassFish, OSGi, EMF, and much much more. Enjoy!

Part 1:

Part 2:

And, FYI, here's a screenshot of the application that Gunnar's team is responsible for, i.e., Exie Builder.

Thanks for the good times in Oslo, Gunnar!

Monday Apr 26, 2010

Small MultiView Editor Experiment (Part 2)

The sample for developers creating multiview editors on the NetBeans Platform (described yesterday in this blog) is now slightly more extensive, since you can add new nodes in the visual view, which will then automatically be added to the source editor (as well as the visual editor, of course):

The sample is available in the Plugin Portal and will work if you are using NetBeans IDE 6.9 Beta:

http://plugins.netbeans.org/PluginPortal/faces/PluginDetailPage.jsp?pluginid=27988

You will then, once you have installed the NBM, have a new sample in the New Project dialog:

Here's the source code you'll then have:

Run the above module, open a file with ".abc" as an extension (since the sample extends the File Type Integration Tutorial, which creates basic support for ".abc" files), and you'll have the multiview editor described here, which you can use as a starting point for your own multiview editors.

Sunday Apr 25, 2010

Small MultiView Editor Experiment (Part 1)

Been looking at multiview editors, since a few developers (Ajith Srikukan and Thusani Nagarathnam) have asked about this lately. I ended up with a small experimental starting point. When a new item is added in the text view of the editor below...

...a new node is added to the BeanTreeView in the visual view of the editor, alphabetically:

Nice to have the synchronization working, at least from one side. Next I will try from the other side too. After that, use the XML editor. Then some more advanced multiview editors, e.g., with visual library support, which Vadiraj Deshpande has also illustrated in his blog. I'm hoping that my example incorporating the visual library will be simpler than his.

Wednesday Apr 21, 2010

Tip for Localizers of JavaHelp in NetBeans Platform Applications

Another small thing I learnt from Toni Epple last week—let's say you're localizing a JavaHelp set. Assuming your helpset is quite small, you can simply put the translated help files into the same folder as the originals, as shown below. Then, set this line in the "platform-private.properties" file of your module (so that it will not be committed to your repo):
run.args.extra=--locale de:DE

That's all, now you can run the module (or whole application) and during development time, without needing to switch the system locale, you can see your translated pages live in your application.

The image below summarizes all this, showing the translated files (i.e., those with "_de" in the name, including the table of contents file itself), as well as the open properties file, together with the "run.args.extra" setting the locale:

When the above module is run, only the files with "_de" as the suffix of their name will be displayed in the JavaHelp window. This tip must be very handy to anyone localizing their JavaHelp sets when wanting to see the localized versions live in the application during development.

Creating Context-Sensitive Capabilities for File-Based Nodes (Epilogue)

Since I stated that yesterday's blog entry would be the final part in this series on creating context-sensitive capabilities for file-based nodes, the fact that the series continues today can only mean that... this is the epilogue! (In fact, it might be Part 1 of the epilogue.)

The reason for yet another part in this series is that loosely coupled data sources could have varying ORM tools behind them. In other words, one module could provide entity classes that use iBatis to connect to a database, while another module could provide entity classes that use JPA (or Hibernate or something else) to connect to a database.

How do we deal with this situation? Well, good thing that the interface for dealing with data is so simple:

public interface DataServiceInterface {

    public List getData(String name);

    public void updateData(String name);

    public void saveData(String name);
    
}

In fact, anything at all could be handled by means of the above interface. Let's say that our generic container application should also be able to display a node representing data coming from a web service. Not a problem, our interface covers that scenario too, doesn't it?

Great, so we have a very flexible interface, which can be implemented in many different ways. The only thing we need is for the relevant database connection files of the respective ORM tool to be registered in the layer. Someone providing a module with configuration files for iBatis, for example, would have this in the layer:

<folder name="Connections">
    <folder name="Cruise">
        <file name="iBatis">
            <attr name="config" stringvalue="resources/CruiseSqlMapConfig.xml"/>
        </file>
    </folder>
</folder>

On the other hand, someone providing configuration files for JPA would register their configuration file like this:

<folder name="Connections">
    <folder name="Cruise">
        <file name="JPA">
            <attr name="config" stringvalue="resources/CruisePersistence.xml"/>
        </file>
    </folder>
</folder>

"Connections" is the folder name containing the connections, below that is an identifier (here "Cruise") for distinguishing between different connections, because in the end, when the NetBeans Platform resolves all the layer files, the "Connections" folder will potentially have content like this:

<folder name="Connections">
    <folder name="Cruise">
        <file name="iBatis">
            <attr name="config" stringvalue="resources/CruiseSqlMapConfig.xml"/>
        </file>
        <file name="JPA">
            <attr name="config" stringvalue="resources/CruisePersistence.xml"/>
        </file>
    </folder>
    <folder name="Logbook">
        <file name="iBatis">
            <attr name="config" stringvalue="resources/LogbookSqlMapConfig.xml"/>
        </file>
        <file name="JPA">
            <attr name="config" stringvalue="resources/LogbookPersistence.xml"/>
        </file>
    </folder>
</folder>

So, now we have modules providing entity classes (some of which, as with JPA, have annotations, hence dependencies on EclipseLink or TopLink or something similar), while others are unannotated entity classes (such as those supported by iBatis). These modules all register their ORM configuration file, exactly as shown above.

And how are the above registrations used? That depends on the implementation of our DataServiceInterface. In the case of iBatis, the implementation of "getData" is as follows. Note the lines in bold, which shows you how the correct configuration file is found, i.e., if you want to use iBatis to get data from the "Cruise" database, all you need to do is provide the identifier "Cruise", followed by the name of the ORM, which is "iBatis" here, and "config" to specify the configuration file. As a user of iBatis in this application, you don't care at all about the implementation, you simply specify a way to find the file that you'll be using.

@ServiceProvider(service = DataServiceInterface.class)
public class iBatisDataServiceProvider implements DataServiceInterface {

    private String getConnectionFolder(String name) {
        return FileUtil.getConfigFile("Connections/" + name +"/iBatis").getAttribute("config").toString();
    }

    @Override
    public List getData(String name) {
        Reader reader = null;
        try {
            reader = reader = Resources.getResourceAsReader(getConnectionFolder(name));
            SqlMapClient sqlMap = SqlMapClientBuilder.buildSqlMapClient(reader);
            return sqlMap.queryForList("getAll", null);
        } catch (SQLException ex) {
            Exceptions.printStackTrace(ex);
        } catch (IOException ex) {
            Exceptions.printStackTrace(ex);
        } finally {
            try {
                reader.close();
            } catch (IOException ex) {
                Exceptions.printStackTrace(ex);
            }
        }
        return null;
    }

    @Override
    public void updateData(String name) {
       //to be done
    }

    @Override
    public void saveData(String name) {
       //to be done
    }
    
}

The module providing the class above could now be in a different module to where the connection files are found, since the class above looks in the layer for the location of the connection file.

The above file is registered in the "DataServiceProviders" layer file of the module that provides it. When the NetBeans Platform resolves all layer files, it will end up with a "DataServiceProviders" folder with this content:

<folder name="DataServiceProviders">
    <folder name="Cruise">
        <folder name="iBatis">
            <file name="no-imr-viewer-serviceproviders-iBatisDataServiceProvider.instance"/>
        </folder>
        <folder name="JPA">
            <file name="no-imr-viewer-serviceproviders-JPADataServiceProvider.instance"/>
        </folder>
    </folder>
    <folder name="Logbook">
        <folder name="iBatis">
            <file name="no-imr-viewer-serviceproviders-iBatisDataServiceProvider.instance"/>
        </folder>
        <folder name="JPA">
            <file name="no-imr-viewer-serviceproviders-JPADataServiceProvider.instance"/>
        </folder>
    </folder>
</folder>

Now let's call "getData" in our implementation classes! When do we do that? When we need to get data. Where do we need to do that? In the loosely coupled ChildFactory classes (see the past entries in this series for details on this). These need to look inside the layer file and find the above folders, turning them into a Lookup, by means of which the implementation can be found, where we can call the "getData", since that is defined in the interface:

public class CruisesChildFactory extends ChildFactory<Cruise> {

    @Override
    protected boolean createKeys(List<Cruise> list) {

        //Here we locate the implementation that will use JPA:
        DataServiceInterface dsi = Lookups.forPath("DataServiceProviders/Cruise/JPA").lookup(DataServiceInterface.class);

        //Here we pass "Cruise" to the implementation, so that the configuration file can be found: 
        List<Cruise> cruises = dsi.getData("Cruise");

        list.addAll(cruises);

        return true;

    }

    @Override
    protected Node createNodeForKey(Cruise key) {
         try {
            return new BeanNode(key);
        } catch (IntrospectionException ex) {
            Exceptions.printStackTrace(ex);
        }
        return null;
    }
    
}

Above, we specify that we want to use JPA, without caring at all about the implementation. We simply say "yes, we want to use JPA for this connection", with the file registered in the "config" attribute determining how the connection is made, as always.

What is the point of all of this? To make the application very flexible, so that contributions from the outside can be made possible. In the scenario that the IMR (Institute of Marine Research) in Norway is working with, new nodes can come from a wide variety of third parties, each providing a broad range of different (unrelated) kinds of data. In this scenario, where the application is a generic container for nodes, a very loose structure such as the above seems to be of fundamental importance.

Monday Apr 19, 2010

Creating Context-Sensitive Capabilities for File-Based Nodes (Part 4)

In this the final part of this ad-hoc series on context-sensitive actions for file-based nodes... we'll do something pretty radical—we'll use the loosely coupled ChildFactories from yesterday's blog entry to provide loosely coupled data sources. That's exactly what the Institute of Marine Research (IMR) in Bergen, Norway, needs.

Look at the screenshots below, you see two nodes, each providing access to different data... in the same TopComponent. While I integrated with the 'sample' database that comes with NetBeans IDE, the IMR will be integrating databases from multiple different external parties. One of these will provide support for the logbooks database, while another will do so for the cruises database. These two have nothing in common, except that the underlying data needs to be displayed for editing purposes, in a common TopComponent, with each database being represented by a node in the tree hierarchy, exactly as below:

The two nodes above don't know anything about each other. Neither does the rest of the application know about the nodes. The application is simply a generic container that needs nothing more than these entries in the layer file of contributed modules in order to build nodes:

<file name="Cruises.imrc">
    <attr name="iconBase" stringvalue="/imr/details/customer.png"/>
    <attr name="displayName" stringvalue="Cruises"/>
    <attr name="window" stringvalue="DetailsTopComponent"/>
    <attr name="childFactoryClass" newvalue="imr.details.CruisesChildFactory"/>
</file>

And then, in the definition of the ChildFactory, the connection is made to the database:

public class CruisesChildFactory extends ChildFactory<Customer> {

    @Override
    protected boolean createKeys(List<Customer> list) {
        DataServiceInterface dsi = Lookup.getDefault().lookup(DataServiceInterface.class);
        List<Customer> customers = dsi.getData("customer");
        list.addAll(customers);
        return true;
    }

    @Override
    protected Node createNodeForKey(Customer key) {
         try {
            return new AnnotatedBeanNode(key);
        } catch (IntrospectionException ex) {
            Exceptions.printStackTrace(ex);
        }
        return null;
    }
    
}

(The "AnnotatedBeanNode" is by Toni Epple, part of his support for annotations that generate BeanInfo classes at runtime, about which more will be written in an upcoming blog entry or article.)

Notice that we load an implementation of the DataServiceInterface, of which there is only one, and pass in a string that is unique for the database that we want to have access to. And here's the implementation, for iBatis (which is currently the only implementation):

@ServiceProvider(service = DataServiceInterface.class)
public class iBatisDataServiceProvider implements DataServiceInterface {

    private String getConnectionFolder(String name) {
        return FileUtil.getConfigFile("Connections/" + name).getAttribute("config").toString();
    }

    @Override
    public List getData(String name) {
        Reader reader = null;
        try {
            reader = reader = Resources.getResourceAsReader(getConnectionFolder(name));
            SqlMapClient sqlMap = SqlMapClientBuilder.buildSqlMapClient(reader);
            return sqlMap.queryForList("getAll", null);
        } catch (SQLException ex) {
            Exceptions.printStackTrace(ex);
        } catch (IOException ex) {
            Exceptions.printStackTrace(ex);
        } finally {
            try {
                reader.close();
            } catch (IOException ex) {
                Exceptions.printStackTrace(ex);
            }
        }
        return null;
    }

    @Override
    public void updateData(String name) {
       //to be done
    }

    @Override
    public void saveData(String name) {
       //to be done
    }
    
}

Each module wanting to hook into the above functionality must (as can be seen in the code above), register their iBatis map file as follows in the layer:

<folder name="Connections">
    <file name="customer">
        <attr name="config" stringvalue="resources/CruisesSqlMapConfig.xml"/>
    </file>
</folder>

And that's basically all that's needed to create a container for the display of data sources as nodes.

To make the solution really modular, for each data source, we have three modules—an API module (providing the entity classes), a DB configuration module (providing the iBatis map files), and a model module (providing the ChildFactory, registered as shown above, together with the icon displayed for the node). Here's how it all looks, take note of the service interface implementation, which is part of the core distribution, while the service interface comes from a separate module:

Later, one can imagine multiple implementations of the service (i.e., one for iBatis, one for JPA, one for Hibernate), all being provided by different modules.

The sources of the above sample will be available on Kenai soon.

Sunday Apr 18, 2010

Creating Context-Sensitive Capabilities for File-Based Nodes (Part 3)

Rather than finding a TopComponent for each Node (as done yesterday), let's share a single TopComponent between multiple Nodes. The benefit is that, well, you'll only have one TopComponent to maintain. You'll simply assign a different ChildFactory to the TopComponent's ExplorerManager, i.e., on the fly, programmatically, as and when needed.

Above, the Node hierarchy in the Details view is built from a ChildFactory registered in the layer. Hence, when a module is added from the outside, it will not provide its own TopComponent. Instead, it will provide its own ChildFactory, registered as an attribute of a virtual file in the layer file. In other words, only the model needs to be provided (i.e., the Nodes), rather than the view, which is shared between all the models. Of course, if a module from the outside needs its own view, it can provide one in its own module, but then it will not be able to integrate with the "Open Window" menu item above. However, preferably, the module from the outside should simply bring its own Node hierarchy with it (via the ChildFactory registration), so that for a minimum amount of work the new Node can be integrated into the application.

How to do this? Well, first follow part 1 and part 2 of this series. Then follow the steps below.

  1. Delete the ".imrc" files in your module. We should probably have defined the "window" key as an attribute in the layer to begin with. Plus, we now won't need to find a TopComponent for each Node anymore. So also delete each TopComponent class that you had defined for your Nodes, i.e., the ones you created via the New Window Component wizard. Then create just one TopComponent, named "DetailsTopComponent". Implement ExplorerManager.Provider, initialize the ExplorerManager, add a BeanTreeView (or whatever explorer view you like), and (as always) associate the ActionMap and the ExplorerManager to the Lookup of the TopComponent.

  2. Now we'll add some attributes to the layer, for each of the two virtual files we registered there, and on top of which the Node hierarchy has been constructed:
    <folder name="Reference">
        <file name="Cruises.imrc">
            <attr name="SystemFileSystem.icon" urlvalue="nbresloc:/no/imr/viewer/customer.png"/>
            <attr name="displayName" stringvalue="Details"/>
            <attr name="window" stringvalue="DetailsTopComponent"/>
            <attr name="childFactoryClass" newvalue="no.imr.viewer.CruisesChildFactory"/>
        </file>
        <file name="Logbooks.imrc">
            <attr name="SystemFileSystem.icon" urlvalue="nbresloc:/no/imr/viewer/customer.png"/>
            <attr name="displayName" stringvalue="Logbooks"/>d
            <attr name="window" stringvalue="LogbooksTopComponent"/>
            <attr name="childFactoryClass" newvalue="no.imr.viewer.LogbooksChildFactory"/>
        </file>
    </folder>

    The most important attribute in the context of this blog entry is highlighted above. The "newvalue" attribute will cause a new instance of the registered "childFactory" class to be created. In other words, you now need to create two ChildFactory classes, one for each of your Nodes, as indicated by the "childFactoryClass" attribute above. For example:

    public class CruisesChildFactory extends ChildFactory<String>{
    
        @Override
        protected boolean createKeys(List<String> list) {
            //Make a connection to the Cruises database,
            //i.e., Lookup.getDefault().lookup(DataServiceProvider.class)
            //and then call "getData("cruises")" on the interface,
            //but let's add some dummy data for prototyping purposes:
            list.add("cruise A");
            list.add("cruise B");
            list.add("cruise C");
            return true;
        }
    
        @Override
        protected Node createNodeForKey(String key) {
            Node node = new AbstractNode(Children.LEAF);
            node.setDisplayName(key);
            return node;
        }
    
    }
    public class LogbooksChildFactory extends ChildFactory<String> {
    
        @Override
        protected boolean createKeys(List<String> list) {
            //Make a connection to the Logbooks database,
            //i.e., Lookup.getDefault().lookup(DataServiceProvider.class)
            //and then call "getData("logbooks")" on the interface,
            //but let's add some dummy data for prototyping purposes:
            list.add("logbook A");
            list.add("logbook B");
            list.add("logbook C");
            return true;
        }
    
        @Override
        protected Node createNodeForKey(String key) {
            Node node = new AbstractNode(Children.LEAF);
            node.setDisplayName(key);
            return node;
        }
    
    }

    As you can see from the above code, you would bring the data connection with you via the ChildFactory.

  3. Now you need to use the various attributes in your own DataObject to use the registered ChildFactory in the construction of the shared TopComponent:
    public class IMRCategoryDataObject extends MultiDataObject {
    
        public IMRCategoryDataObject(final FileObject pf, MultiFileLoader loader) throws DataObjectExistsException, IOException {
    
            super(pf, loader);
    
            CookieSet cookies = getCookieSet();
            cookies.add((Node.Cookie) DataEditorSupport.create(this, getPrimaryEntry(), cookies));
    
            final String windowAttribute = (String) pf.getAttribute("window");
    
            OpenCapability oc = new OpenCapability() {
                @Override
                public void open() {
                    DetailsTopComponent tc = DetailsTopComponent.findInstance();
                    Children children = Children.create((ChildFactory) 
                           pf.getAttribute("childFactoryClass"), true);
                    tc.getExplorerManager().setRootContext(new AbstractNode(children));
                    tc.getExplorerManager().getRootContext().setDisplayName((String) 
                           pf.getAttribute("displayName"));
                    tc.open();
                    tc.requestActive();
                }
            };
    
            if (windowAttribute != null) {
                getCookieSet().assign(OpenCapability.class, oc);
            }
    
        }
    
        @Override
        protected Node createNodeDelegate() {
            final DataNode node = new DataNode(this, Children.LEAF, getLookup());
            node.setDisplayName(node.getName());
            return node;
        }
    
        @Override
        public Lookup getLookup() {
            return getCookieSet().getLookup();
        }
        
    }

As a result of the above code, when you right-click a Node in the viewer TopComponent, if the "window" attribute is defined, the menu item is enabled. Then the registered ChildFactory is assigned to the root context of the ExplorerManager that is implemented in the shared TopComponent.

Creating Context-Sensitive Capabilities for File-Based Nodes (Part 2)

In part 1, we created an Action for files of a certain type in the NetBeans Platform. What was special was that the Action is contextually sensitive to a custom defined "OpenCapability" interface. If an implementation of that interface is present, something can happen. If it is absent, something different can happen.

But what is it that can happen? I.e., how can the above infrastructure be used to do something useful? In this second part, we continue by looking at a scenario where an Action is enabled/disabled based on the presence/absence of a key found in a file. If the key is present, the value is used to find a specific TopComponent to be opened when the Action is invoked. As with the earlier part, this uses an approach introduced by Toni Epple in the recent NetBeans Platform Certified Training to the Institute of Marine Research in Bergen, Norway.

  1. In my module's layer file, I have a folder named "Reference", containing two FileObjects. The definition of the FileObjects points to actually existing files within my module:
    <folder name="Reference">
        <file name="Cruises.imrc" url="cruises.imrc">
            <attr name="SystemFileSystem.icon" urlvalue="nbresloc:/no/imr/viewer/reference.png"/>
        </file>
        <file name="Logbooks.imrc" url="logbooks.imrc">
            <attr name="SystemFileSystem.icon" urlvalue="nbresloc:/no/imr/viewer/reference.png"/>
        </file>
    </folder>

  2. So, there are two files within my module, named "cruises.imrc" and "logbooks.imrc". The former contains this content:
    window=CruisesTopComponent

    ... while the latter, i.e., "logbooks.imrc" is empty.

  3. Next, I enable the NetBeans Platform to recognize the above two files. I do this by using the New File Type wizard to create support for files that have "imrc" as their file extension. The wizard registers the new file type and also creates a skeleton DataObject for it.

  4. Within that generated DataObject, I load files for which the DataObject is created. I load them as Properties files. Then I check for the existence of a "window" key in that file. If that key exists, I add the "OpenCapability" to the cookieset. (I agree with Jean-Marc Borer in my recent blog entry relating to this topic, i.e., that Lookup should be used rather than Cookies. However, here I'm simply going along with the code generated by the New File Type wizard, i.e., code relating to cookies is generated, hence my additional code uses that too.)
    public class IMRCategoryDataObject extends MultiDataObject {
    
        Properties p;
    
        public IMRCategoryDataObject(FileObject pf, MultiFileLoader loader) throws DataObjectExistsException, IOException {
            super(pf, loader);
            p = new Properties();
            //"pf" is the FileObject that is bound to this DataObject in the layer file,
            //thanks to the New File Type wizard. It has an InputStream, so that we
            //can load it into a Properties object:
            p.load(pf.getInputStream());
            
            CookieSet cookies = getCookieSet();
            cookies.add((Node.Cookie) DataEditorSupport.create(this, getPrimaryEntry(), cookies));
    
            OpenCapability oc = new OpenCapability() {
                @Override
                public void open() {
                    TopComponent tc = WindowManager.getDefault().findTopComponent(p.getProperty("window"));
                    tc.open();
                    tc.requestActive();
                }
            };
    
            if (p.getProperty("window") != null) {
                getCookieSet().assign(OpenCapability.class, oc);
            }
    
        }
    
        @Override
        protected Node createNodeDelegate() {
            final DataNode node = new DataNode(this, Children.LEAF, getLookup());
            node.setDisplayName(node.getName());
            return node;
        }
    
        @Override
        public Lookup getLookup() {
            return getCookieSet().getLookup();
        }
    
    }

    Note: The rest of this blog entry discusses the highlighted code above in detail. (The remainder of the code above is not interesting, i.e., it is standard DataObject code.)

  5. Then I create Nodes on top of the two FileObjects and display those Nodes in an explorer view. Only the Node for the "cruises.imrc" has an enabled "Open Window" action, since the Node for the other FileObject has no "window" key. In other words, the "OpenCapability" is only available in one of the two cases, causing the Action registration to enable the menu item for "cruises.imrc", but not for "logbooks.imrc".

  6. As in the previous blog entry, the "OpenCapability" is defined as follows:
    package no.imr.viewer;
    
    public interface OpenCapability {
        public void open();
    }

    ...while the NetBeans Platform is sensitive to the above capability via this registration in the layer file, which is linked into the contextual menu of the IMRCNode displayed in the explorer view via a layer entry in Loader/text/x-imrcategory, which is the MIME type of files using the ".imrc" file extension:

    <folder name="Actions">
        <folder name="Edit">
            <file name="no-imr-viewer-OpenCategoryAction.instance">
                <attr name="delegate" methodvalue="org.openide.awt.Actions.inject"/>
                <attr name="displayName" bundlevalue="no.imr.viewer.Bundle#CTL_OpenCategoryAction"/>
                <attr name="injectable" stringvalue="no.imr.viewer.OpenCategoryAction"/>
                <attr name="instanceCreate" methodvalue="org.openide.awt.Actions.context"/>
                <attr name="noIconInMenu" boolvalue="false"/>
                <attr name="selectionType" stringvalue="EXACTLY_ONE"/>
                <attr name="type" stringvalue="no.imr.viewer.OpenCapability"/>
            </file>
        </folder>
    </folder>

    Since the "OpenCapability" is only added to the cookieset if the "window" key is present, which is absent in the case of "logbooks.imrc", the Action is disabled when you right-click the Node for "logbooks.imrc". When the Action is enabled, invocation of the Action results in "open" being called on the "OpenCapability":

    public final class OpenCategoryAction implements ActionListener {
    
        private final OpenCapability context;
    
        public OpenCategoryAction(OpenCapability context) {
            this.context = context;
        }
    
        public void actionPerformed(ActionEvent ev) {
            context.open();
        }
        
    }

    And what does "open" mean in this context? In this context, defined in the DataObject with which this blog entry started, "open" results in the opening of the TopComponent for which the "window" key has been defined. In the case of the "cruises.imrc" file, that means the "CruisesTopComponent".

And that's how setting a property in a file can result in an Action being enabled/disabled on a Node. Why is this useful? Well, maybe a new module will register a new ".imrc" file within the "Reference" folder. Let's say, for example, that a "Fish" node should be displayed under the "Reference" node. But, possibly that "Fish" node will not have a TopComponent to be opened. In that case, the ".imrc" file will not have a "window" key, so that the "Open" menu item will be disabled. If, on the other hand, the "Fish" node has a related TopComponent, i.e., one displaying the currently selected fish, the "fish.imrc" file would have a key/value pair like this, for enabling the "Open" menu item and causing the "FishTopComponent" to be found:

window=FishTopComponent

And that's how new additions to the "Reference" node can come from the outside, while still having distinct behavior. In the case of the Institute of Marine Research this could be pretty useful since each Node represents a different database which could have some very specific functionality. In such scenarios, contextually available (enabled/disabled present/absent) Actions on Nodes is essential and the above approach very elegantly solves this requirement. It is also a generic approach that can be used in any number of similar scenarios.

Best thing to do, however, is to make sure that the TopComponent, ".imrc" file, and layer registration are all found within the same module. That's what would probably be done anyway, since the usecase for this scenario assumes that each file would come from a different module, but it's important to remember anyway. For example, imagine that the TopComponent comes from one module, while the ".imrc" file comes from another one. Who is to say that both modules will be present and that the TopComponent will actually be found? Only way to really guarantee this is to put everything related to specific instance of this infrastructure into the same module.

Of course, the context-aware opening of TopComponents is just one example. The sky is the limit when you're using the NetBeans Platform, but if you've read this far, you probably already know that.

Friday Apr 16, 2010

Norwegian Wood

Today Toni Epple and I completed the delivery of the training/workshop at the Institute of Marine Research (IMR) in Bergen, Norway. It can definitely be seen as a successful adventure. The first three days were spent doing the NetBeans Platform Certified Training, focusing on all the main themes of the NetBeans Platform (module system, window system, lookup, filesystem, nodes, explorer views, and visual library)...

...as well as a long list of miscellaneous topics (from 'how to port' to 'how to distribute patches'). Then followed 2 days where we did a very intensive workshop. It all basically turned into a hands on porting session! We started out with the IMR's front end and ported it, step by step, to the NetBeans Platform. Of course, we didn't port all of it, since it is a very large and intricate application. Instead, we focused on some specific topics. The topics were proposed by the IMR team:

At the end of the first day, we had created an application containing a few custom modules (e.g., a viewer module and a module containing capabilities), together with a long list of module wrappers for all the JARs that IMR works with (e.g., iBatis, Log4J, Dom4J, and many [many!] others, including most/all of the JARs produced by the original application). The viewer module provided a window that contained an explorer view exposing a node built from files registered in the layer file. The files in the layer file represent folders in the original application, making the set of folders extensible in the future.

A connection was established to the actual database at IMR (well, copied to the local disk of the laptop we were all sharing, via overhead projector, driven by one of the students), with data displayed via a BeanNode in an OutlineView, with all properties shown in a Properties window:

(Permission to publish the above image was asked and received, of course.)

Toni introduced a lot of very interesting and flexible code. That code included the dynamic capabilities that I outlined yesterday in this blog, as well as a property-file based capability for opening a specific window for each node in the tree, despite all the files being of the same MIME type (more on that in one of my next blog entries).

At that point, we had covered many of the topics requested by the IMR team, all within the first day, and all driven by the students, with Toni and myself explaining the code and instructing the driving student how and where to code everything.

During the evening, after the course, Toni and I covered a bunch of other areas, from finetuning the OutlineView (e.g., removing the first column), to adding a progress bar, to adding SaveCookie support, to working on internationalization concerns (including internationalized helpsets). The next day, i.e., the 2nd day of the 2 day workshop, we started by explaining what we had done the previous evening together. Then we continued, with Toni introducing even more flexibility for reusing the same window to display different nodes. During that time, I focused on providing a JComboBox in the OutlineView, which was also one of the requirements. Here's a screenshot of my prototype, which is now in the IMR prototype:

The above is a step towards mirroring the functionality provided by OpenSwing, which is the framework that the IMR is now migrating away from, thanks to its lack of flexibility in the light of the many features provided by the NetBeans Platform. We also migrated from our BeanNode to an AbstractNode, giving much more control over the display. The BeanNode had served its prototyping purpose and the benefits of the AbstractNode were immediately apparent. We created a property sheet with one property, into which we plugged the inplace editor that provided the JComboBox.

The result at the end of the 2 days was very pleasing to everyone involved. The IMR now has a starting point for further development. Though they have only a small bit of code, many patterns have been covered in concrete code samples integrated into an application that the developers are able to relate to, since it all relates very closely to their original code.

It wasn't all hard work, of course, and one evening was spent with alcohol and conversation in some of the nice Bergen bars, which I might be visiting a lot in the coming weeks thanks to Iceland:

Toni and I are looking forward to doing more of these kinds of trainings, i.e., going beyond the training materials and straight into concrete porting scenarios. Since the NetBeans Platform is the only modular Swing application framework in the world, with Oracle database servers being ideal for the type of large data-centric applications relevant to the NetBeans Platform, the market for NetBeans Platform migration workshops, exactly as done here in Bergen, must be very large indeed... especially since the modern world of mobile and web development apparently cannot do without desktop alternatives. And don't take my word for it!

In the interim, at least, others interested in migration workshops to the NetBeans Platform should write to users@edu.netbeans.org, the mailing list for those wanting training/consultancy relating to the NetBeans Platform. Looking forward to porting your application with you!

Wednesday Apr 14, 2010

Creating Context-Sensitive Capabilities for File-Based Nodes (Part 1)

Here's a capability for opening something:
package no.imr.viewer;

public interface OpenCapability {
    public void open();
}

In my DataObject, I assign the OpenCapability to the dynamic Lookup of the DataObject, take note of the code in bold below:

public class IMRCategoryDataObject extends MultiDataObject {

    InstanceContent content = new InstanceContent();

    public IMRCategoryDataObject(FileObject pf, MultiFileLoader loader) throws DataObjectExistsException, IOException {
        super(pf, loader);
        CookieSet cookies = getCookieSet();
        cookies.add((Node.Cookie) DataEditorSupport.create(this, getPrimaryEntry(), cookies));
    }

    @Override
    protected Node createNodeDelegate() {

        final DataNode node = new DataNode(this, Children.LEAF, 
           new ProxyLookup(getLookup(), new AbstractLookup(content)));

        content.add(new OpenCapability() {
            @Override
            public void open() {
                //Define what should happen for each node:
                StatusDisplayer.getDefault().setStatusText("Opened: " + node.getName());
            }
        });

        return node;

    }

    @Override
    public Lookup getLookup() {
        return getCookieSet().getLookup();
    }
    
}

(Alternatively, use "getCookieSet.assign(OpenCapability.class", oc), if you want to use cookies, which is simply an outdated pattern replaced by Lookup.)

I then use the New Action wizard to create a new "OpenCategoryAction" action that is sensitive to "OpenCapability" objects (which is added to the dynamic content of the DataObject, as shown above). Here's the layer entry generated:

<folder name="Actions">
    <folder name="Edit">
        <file name="no-imr-viewer-OpenCategoryAction.instance">
            <attr name="delegate" methodvalue="org.openide.awt.Actions.inject"/>
            <attr name="displayName" bundlevalue="no.imr.viewer.Bundle#CTL_OpenCategoryAction"/>
            <attr name="injectable" stringvalue="no.imr.viewer.OpenCategoryAction"/>
            <attr name="instanceCreate" methodvalue="org.openide.awt.Actions.context"/>
            <attr name="noIconInMenu" boolvalue="false"/>
            <attr name="selectionType" stringvalue="EXACTLY_ONE"/>
            <attr name="type" stringvalue="no.imr.viewer.OpenCapability"/>
        </file>
    </folder>
</folder>

It is registered for the MIME type of the node, which I created via the New File Type dialog, so will appear in the right-click contextual menu item of my node, having the same appearance for each node, but with behavior that depends on the particular node in question (thanks to the dynamicity of the Lookup used in the definition of the DataObject, as shown above). Here's how the ActionListener is registered (by the wizard automatically) in the layer:

<folder name="Loaders">
    <folder name="text">
        <folder name="x-imrcategory">
            <folder name="Actions">
                <file name="no-imr-viewer-OpenCategoryAction.shadow">
                    <attr name="originalFile" stringvalue="Actions/Edit/no-imr-viewer-OpenCategoryAction.instance"/>
                    <attr name="position" intvalue="0"/>
                </file>
                ...
                ...
                ...

And here's how the ActionListener is defined, simply getting the capability injected into it (thanks to the layer registration for the Action), from where the method is called, which behaves context sensitively, i.e., as defined in the DataObject above:

public final class OpenCategoryAction implements ActionListener {

    private final OpenCapability context;

    public OpenCategoryAction(OpenCapability context) {
        this.context = context;
    }

    public void actionPerformed(ActionEvent ev) {
        context.open();
    }
    
}

That's how a capability can be added to a Node, letting me dynamically compose the capabilities of my Node as and when needed. Currently, the "OpenCapability" is set in the definition of the DataObject, but since the InstanceContent is in the Lookup of the DataObject, we can dynamically add/remove the OpenCapability to/from the Node's Lookup as needed.

iBatis Sample and NetBeans Platform Pic from Norway

My small iBatis based sample application is really taking shape now. Apart from the cool PropertyPanel integration in the central editor area below, the other noteworthy thing is that all three of the windows are aware of changes to the underlying model. So, when either the Properties window or the editor are used to change a property, all the other windows are notified and appropriate events take place, such as the display name of the node in the viewer on the left getting updated. Despite the fact that all three windows come from different modules, of course.

I'm also discovering that BeanNode (properties support) + FilterNode (finetuning of BeanNode display) is a far more powerful combination than AbstractNode. And all along I've been using AbstractNode, until now. Plus, the SaveCookie has been integrated successfully. Now I simply need to hook in the standard iBatis code for saving objects to a database and then the "U" in "CRUD" is taken care of.

And here's a pic taken today of the group at the Institute of Marine Research in Bergen Norway on the course that Toni and I are delivering here (we're also in the pic somewhere). Behind the group, you see the amazing view over the Bergen harbor, as well as the mountains right behind it, that's visible from the top of the building and where the pic was taken (and where they have lunch, how cool is that):

Two NetBeans Platform applications they're drawing some inspiration from are the work done at Boeing (especially the JavaOne presentation from 2008 on that topic) and Puzzle GIS.

Tuesday Apr 13, 2010

FileObject, DataObject, Node in Serbia

I found a bunch of movies on YouTube by the NetBeans User Group Serbia (NUGS), taken during the time Toni and I gave a training recently at the University of Belgrade. Here's a nice one showing a small part of Toni's explanation on FileObject vs DataObject vs Node:

Now read Porting Neuroph to NetBeans Platform, which announces the recent official NUGS plans to move their Neuroph neural network editor to the NetBeans Platform, as well as Neuroph on Hadoop: Massive Parallel Neural Network System?, which reveals a convergence of NetBeans Platform applications in the context of a proposed Google Summer of Code project.

Monday Apr 12, 2010

Bergen & iBatis on the NetBeans Platform

Toni and I are visiting the Institute of Marine Research in Bergen, Norway, where a set of heavily data-centric applications (i.e., for processing data coming back from research cruises all over the world) are being integrated into a consolidated application on the NetBeans Platform.

A snapshot of the group to give a quick impression:

They're strongly in favor of using free & open-source solutions, of which the NetBeans Platform is obviously one. (In several cases, we're introducing them to NetBeans IDE for the first time, since several didn't know about the lightbulb hints in the IDE, while others hadn't used the Matisse GUI Builder before.) Another one is iBatis, which I had heard of but never used before. This gave me sufficient info and now I have a small CRUD (well, only "R" at this point) sample on the NetBeans Platform via iBatis (i.e., no JPA is being used, only a Derby database and iBatis XML files, with iBatis code in the TopComponent):

To give a quick intro to iBatis in the context of the NetBeans Platform, here's a screenshot of the application structure, followed by a brief description of the highlighted nodes below:

The highlighted files above:

  • Customer API. An API module that exposes two classes (generated in the IDE via the wizard, after which I deleted all the JPA annotations, leaving me with two POJOs representing rows in a database).

  • CustomerViewer. A module that provides the window that you see in the application, together with the two XML files typical to iBatis applications:
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE sqlMapConfig PUBLIC "-//iBATIS.com//DTD SQL Map Config 2.0//EN" 
       "http://www.ibatis.com/dtd/sql-map-config-2.dtd">
    <sqlMapConfig>
        <settings cacheModelsEnabled="true" enhancementEnabled="true" 
            lazyLoadingEnabled="true" maxRequests="32" maxSessions="10" maxTransactions="5" 
            useStatementNamespaces="false" />
        <transactionManager type="JDBC">
            <dataSource type="SIMPLE">
                <property name="JDBC.ConnectionURL" value="jdbc:derby://localhost:1527/sample"/>
                <property name="JDBC.Password" value="app"/>
                <property name="JDBC.Driver" value="org.apache.derby.jdbc.ClientDriver"/>
                <property name="JDBC.Username" value="app"/>
            </dataSource>
        </transactionManager>
        <sqlMap resource="Customer.xml" />
    </sqlMapConfig>
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE sqlMap PUBLIC "-//ibatis.apache.org//DTD SQL Map 2.0//EN" 
       "http://ibatis.apache.org/dtd/sql-map-2.dtd">
    <sqlMap namespace="Customer">
        <typeAlias alias="customer" type="org.customer.api.Customer"/>
        <select id="getAll" resultClass="customer">
                select \* from customer
        </select>
    </sqlMap>

    The above two do the same as persistence.xml in JPA, i.e., make a connection to the (in this case) Sample database in NetBeans IDE and retrieve the requested data.

  • DerbyClientLib. A single JAR (derbyclient.jar from the JDK) wrapped into a module and made available to the iBatis module.

  • iBatisLib. Two JARs from iBatis wrapped into a module and made available to the CustomerViewer module.

And how is the data retrieved by iBatis and shown in the window? Like this, in the TopComponent:

public CustomerTopComponent() {
    Reader reader = null;
    try {
        initComponents();
        setName(NbBundle.getMessage(CustomerTopComponent.class, "CTL_CustomerTopComponent"));
        setToolTipText(NbBundle.getMessage(CustomerTopComponent.class, "HINT_CustomerTopComponent"));
        add(new BeanTreeView(), BorderLayout.CENTER);
        reader = Resources.getResourceAsReader("SqlMapConfig.xml");
        SqlMapClient sqlMap = SqlMapClientBuilder.buildSqlMapClient(reader);
        List<Customer> customers = (List<Customer>) sqlMap.queryForList("getAll", null);
        //And here we pass the retrieved customers into the factory that creates nodes:
        em.setRootContext(new AbstractNode(Children.create(new CustomerChildFactory(customers), true)));
        em.getRootContext().setDisplayName("Customers");
    } catch (SQLException ex) {
        Exceptions.printStackTrace(ex);
    } catch (IOException ex) {
        Exceptions.printStackTrace(ex);
    } finally {
        try {
            reader.close();
        } catch (IOException ex) {
            Exceptions.printStackTrace(ex);
        }
    }
    associateLookup(ExplorerUtils.createLookup(em, getActionMap()));
}

Here's the 'CustomerChildFactory' you see referenced above:

import java.beans.IntrospectionException;
import java.util.List;
import org.customer.api.Customer;
import org.openide.nodes.BeanNode;
import org.openide.nodes.ChildFactory;
import org.openide.nodes.Node;
import org.openide.util.Exceptions;

public class CustomerChildFactory extends ChildFactory<Customer> {

    private final List<Customer> customers;

    CustomerChildFactory(List<Customer> customers) {
        this.customers = customers;
    }

    @Override
    protected boolean createKeys(List<Customer> list) {
        list.addAll(customers);
        return true;
    }

    @Override
    protected Node createNodeForKey(Customer key) {
        try {
            return new BeanNode(key);
        } catch (IntrospectionException ex) {
            Exceptions.printStackTrace(ex);
        }
        return null;
    }
    
}

And that's all. A simple example of iBatis usage on the NetBeans Platform. But I wonder why/when iBatis would be used rather than JPA?

Saturday Apr 10, 2010

Exif Reader on the NetBeans Platform

On the dev mailing list, Mario Schroeder wanted to get started with Exif on the NetBeans Platform, while Ernie Rael has been asking about ContextAwareAction. So I looked into both at the same time, creating an Exif Reader for JPEG/JPG files (on top of this library), if Exif metadata is identified in the selected file. In that case, a popup menu item appears on the node in the Favorites window, and the reader in the navigator mode opens (as shown below), with related properties of the current tag shown in the Properties window:

Source of the above sample are here in a ZIP file.

Thursday Apr 08, 2010

Are You In Norway???

Are you in Norway (or thereabouts)? Toni Epple and myself will be giving a NetBeans Platform Certified Training and related presentations, as well as meeting developers interested in developing their Swing applications on the NetBeans Platform in Oslo and Bergen during the coming two weeks:
  • Institute for Marine Research. Based in Bergen, the institute has a large set of databases (handling things like species lists, vessels, institutions, survey logbooks, biological samples) that they want to maintain via an integrated desktop solution. More on the specific needs and plans of this group can be seen in the slideshow below!

  • Exie. An Oslo-based company involved in people-driven performance management solutions. The developers on this team have contributed two of the most interesting things imaginable to the NetBeans Platform: EMF integration (https://nb-emf.dev.java.net), as discussed here, and Office Look and Feel support (https://officelaf.dev.java.net), as explained here. The latter enables really cool looking apps like this one:

    Definitely time to meet the developers behind these creative innovations in person and have a drink with them!

  • Zirius. An Oslo-based company developing ERP software called Zirius that is in use by close to 400 Norwegian companies. It is a Swing application using JNLP for distribution, while the codebase is getting old, needing an update. A fresh start for their application's architecture on the NetBeans Platform is being explored.

Toni and I are pretty flexible during the time that we'll be there, so if anyone else (universities/institutions/companies/JUGs) want to attend/organize a presentation about modular development on the NetBeans Platform, leave a message in the comments or contact me in person at geertjan dot wielenga at sun dot com.

And here's a very interesting slideshow from the Institute for Marine Research in Bergen, mentioned above, about their application and their plans/questions about using the NetBeans Platform:

Looking forward to our Norwegian adventures!

Tuesday Apr 06, 2010

Middle Eastern Water Management on the NetBeans Platform

Next, on our virtual world tour of NetBeans Platform applications, we travel to the Middle East. Here we find a software package that is used for managing a municipality's water, waste water piping, and water subscriptions. In short, a water management system.

Here's a screenshot, with some nice Arabic script, proving the NetBeans Platform's support for Arabic (as well as Persian and Hebrew). Click the image for a closer look:

My basic Arabic comprehension tells me that the title bar includes the word "mudeerait", meaning "managers", which is unsurprising in this context. Also, below the graph image, one of the words is "faktuur", which is the same in Dutch, meaning "receipt"!

The application is based on the NetBeans Platform, Derby, JasperReport, Java CSV, and other open source projects.

Some more info about what this application does:

There are several companies that install the pipelines and water distribution subscriptions to people's houses. For example, when a new block of houses is going to be built, the company takes the water distribution pipes to each house of that new block. The software manages the entire process, from when plans are being prepared down to following consumer complaints about broken pipes and so on. The software does the same task for installing sewer pipes to newly created houses, city sectors, and so on.

The software is developed for the company that manages multiple contractor companies that do the actual job of installing the pipes. Before this software, they had many Excel spreadsheets and some old software that didn't do a good job of managing the contractors and pipes. With this new system, all of those separate Excel spreadsheets and software systems are superfluous and this single software package manages all of the requirements in one go.

Since it is developed on top of the NetBeans Platform, the company can add new functionalities in the form of plugins and very easily distribute them to all the branches of the customer companies, simply by sending an updated NBM file or by updating the system through the central server, which also hosts their Derby database server.

Cool. Isn't it interesting to see the NetBeans Platform applied in this context?

Presenter.Toolbar & Presenter.Menu (Part 2)

A big weakness in the previous blog entry was that I specified a particular folder in the layer ("Actions/MyActions") as being the one of interest. Now... let's handle things differently. We'd like to build up a list of JMenuItems and JPopupMenu's for display when the menu item or toolbar button are clicked. Rather than forcing everyone to register their actions in the same folder in the layer.xml, let's simply tell them they need to add an attribute to their action registration. Then we'll find that attribute and add the action accordingly.

Concretely, we have a menu item and toolbar button that should display all actions that relate to tasks. So, whenever an external module developer registers such an action, they should include an attribute that we tell them to use especially for this purpose. I.e.:

<folder name="Actions">
    <folder name="Edit">
        <file name="com-netbeansrcp-taskeditor-NewTaskAction.instance">
            <attr name="task-related" boolvalue="true"/>
            <attr name="delegate" newvalue="com.netbeansrcp.taskeditor.NewTaskAction"/>
            <attr name="displayName" bundlevalue="com.netbeansrcp.taskeditor.Bundle#CTL_NewTaskAction"/>
            <attr name="iconBase" stringvalue="com/netbeansrcp/taskeditor/Add.png"/>
            <attr name="instanceCreate" methodvalue="org.openide.awt.Actions.alwaysEnabled"/>
            <attr name="noIconInMenu" boolvalue="false"/>
        </file>
    </folder>
</folder>

OK. Now the actions that relate to tasks are marked as such.

Now this is the action that builds up a list of task-related actions and makes them available as JMenuItems from a menu item and JPopupMenus from a toolbar button:

public final class AddAction implements ActionListener, Presenter.Toolbar, Presenter.Menu {

    private ArrayList<Action> foundActions = new ArrayList();
    private String ICON_PATH = "org/netbeansrcp/taskactions/Add.png";

    public AddAction() {
        findTaskRelatedActions();
    }
 
    //Go through all actions and find the ones that have our attribute
    //and then add them to our "foundActions" array list:
    public void findTaskRelatedActions() {
        FileObject actionsFolder = FileUtil.getConfigFile("Actions");
        FileObject[] actionsCategories = actionsFolder.getChildren();
        for (FileObject oneActionsCategory : actionsCategories) {
            FileObject[] actions = oneActionsCategory.getChildren();
            for (FileObject fo : actions) {
                Boolean isTaskRelated = (Boolean) fo.getAttribute("task-related");
                if (isTaskRelated != null && isTaskRelated) {
                    DataObject dob;
                    try {
                        dob = DataObject.find(fo);
                        InstanceCookie ck = dob.getLookup().lookup(InstanceCookie.class);
                        try {
                            foundActions.add((Action) ck.instanceCreate());
                        } catch (IOException ex) {
                            Exceptions.printStackTrace(ex);
                        } catch (ClassNotFoundException ex) {
                            Exceptions.printStackTrace(ex);
                        }
                    } catch (DataObjectNotFoundException ex) {
                        Exceptions.printStackTrace(ex);
                    }
                }
            }
        }
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        //Nothing needs to happen here.
    }

    @Override
    public JMenuItem getMenuPresenter() {
        JMenu menu = new JMenu(NbBundle.getMessage(AddAction.class, "CTL_AddAction"));
        menu.setIcon(ImageUtilities.loadImageIcon(ICON_PATH, true));
        //Iterate through the found actions:
        for (Action action : foundActions) {
            //Create a new JMenuItem for each found action:
            JMenuItem item = new JMenuItem(action);
            //Add the new JMenuItem to the JMenu:
            menu.add(item);
        }
        return menu;
    }

    @Override
    public Component getToolbarPresenter() {
        return new MyToolbarPresenter();
    }

    private class MyToolbarPresenter extends JButton {

        private JPopupMenu popup;

        MyToolbarPresenter() {
            this.setText(NbBundle.getMessage(AddAction.class, "CTL_AddAction"));
            this.setIcon(ImageUtilities.loadImageIcon(ICON_PATH, true));
            this.popup = new JPopupMenu();
            //Iterate through all the found actions:
            for (Action action : foundActions) {
                //Add the current found action from the layer to the popup:
                this.popup.add(action);
                //Show the button when the popup is clicked:
                this.addActionListener(new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent arg0) {
                        MyToolbarPresenter.this.popup.show(
                                MyToolbarPresenter.this, 0,
                                MyToolbarPresenter.this.getHeight());
                    }
                });
            }
        }
    }
    
}

The above action is registered in the layer, as shown in the previous blog entry. Now all the task-related actions are identified and made available as JMenuItems and JPopupMenus from a menu item and a toolbar button.

Presenter.Toolbar & Presenter.Menu (Part 1)

Let's create a slightly more complex menu item and toolbar button in a NetBeans Platform application. They'll look like this:

Here's the definition of this action:

 

public final class AddAction implements ActionListener, Presenter.Toolbar, Presenter.Menu {

    //The action that will be invoked is defined in the layer.xml,
    //as the first action within "Actions/MyActions":
    Action newTaskAction = Utilities.actionsForPath("Actions/MyActions").get(0);

    //Icon will be reused, so set its path once:
    private String ICON_PATH = "org/netbeansrcp/taskactions/Add.png";

    @Override
    public void actionPerformed(ActionEvent e) {
        //Nothing needs to happen here.
    }

    //Define what will be displayed in the menubar:
    @Override
    public JMenuItem getMenuPresenter() {
        //First, a JMenu will be displayed...
        JMenu menu = new JMenu(NbBundle.getMessage(AddAction.class, "CTL_AddAction"));
        //...with the icon defined above:
        menu.setIcon(ImageUtilities.loadImageIcon(ICON_PATH, true));
        //The menu item will be the action identified above:
        JMenuItem item = new JMenuItem(newTaskAction);
        menu.add(item);
        return menu;
    }

    //Define what will be displayed in the toolbar:
    @Override
    public Component getToolbarPresenter() {
        return new MyToolbarPresenter();
    }

    private class MyToolbarPresenter extends JButton {

        private JPopupMenu popup;

        MyToolbarPresenter() {
            //Set the text of the button:
            this.setText(NbBundle.getMessage(AddAction.class, "CTL_AddAction"));
            //Set the icon of the button:
            this.setIcon(ImageUtilities.loadImageIcon(ICON_PATH, true));
            //Create a popup:
            this.popup = new JPopupMenu();
            //Add the action from the layer to the popup:
            this.popup.add(newTaskAction);
            //Show the button when the popup is clicked:
            this.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent arg0) {
                    MyToolbarPresenter.this.popup.show(
                            MyToolbarPresenter.this, 0,
                            MyToolbarPresenter.this.getHeight());
                }
            });
        }
    }
    
}

It is registered in the layer like this:

<folder name="Actions">
    <folder name="Edit">
        <file name="org-netbeansrcp-taskactions-AddAction.instance"/>
    </folder>
</folder>
<folder name="Menu">
    <folder name="Edit">
        <file name="org-netbeansrcp-taskactions-AddAction.shadow">
            <attr name="originalFile" stringvalue="Actions/Edit/org-netbeansrcp-taskactions-AddAction.instance"/>
            <attr name="position" intvalue="0"/>
        </file>
    </folder>
</folder>
<folder name="Toolbars">
    <folder name="Edit">
        <file name="org-netbeansrcp-taskactions-AddAction.shadow">
            <attr name="originalFile" stringvalue="Actions/Edit/org-netbeansrcp-taskactions-AddAction.instance"/>
        </file>
    </folder>
</folder>

It's part of Juergen Petri's NetBeans RCP book, but now updated to use ActionListener instead of CallableSystemAction. And SystemAction calls are replaced by Utilities.actionsForPath.

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 2010 »
SunMonTueWedThuFriSat
    
5
7
9
11
15
17
20
22
23
24
30
 
       
Today