Writing POV-Ray Support for NetBeans III - Implementing a Project Type
Tim Boudreau
6 June 2006
Feedback
This is a DRAFT!
This is a continuation of the tutorial for building POV-Ray support for
NetBeans. If you have not read the
first
or
second parts of this tutorial, you may
want to start there.
Setting Up Dependencies
There are a few APIs which we will need to use classes from - so before
starting to code, let's add dependencies from the Povray Projects module
to those:
- Right-click the Povray Projects project in the Projects tab in the IDE
package, and choose Properties from the popup menu
- On the Libraries page of the project properties dialog, click the
Add button
- In the dialog that appears, type "ProjectFactory". The
Projects API module should become selected in the list below - it is
the module that provides this class, so we need a dependency on it to
be able to use the class.
- Press Enter or Click OK, and then either press Tab and then Space,
or click the Add button to add another dependency
- In the add dependency dialog, type "FileObject". When
"FileSystems API" becomes selected, press Enter or click OK.
- Repeat these steps again, typing the name "Lookup", and
adding a dependency on the Utilities API.
- Repeat these steps yet again, typing the name "AbstractNode", and
adding a dependency on the Nodes API.
- Repeat these steps once again, typing the name "DataFolder", and
adding a dependency on the Loaders API.
- Repeat these steps once more, typing the name "LogicalViewProvider", and
adding a dependency on the Project UI API.
- Press Enter or Click OK twice to dismiss both dialogs.
Creating the Project Factory
As with
DataObjects and
DataLoaders, the system
keeps a registry of things that can identify a directory as being a project
and create a
Project object to represent it. So the first step
in creating a our own project type is to create a factory - an implementation of
ProjectFactory from the Projects API - which can figure
out if a directory is a POV-Ray project and, if it is one, make an instance of our
Project implementation for it.
- Right-click the
org.netbeans.examples.modules.povfile
package, and choose New > Java Class
- In the New File wizard that appears, enter the name
"PovProjectFactory"
- Press Enter or click OK to create the new file
- In the code editor, modify the signature line of PovProjectFactory
as follows:
public class PovProjectFactory implements ProjectFactory {
- Press Alt-Shift-F (Ctrl-Shift-F on Macintosh) to Fix Imports.
- Position the caret somewhere in the class signature line. When the
lightbulb glyph appears in the margin, press Alt-Enter, and then Enter
again to accept the hint "Implement All Abstract Methods"
- Add the following constants to the head of the
PovProjectFactory
class definition, as follows:
public class PovProjectFactory implements ProjectFactory {
public static final String PROJECT_DIR = "pvproject";
public static final String PROJECT_PROPFILE = "project.properties";
- The first method we will implement is the
isProject()
method. This method needs to be very fast - it should determine whether
or not a directory is a project as quickly as possible, because it will
be called once for each directory shown in the file chooser when the
user selects File > Open Project.
Implement the method as follows:
public boolean isProject(FileObject projectDirectory) {
return projectDirectory.getFileObject(PROJECT_DIR) != null;
}
This simple test for the presence of a subdirectory called
"pvproject" is all we need to determine that something is
not one of our projects.
Next, we will implement the code that actually loads a project, given a
directory. The project system handles caching of projects, so all that's
needed here is to create a new project:
public Project loadProject(FileObject dir, ProjectState state) throws IOException {
return isProject (dir) ? new PovrayProject (dir, state) : null;
}
The only interesting thing here is the ProjectState object, which
we pass along with the directory to our project's constructor. It is provided
to us by the project system, and can be used to mark a project as needing to
be saved. We will use it later to do that when the user changes the main file
of the project, which will be written to disk in the project.properties
when our project is closed.
-
The final thing to implement is
saveProject() - this is what will
write out any unsaved changes to disk when a POV-Ray project is closed, or
when NetBeans shuts down:
public void saveProject(final Project project) throws IOException, ClassCastException {
FileObject projectRoot = project.getProjectDirectory();
if (projectRoot.getFileObject(PROJECT_DIR) == null) {
throw new IOException ("Project dir " + projectRoot.getPath() + " deleted," +
" cannot save project");
}
//Force creation of the scenes/ dir if it was deleted
((PovrayProject) project).getScenesFolder(true);
//Find the properties file pvproject/project.properties,
//creating it if necessary
String propsPath = PROJECT_DIR + "/" + PROJECT_PROPFILE;
FileObject propertiesFile = projectRoot.getFileObject(propsPath);
if (propertiesFile == null) {
//Recreate the properties file if needed
propertiesFile = projectRoot.createData(propsPath);
}
Properties properties = (Properties) project.getLookup().lookup (Properties.class);
File f = FileUtil.toFile(propertiesFile);
properties.store(new FileOutputStream(f), "NetBeans Povray Project Properties");
}
We haven't written the PovrayProject yet, but from this code it's pretty clear
what it will look like: We are creating the scenes/ directory if it
does not exist or was deleted; we fetch a Properties object out of the project's
Lookup, and save it into pvproject/project.properties - that's
all there is or will be to saving a POV-Ray project.
Registering the Project Factory
The system needs to know about our project type, for this module to do anything.
We will register our project type into the
default
lookup using the technique of adding a file to
META-INF/services
in our module's jar (if you are using NetBeans 6.0, you should simply be
able to right-click
PovProjectFactory and choose Register as a
Service; these instructions apply to NetBeans 5.0 and 5.5):
- Right-click the Source Packages node underneath the Povray Projects
project in the Projects tab in the IDE. Choose New > File/Folder
- In the New File wizard, select Other > Folder and click Next or
press Enter
- Enter the name "META-INF/services" for the folder name and
press Enter to create the folder
- Right-click the newly created folder and choose New > File/Folder
again
- This time, in the New File wizard, select Other > Empty File and
click Next or press Enter
- When prompted, enter the file name
"org.netbeans.spi.project.ProjectFactory"
and click Finish
or press Enter to create the new empty file
- When the new empty file open in the editor, enter one line of text:
org.netbeans.examples.modules.povproject.PovProjectFactory
and press Enter to end the file with a newline.
That's all it takes to register our project type, so that when our module is
loaded, it NetBeans will start recognizing POV-Ray projects.
Implementing PovrayProject
Now we need to create the Java class that represents a POV-Ray project - this is
what our
PovProjectFactory will create if the user opens a project
that it owns. The Project API in NetBeans is quite simple. A "project",
programmatically is the association of a directory on disk with a
Lookup -
a bag-o-stuff that can be queried for known interfaces. The Project API then
defines some interfaces and classes that should be available from a
Project's
Lookup.
So the first thing will be to create our implementation of
org.netbeans.api.project.Project.
- Right-click the
org.netbeans.examples.modules.povproject
package in the Povray Projects project, and choose New > Java Class
- In the New File wizard that appears, enter the name
"PovrayProject"
- Press Enter or click OK to create the new file
- In the code editor, modify the signature line of
PovrayProject
as follows:
public final class PovrayProject implements Project {
- Press Alt-Shift-F (Ctrl-Shift-F on Macintosh) to Fix Imports.
- Position the caret somewhere in the class signature line. When the
lightbulb glyph appears in the margin, press Alt-Enter, and then Enter
again to accept the hint "Implement All Abstract Methods"
- Implement the top of the file as follows:
public final class PovrayProject implements Project {
public static final String SCENES_DIR = "scenes"; //NOI18N
public static final String IMAGES_DIR = "images"; //NOI18N
private final FileObject projectDir;
LogicalViewProvider logicalView = new PovrayLogicalView(this);
private final ProjectState state;
public PovrayProject(FileObject projectDir, ProjectState state) {
this.projectDir = projectDir;
this.state = state;
}
public FileObject getProjectDirectory() {
return projectDir;
}
FileObject getScenesFolder(boolean create) {
FileObject result =
projectDir.getFileObject(PovProjectFactory.SCENES_DIR);
if (result == null && create) {
try {
result = projectDir.createFolder (PovProjectFactory.SCENES_DIR);
} catch (IOException ioe) {
ErrorManager.getDefault().notify(ioe);
}
}
return result;
}
FileObject getImagesFolder(boolean create) {
FileObject result =
projectDir.getFileObject(IMAGES_DIR);
if (result == null && create) {
try {
result = projectDir.createFolder (IMAGES_DIR);
} catch (IOException ioe) {
ErrorManager.getDefault().notify(ioe);
}
}
return result;
}
The last two methods we will use later on - they define (and can create) the
scenes code and images folders that POV-Ray source
files and their resulting image files will go into when the project is
rendered.
- The actually interesting code goes into our implementation of
getLookup().
Eventually we will put some of our own interfaces into the project's Lookup;
for now it will be mainly standard stuff - interfaces provided by the Project API
module which we will implement. Implement getLookup() as follows:
private Lookup lkp;
public Lookup getLookup() {
if (lkp == null) {
lkp = Lookups.fixed(new Object[] {
this, //project spec requires a project be in its own lookup
state, //allow outside code to mark the project as needing saving
new ActionProviderImpl(), //Provides standard actions like Build and Clean
loadProperties(), //The project properties
new Info(), //Project information implementation
logicalView, //Logical view of project implementation
});
return lkp;
}
- The one interesting thing in the code above
is the call to
loadProperties() - for
storing project settings, we will use a properties file. So all we do here
is read it into a Properties object, and make that object available
through the project's Lookup. We will want to save any changes
made to this properties object, so we'll use a bit of cleverness and create
a Properties subclass that will mark the project as needing
saving whenever something calls put():
private Properties loadProperties() {
FileObject fob = projectDir.getFileObject(PovProjectFactory.PROJECT_DIR +
"/" + PovProjectFactory.PROJECT_PROPFILE);
Properties properties = new NotifyProperties(state);
if (fob != null) {
try {
properties.load(fob.getInputStream());
} catch (Exception e) {
ErrorManager.getDefault().notify(e);
}
}
return properties;
}
private static class NotifyProperties extends Properties {
private final ProjectState state;
NotifyProperties (ProjectState state) {
this.state = state;
}
public Object put(Object key, Object val) {
Object result = super.put (key, val);
if (((result == null) != (val == null)) || (result != null &&
val != null && !val.equals(result))) {
state.markModified();
}
return result;
}
}
Other than that, the things in the Lookup are what should typically be found
in the Lookup of any project - the project itself (the project
infrastructure reserves the right to wrap any Project type in a wrapper Project
object, so this guarantees being able to get at the real project instance),
its state, an ActionProvider to handle standard commands like
Build and Clean, a ProjectInformation implementation that
supplies the display name and icon for the project. The last thing in the
lookup is the logical view which we will come to next - this is what
provides a Node for the project that will be displayed on the
Projects tab in NetBeans.
- There are two remaining classes we need to create - the implementations of
ActionProvider and ProjectInformation. We will simply
stub these for now - add these two classes as inner classes of PovrayProject:
private final class ActionProviderImpl implements ActionProvider {
public String[] getSupportedActions() {
return new String[0];
}
public void invokeAction(String string, Lookup lookup) throws IllegalArgumentException {
//do nothing
}
public boolean isActionEnabled(String string, Lookup lookup) throws IllegalArgumentException {
return false;
}
}
/** Implementation of project system's ProjectInformation class */
private final class Info implements ProjectInformation {
public Icon getIcon() {
return new ImageIcon (Utilities.loadImage(
"org/netbeans/modules/PovrayProject/resources/PovRayIcon.gif"));
}
public String getName() {
return getProjectDirectory().getName();
}
public String getDisplayName() {
return getName();
}
public void addPropertyChangeListener (PropertyChangeListener pcl) {
//do nothing, won't change
}
public void removePropertyChangeListener (PropertyChangeListener pcl) {
//do nothing, won't change
}
public Project getProject() {
return PovrayProject.this;
}
}
Implementing the Logical View
One line in the code you entered above should be marked as being an error:
LogicalViewProvider logicalView = new PovrayLogicalView(this);
In the NetBeans IDE, what you see on the Projects tab is a "logical
view" of your project. This is a view that may not exactly reflect
the structure of files on disk (the Files tab is for that), but is more
convenient to work with - for example, collapsing a tree of directories
into a single node with a Java package name.
What we will implement now is a LogicalViewProvider. This is
basically a factory that produces a
Node
that represents the project. What child Nodes that Node
has, and what actions are available on them is up to us.
- Right-click the
org.netbeans.examples.modules.povproject
package in the Povray Projects project, and choose New > Java Class
- In the New File wizard that appears, enter the name
"PovrayLogicalView"
- Press Enter or click OK to create the new file
- In the code editor, modify the signature line of
PovrayLogicalView
as follows:
class PovrayLogicalView implements LogicalViewProvider {
- Press Alt-Shift-F (Ctrl-Shift-F on Macintosh) to Fix Imports
- Position the caret somewhere in the class signature line. When the
lightbulb glyph appears in the margin, press Alt-Enter, and then Enter
again to accept the hint "Implement All Abstract Methods"
We now have a skeleton implementation of our logical view.
Part of the value of having a concept of a project is the ability to present
data in a way that is closer to the way a user will think about their
project than the structure of files on disk may be. The logical view of a project
should present a simplified structure showing users what they need to
get their work done.
In our case, we already decided that
the user did not need to see the images/ subdirectory, they should
just be able to click a scene file and choose View, and that we want
to put scene files in a scenes/ subdirectory. So the logical
thing to do for our logical view is to have it show the contents of that
scenes/ directory. We can return whatever Node we
want as the root of our logical view of the project, and NetBeans makes
using the content of the scenes/ subdirectory very easy.
In the Nodes API is a class called FilterNode. What it does is
wrap an existing Node, and by default, simply expose the same
child nodes, display name, icon, actions, etc. as the original. We can
subclass FilterNode to change its icon and the set of actions
available on it. The DataLoader infrastructure already provides
a loader that recognizes Filesystem folders - an API class called
DataFolder. So we get the original node for the folder for free -
we just need to provide a subclass that uses our icon and (eventually) actions.
- We can now implement
PovrayLogicalView as follows:
class PovrayLogicalView implements LogicalViewProvider {
private final PovrayProject project;
public PovrayLogicalView(PovrayProject project) {
this.project = project;
}
public org.openide.nodes.Node createLogicalView() {
try {
//Get the scenes directory, creating if deleted
FileObject scenes = project.getScenesFolder(true);
//Get the DataObject that represents it
DataFolder scenesDataObject =
DataFolder.findFolder (scenes);
//Get its default node - we'll wrap our node around it to change the
//display name, icon, etc.
Node realScenesFolderNode = scenesDataObject.getNodeDelegate();
//This FilterNode will be our project node
return new ScenesNode (realScenesFolderNode, project);
} catch (DataObjectNotFoundException donfe) {
ErrorManager.getDefault().notify(donfe);
//Fallback - the directory couldn't be created -
//read-only filesystem or something evil happened
return new AbstractNode (Children.LEAF);
}
}
/** This is the node you actually see in the project tab for the project */
private static final class ScenesNode extends FilterNode {
final PovrayProject project;
public ScenesNode (Node node, PovrayProject 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;
}
public Image getIcon(int type) {
return Utilities.loadImage (
"org/netbeans/examples/modules/povproject/resources/scenes.gif");
}
public Image getOpenedIcon(int type) {
return getIcon(type);
}
public String getDisplayName() {
return project.getProjectDirectory().getName();
}
}
public Node findPath(Node root, Object target) {
//leave unimplemented for now
return null;
}
}
The interesting code above is in the method createLogicalView().
What we do there is quite simple and elegant - we have already decided that
there will be a scenes/ directory in our project, and that's
where new .pov and .inc files will be created.
And that is all we want to expose to the user when they interact with one of
our projects. So, we simply find the Node for that folder in
the real filesystem on disk, and wrap it in our own FilterNode,
which can expose whatever actions, icon, child Nodes or
properties we choose. Essentially, the logical view of the project is a
view of a subdirectory of the project, with a special icon and (eventually)
set of actions.
The final method, findPath() allows a user to use a keystroke to
select whatever they're editing in the Projects tab in the main window -
we will leave that unimplemented for now.
- One final thing we need to do is to provide the icon referenced from
PovrayLogicalView.ScenesNode.getIcon() above. Any 16x16 .gif
or .png file will do, or you can use
this one
. Create
a new java package "resources" underneath
org.netbeans.examples.modules.povproject, and copy or save the
image file there, modifying the file name in the source code if necesary.
Next Steps
We now have a working (albeit not terribly useful) implementation of POV-Ray
projects. As yet we have no way to create such a project on disk, but if you
were to have one, you could open it and view it.
If you want to test your code at this point, download this
zip file which contains such a project, unpack it to a temporary
directory and attempt to open it as a project.
The next tutorial will begin to add truly
useful functionality to our projects.