Monday, August 16, 2010

Project Type Module + ChildFactory

After completing the Project Module tutorial (found here), I got a nice folder structure... However version control folders and other files that where not relevant to the project where displayed under the project structure. I wanted to display only the relevant files and folders and hide the rest. Next I show how to build a ChildFactory that does exactly what I needed.

In the original tutorial at Step 2 of section Creating the Logical View Provider, you defined a TextNode that has the following constructor:

public TextNode(Node node, DemoProject project) throws DataObjectNotFoundException {
super(node, new FilterNode.Children(node),
//The projects system wants the project in the Node's lookup.
//NewAction and friends want the original Node's lookup.
//Make a merge of both
new ProxyLookup(new Lookup[]{Lookups.singleton(project),
node.getLookup()
}));
this.project = project;
}

The important bit to notice is the new FilterNode.Children(node) part. This is basically creating a default Children layout for your project. By default I mean that all child files and folders under your main project folder will be displayed under the project node tree. In my specific case the tree I got can be seen in the following figure, where for example you can see version control folders (.svn).




To change this behavior we need to create a new custom child factory. First create a class that extends org.openide.nodes.ChildFactory<T>:

public class AddonChlidFactory extends org.openide.nodes.ChildFactory<String> {

FileObject folder;

public AddonChlidFactory(FileObject folder) {
this.folder = folder;
}
}


The folder FileObject will be used later for inspecting subfolders and filtering them as needed. The ChildFactory works by creating a node for each key in a set of keys (the set of keys is arbitrary). Basically, you supply the set of keys and then the IDE asks you to give him the node(s) that should be generated according to the key. According to the API documentation the set of keys should be generated dynamically (probably according to the actual files and folders in your project folder). But for simplicity I created it statically. My set contains the extensions of the files I am interested and the folders string to provide nodes for folders. This is how my createKeys method looks:

@Override
protected boolean createKeys(List<String> list) {
// File types of interest
list.add("lua");
list.add("toc");
list.add("xml");
list.add("ttf");
list.add("tga");
list.add("blp");
list.add("txt");
list.add("folders");
return true;
}

Next, we need to decide what nodes to return for each key. For data files we will just return the node representing the file. For folders we will inspect the folder name and only return nodes if some conditions are met. Specifically we are interested in NOT returning nodes for version control folders. Next is the code of the createNodesForKey method:

@Override
protected Node[] createNodesForKey(String key) {
ArrayList<Node> nodes = new ArrayList<Node>();
if(key.equals("folders")) {
/* add folder nodes */
for(FileObject o : Collections.list(folder.getFolders(false))) {
if(o.getName().equals("nbproject")) {
try {
nodes.add(new nbProjectNode(o));
} catch (DataObjectNotFoundException ex) {
Exceptions.printStackTrace(ex);
}
} else if(!(o.getName().equals(".svn")
|| o.getName().equals(".cvs")
|| o.getName().equals(".hg"))) {
// Add all folders that are not version control
Node addonFolderNode = DataFolder.findFolder(o).getNodeDelegate();
nodes.add(new AddonFolderNode(addonFolderNode, o));
}
}
} else {
// Add data files
for(FileObject o : Collections.list(folder.getData(false))) {
if (o.hasExt(key)) {
try {
Node addonNode = DataObject.find(o).getNodeDelegate();
nodes.add(new FilterNode(addonNode, Children.LEAF));
} catch (DataObjectNotFoundException ex) {
Exceptions.printStackTrace(ex);
}
}
}
}
return(nodes.toArray(new Node[nodes.size()]));
}

You can notice that for the folders keyword I have two options: First, if the folder is the nbproject folder we create a nbProjectNode (based on the NetBeans Project Type Extension Module Tutorial). Second, if the folder name is not any of the common revision control folder names a node is returned. For data files, we create nodes for any files who's extension is one of the keys. Notice also that for folders we used our own AddonFolderNode node, for which we have a nested class. This will allow us to define custom behavior for our inner project folders (for example what actions can be invoked). Putting it all together we will have:

