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):