public class AddonChlidFactory extends org.openide.nodes.ChildFactory<String> {

FileObject folder;

public AddonChlidFactory(FileObject folder) {
this.folder = folder;
}

@Override
protected boolean createKeys(List<string> list) {
// File types of interest
list.add("lua");
list.add("toc");
list.add("xml");
list.add("ttf");
list.add("tga");
list.add("blp");
list.add("txt");
list.add("folders");
return true;
}

@Override
protected Node[] createNodesForKey(String key) {
ArrayList<Node> nodes = new ArrayList<Node>();
if(key.equals("folders")) {
/* add folder nodes */
for(FileObject o : Collections.list(folder.getFolders(false))) {
if(o.getName().equals("nbproject")) {
try {
nodes.add(new nbProjectNode(o));
} catch (DataObjectNotFoundException ex) {
Exceptions.printStackTrace(ex);
}
} else if(!(o.getName().equals(".svn")
|| o.getName().equals(".cvs")
|| o.getName().equals(".hg"))) {
// Add all folders that are not version control
Node addonFolderNode = DataFolder.findFolder(o).getNodeDelegate();
nodes.add(new AddonFolderNode(addonFolderNode, o));
}
}
} else {
// Add data files
for(FileObject o : Collections.list(folder.getData(false))) {
if (o.hasExt(key)) {
try {
Node addonNode = DataObject.find(o).getNodeDelegate();
nodes.add(new FilterNode(addonNode, Children.LEAF));
} catch (DataObjectNotFoundException ex) {
Exceptions.printStackTrace(ex);
}
}
}
}
return(nodes.toArray(new Node[nodes.size()]));
}


private static final class AddonFolderNode extends FilterNode {

FileObject folder;

public AddonFolderNode(Node node, FileObject folder) {
super(node, Children.create(new AddonChlidFactory(folder), true));
this.folder = folder;
}

@Override
public Action[] getActions(boolean arg0) {
Action[] nodeActions = new Action[1];
nodeActions[0] = CommonProjectActions.newFileAction();
return nodeActions;
}

@Override
public String getDisplayName() {
return folder.getName();
}

}

private static final class nbProjectNode extends FilterNode {

public nbProjectNode(FileObject node) throws DataObjectNotFoundException {
super(DataFolder.findFolder(node).getNodeDelegate());
}

@Override
public String getDisplayName() {
return "Important Files";
}
}

}


Finally on our project view we get (look that the svn folder is gone, the nbproject fodler is renamed to Important Files and how files are sorted by type):

Thursday, July 29, 2010

Project Type Module + Privileged Templates

I started working on a Project Module for managing WoW addons (Lua Topping). After fidling arround I got the nodes to work as intended (posted here) I wanted to have a nice set of templates for the basic addon files: lua, toc and xml, plus some specific to WowAce. You can look at the NetBeans tutorial here.

Taking a look at the NetBeans API and the Project class I found that for the lookup you could add a PrivilegedTemplates interface. This interface only has one method:

String[] getPrivilegedTemplates()


According to the API documentation the returned string array should contain "full paths to privileged templates, e.g. Templates/Other/XmlFile.xml". But what is the correct full path for a template?. The simplest way to find out is to view your layer.xml file (you should read about NetBeans and layer files if you want details on how they work). In my case, the Templates section of my layer.xml file looks like this:


<folder name="Templates">
<folder name="WoW">
<file name="TocTemplate.toc" url="TocTemplate.toc">
<attr name="displayName" bundlevalue="net.sourceforge.luatopping.wow.addon.project.Bundle#Templates/WoW/TocTemplate.toc"/>
<attr name="template" boolvalue="true"/>
</file>
<file name="WoWUIxmlTemplate.xml" url="WoWUIxmlTemplate.xml">
<attr name="displayName" bundlevalue="net.sourceforge.luatopping.wow.addon.project.Bundle#Templates/WoW/WoWUIxmlTemplate.xml"/>
<attr name="template" boolvalue="true"/>
</file>
<file name="Template.pkgmeta" url="Template.pkgmeta">
<attr name="displayName" bundlevalue="net.sourceforge.luatopping.wow.addon.project.Bundle#Templates/WoW/Template.pkgmeta"/>
<attr name="template" boolvalue="true"/>
</file>
</folder>
</folder>


It turns out the correct full path for a template is the path starting at Templates/ and finishing at the template name (as in the name property of the file entry), containing all the nested sub folders. So in my case, for the TocTemplate.toc template this would be: Templates/WoW/TocTemplate.toc.

Putting it all together (assuming you have followed the NetBeans tutorial):
  1. Add a PrivilegedTemplates implementation to your project lookup

    //The project type's capabilities are registered in the project's lookup:
    @Override
    public Lookup getLookup() {
    if (lkp == null) {
    lkp = Lookups.fixed(new Object[]{
    state, //allow outside code to mark the project as needing saving
    new ActionProviderImpl(), //Provides standard actions like Build and Clean
    new WowAddonDeleteOperation(),
    new WowAddonCopyOperation(this),
    new Info(), //Project information implementation
    new WowAddonProjectLogicalView(this), //Logical view of project implementation
    new WowAddonProjectCustomizer(this), //Customizer for the project
    new WowAddonProjectPrivilegedTemplates(),
    });
    }
    return lkp;
    }


  2. In you PrivilegedTemplates implementation construct the desired templates names array

    private final class WowAddonProjectPrivilegedTemplates implements PrivilegedTemplates {

    private String[] privileged = new String[]{
    "Templates/WoW/TocTemplate.toc",
    "Templates/Other/LuaTemplate.lua",
    "Templates/WoW/WoWUIxmlTemplate.xml",
    "Templates/WoW/Template.pkgmeta",
    };

    public String[] getPrivilegedTemplates() {
    return privileged;
    }
    }



That's it!. When you run your Project Module, your templates should be visible when you right click the project node and select the New option: