Sang Shin,
sang.shin@sun.com, Sun
Microsystems, www.javapassion.com
This course is INCOMPLETE. It is a draft ONLY. Starting Date to Be Announced. This course is INCOMPLETE. It is a draft ONLY. Starting Date to Be Announced. 
At the end of the course, you will have built support for POV-Ray: the Persistence Of Vision Raytracer. POV-Ray is an open-source 3D rendering engine: one that only a programmer could love. Rather than having a graphical modelling tool, it is based around a scene language which is "compiled" into an image.
We will walk through creating several NetBeans modules. One implements support for POV-Ray Scene Files. Another provides a POV-Ray project type, and allows scene files to be rendered into image files. We will also create a module that will provide POV-Ray samples for our users:
Right at the end, you will be able to open POV-Ray files and render them in the IDE. A few tweaks later, you'll have a separate rich-client application, independent of the IDE:
We are going to use a newly created Google alias (netbeanplatform@googlegroups.com) as of August 2008. Please register yourself by sending a blank email to the following. (Please read registration FAQ before you sign up.)
Basically, when you send a blank email to the above, you are subscribing our class forum. This forum can be used for posting questions/answers either by email or through the website. Please use this forum for all class related communication (technical or non-technical). It is strongly recommended you don't send an email directly to me. For further questions, please see course FAQ below. Please see the FAQ before posting questions to the class alias or sending an email directly to me.The general principles of participating in this course are outlined below:
Once you are up to speed, i.e., you have a basic background in Java and Swing, and you're interested in creating large, complex applications on top of a reliable framework, there should be nothing to stop you!
In order to receive a certificate for completing this course, you will need to successfully complete a 'Student Assignment', as described in detail at the end of this course. If you complete the Student Assignment, you will receive a certificate stating that you are a NetBeans Certified Engineer or a NetBeans Certified Contributor. One of these qualifications will follow automatically from completing this course in its entirety.
In the first week, once we have completed the Resources section, we will create a "module suite" project to contain all the modules we will create throughout this course. Next, also during this week, we will create basic support that enables NetBeans to recognize ".pov" files and to open them in the editor: which will give us a place to hang Actions, provide special icons, and enable other special support specific to ".pov" files.
Resources
Before continuing with the lesson, read through and understand (and, where applicable, take the steps described) in the following documents:
You are assumed to have become very familiar with all of the above documents before continuing.
Video
Watch this video, which will give you a basis for understanding the NetBeans Platform:
Next, read/follow these essential documents:
Lab
Since support for POV-Ray will be made up of multiple modules, we will start by creating a module suite: a container that can hold multiple interdependent modules.
.pov files: POV-Ray scene language files. Select
File > New Project again from the main menu. (Or right-click the Modules
node in the Projects window and choose "Add New...") In the New Project wizard, this time you'll choose NetBeans
Modules > Module:
You should see the following:
The first thing we will want to create is basic support that enables NetBeans to recognize .pov files and open them in the editor: which will give us a place to hang Actions, provide special icons, and other special support specific to .pov files.
A
DataLoader
is a factory which is registered against a particular MIME type or file extension.
It creates DataObjects: typically
one per file. So, when you expand a folder in the Projects window or Files window
in the IDE, what happens under the hood is that all of the files in the folder you expanded
are found, and for each file, the system locates the DataLoader for its
file type and asks it to create a DataObject for it. DataObjects
contain logic that enables them to actually parse or understand a files contents, or
know what to do with that particular type of file. So there is a 1:1 mapping
between file types and DataLoaders.
NetBeans 5.0 and up has a template called "File Type" that makes it
very easy to generate basic support for a new file type:
org.netbeans.examples.modules.povfile and
choose New > Other from the popup menu.
Select Module Development > File Type in the New File wizard:
Click Next or press Enter.
Click Next or press Enter.
Press Enter or click Finish and the IDE will generate the Java classes and metadata files needed for basic POV-Ray file support in NetBeans.
You should now have the following file structure in the povfile package:
org.netbeans.examples.modules.povfile/
Bundle.properties: A resource bundle for miscellaneous localized strings.layer.xml: A
module layer file which
allows the module to install some objects declaratively.povicon.gif: The icon you chose in the wizard, which will appear
whenever Povray files are edited or displayed in an explorer view.PovrayDataObject.java: This is the object that
understands what a .pov file is. If we were to provide advanced support for
POV-Ray files, we would probably parse those files here, and provide some sort
of model of the structure of the file that could be shown in Navigator or manipulated
programmatically.PovrayResolver.xml: This is a small bit of XML that
declares that .pov and .inc files map to the MIME type
text/x-povray (which we have invented for purposes of this tutorial).
This XML file is referenced from the module's layer.xml file.PovrayTemplate.pov: This is an empty template POV-Ray
file which can be modified and will be used as the basis of new POV-Ray files
in the New File wizard..pov file in your home
directory and then browse, for example, in the Favorites window (Window > Favorites
from the main menu), you will see that it is indeed recognized by NetBeans,
and has the icon that you specified:
DataLoader is registered in the layer file: if you expand
Povray File Support > Important Files and open the XML layer in
the editor, you will
see the following:
<folder name="Factories">
<file name="PovrayDataLoader.instance">
<attr name="SystemFileSystem.icon" urlvalue="nbresloc:/org/netbeans/examples/modules/povfile/povicon.gif"/>
<attr name="dataObjectClass" stringvalue="org.netbeans.examples.modules.povfile.PovrayDataObject"/>
<attr name="instanceCreate" methodvalue="org.openide.loaders.DataLoaderPool.factory"/>
<attr name="mimeType" stringvalue="text/x-povray"/>
</file>
</folder> |
layer.xml file now also contains a folder, defined in XML,
Loaders/text/x-povray/Actions, and all of the actions that
appear on the popup menu of a .pov or .inc file
are registered there. Currently the list only contains standard NetBeans
API actions such as Cut and Delete (as you can see in the screenshot above);
we will eventually add more.
Things to think about and/or activities to complete:
Next week we will cover designing and planning our project type and file support and how they will interrelate.
In this part of the course, we will walk through how to create a basic project type. It will be a project type that supports building 3D graphics scenes using POV-Ray's scene language, and eventually, rendering them as images and displaying the result in NetBeans.
It is assumed you have completed the steps in the previous session for creating basic POV-Ray support.
This session will go through the up-front design and the thinking through
needed to successfully implement a project type that will serve its users
well. The subsequent tutorial walks through the implementation of the basic
project.
Resources
This week, there will be no coding that you will need to do during the lab. You will be focusing on questions of design and thinking about how best to implement the project we're working on in this course.
Nevertheless, the resources for this week will let you get your hands dirty creating some complete NetBeans Platform applications, to give you a feel for what that entails. Do these tutorials slowly and carefully and try to understand everything you do. Think about the design of the application in question as you work through the tutorials. Therefore, before continuing with the lesson below, do the following tutorials:
Lab
A NetBeans project is a directory on disk; typically there is some signature, such as having a subdirectory with a specific name, which identifies that directory as being a project.
The major requirement of identifying a folder as being a project is that the test fail quickly: the code that determines that a folder is not a project must complete very quickly, because it is going to be called once for every folder visible in the file chooser that lets users open projects. We will stick with what works, and use a specific subdirectory name to identify our POV-Ray projects.
Notice that we implemented support for .pov and .inc
files in a separate module. Design-wise
this makes sense: data recognition
and project types are orthagonal: and it provides a good demonstration of
how to do loose coupling between modules.
So at this point we need to think about what we're designing, what functionality belongs to which module, and what will serve our users' needs best. Before we do any coding, some design has to happen. Here's what we know about POV-Ray and its usage patterns:
.pov files down
into image files.
.pov file can reference other files, which typically get
the extension .inc.
The biggest killer of projects is trying to "save the world": trying to provide every combination of every possible option to the user, such that the user is confronted with a bewildering array of choices when most of the time they want to do something very simple. Trying to support every imaginable permutation of anything leads to projects which are permanently six months from completion. So we need to limit the scope while providing value to the user. But we also want to keep power users happy and give them the ability to do what they need to do. So we have some choices to make, and some hard thinking to do about what is the right UI to balance the power of POV-Ray with the need to keep things simple and easy to use for beginners.
The first question, from item 1, is, which module owns the rendering infrastructure?
Well, when you render an image, you need a place to put it. A project owns
a directory and its subdirectories. So we'll make an executive decision right
now that we don't render a random .pov file on disk: it must
belong to a project which can provide a place to put the result. So the
code that actually calls out to POV-Ray will be part of the project support
module, not the file support module.
A follow-on to this is, should it be possible to render absolutely any .pov
or .inc file? Probably yes, due to item 6: but we'll only do
that if it belongs to a project.
Do we want to provide structure (subfolders, etc.) in POV-Ray projects? It would probably be nice for the future, but to keep the scope of things manageable, we're not going to do that now. In practice, POV-Ray projects can be just as messy as projects written in C, with source files scattered hither and yon. We are not going to try to solve all the world's problems here; but neither are we going to impose our own idea of what a well structured POV-Ray project looks like on the user.
How many of the myriad command-line options that POV-Ray supports should we
expose the user to? The real usage pattern here is that, for test renders,
any of a few sets of standard settings will do for almost all cases. So we
will provide a set of standard resolutions like 320x200, 640x480, 1024x768.
Those settings will be unmodifiable. But, associated with each project will
be a set of production settings, which are saved with the project, are
shareable and go with the project wherever it goes. We will provide a
basic GUI customizer for the production settings.
It will not cover every possible setting POV-Ray has: however, the
customizer will be writing into the project's project.properties file: and
a power user can edit in their own settings to do anything they want, so
the full power of POV-Ray stays available to power users: any desired line
switch can be encoded into the project's Properties file and end up passed
on the command line to POV-Ray.
Because of item 6, we know there can be multiple files in a project, but only one is the master scene that is the final output of the project. So we will give our projects the concept of a main file which is what gets rendered when they invoke Build, and we'll need a UI to select which file it is.
We also need to make some decisions about what will be stored on disk and where. We know that
projects typically have Build and Clean actions, and these will be useful
for POV-Ray projects as well. Where should
scene files go? Well, we want to keep some flexibility for future versions,
so we don't want them in the root directory of our projects: we'll create a
scenes/ folder for them.
We also know we will end up with image
files from rendering. The Clean operation will be much less complex to
implement if it just means deleting a directory. And the UI will
be less cluttered if we put images in their own
directory, so rendered images will go in an images/ subdirectory
of the project. The project UI won't even show the images/
directory: you'll just right click a scene file and select View,
to render and show a single file, or build the project to show the "main file".
As with compiling, we will do a date check to see if the source file is
newer than the image file, and if so, render it again.
We also know we need to use a subdirectory to identify our project type, and
to store configuration data that should be saved, such as which file is the
main file and what are the production renderer settings. So we'll decide that every
POV-Ray project will have a pvproject subdirectory, and in
that directory will be a properties file. So on disk,
a typical POV-Ray project will look like:
Typically a user is going to be dealing with files, and we already decided that
a user should be able to render any file in a project; and we know we are
going to have to give the user the ability to pick which file is the main file
for a project. That means our Nodes for .pov files
will need to have Actions that will be invoking rendering and main-file-setting
code that belongs to the project module, not to their module. So we know we
that the project module will need to expose some API for doing these things,
and the Node of a POV-Ray file will need to be able to find the
project that owns it and call this code. But if a .pov is
orphaned on disk, or is inside, say, a J2EE project for some reason, it
must fail gracefully.
This
gives a great opportunity to demonstrate how loose coupling between
modules is done. There are two simple mechanisms that will allow us to
easily do this: The first is FileOwnerQuery: this is a class
from the Project module, which allows one to find out what
project owns any file. The second is that Project, the interface
we implement to create our project type, has a method getLookup().
So we will provide some interfaces in the POV-Ray project type which we'll
make available as API. When a node finds out what project its file belongs
to, it can simply request the implementation of one of those interfaces.
If it gets non-null, it can call the rendering or main-file-setting functionality;
if it gets null, or the project is null, those actions will just be disabled.
Start thinking about an application that you will create on the NetBeans Platform! Or start thinking about a plugin that you will create. Read through the thoughts above again and then try to apply them to your own project. Try and plan your project and divide it into different stages: what will the first release consist of? Write down some requirements for your project and prioritize them. Then think about why something is prioritized higher than something else! Whose criteria did you use?
Design is an inescapable first-step in developing modules. It pays to think hard about what functionality belongs where, and what the user experience should be before starting to code.
Next week we will cover providing project support for POV-Ray.
In this section, you create a new project type for POV-Ray projects.
The NetBeans APIs enable you to extend existing
project types, either by removing or adding logical view nodes, lookup items, and customizer panels
from projects that already exist. If you are reading this section in order to
learn about creating a project type for your own scenario, you may want to consider taking the other
route (i.e., extend an existing project type rather than create a new one from scratch),
especially if the project type you want to create is similar to one of the existing types.
For information on this approach,
see the NetBeans Project Type Extension Module Tutorial.
Resources
Before continuing with the lesson, read through and understand (and, where applicable, take the steps described) in the following documents:
Lab
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:
Press Enter or click OK.
Press Enter or click OK to dismiss the Project Properties dialog.
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 Project 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.
org.netbeans.examples.modules.povproject
package, and choose New > Java Class. In the New File wizard that appears, enter the name
"PovrayProjectFactory":
Press Enter or click Finish to create the new file.
public class PovrayProjectFactory implements ProjectFactory { |
Press Ctrl-Shift-I to Fix Imports.
You should now see the following class definition:
public class PovrayProjectFactory implements ProjectFactory { public boolean isProject(FileObject arg0) { throw new UnsupportedOperationException("Not supported yet."); } public Project loadProject(FileObject arg0, ProjectState arg1) throws IOException { throw new UnsupportedOperationException("Not supported yet."); } public void saveProject(Project arg0) throws IOException, ClassCastException { throw new UnsupportedOperationException("Not supported yet."); } } |
PovrayProjectFactory
class definition:
Type Psfs and press the TAB key to quickly create 'public static final String' declarations.
public class PovrayProjectFactory implements ProjectFactory { public static final String PROJECT_DIR = "pvproject"; public static final String PROJECT_PROPFILE = "project.properties"; |
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, first replacing the method parameter's name from "arg0" to "projectDirectory", and then filling out the body of the method:
@Override 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. As before, make sure that the names of the method parameters match the names of the parameters in the body of the method:
@Override
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 (there is no "Save Project" action because the system will invoke saveProject() for us behind the scenes). 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 reference to a class called "PovrayProject" is now underlined in red,
because it does not yet exist. We will create it in a later step. Until then,
don't worry about the red underlined class reference.
saveProject(): this is what will
write out any unsaved changes to disk when a POV-Ray project is closed, or
when NetBeans shuts down:
@Override 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"); } |
Press Ctrl-Shift-I to Fix Imports.
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.
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:
Press OK and then press OK again.
The file contains a single line, consisting of the fully qualified name of the class that implements the class after which the file is named.
That's all it takes to register our project type, so that when our module is loaded, NetBeans will start recognizing POV-Ray projects.
Now we need to create the Java class that represents a POV-Ray project: this is
what our PovrayProjectFactory 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-of-stuff that can be queried for known interfaces or abstract Java classes. 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.
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 Finish to create the new file.
PovrayProject
as follows:
public final class PovrayProject implements Project { |
Press Ctrl-Shift-I to Fix Imports.
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(SCENES_DIR); if (result == null && create) { try { result = projectDir.createFolder(SCENES_DIR); } catch (IOException ioe) { Exceptions.printStackTrace(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) { Exceptions.printStackTrace(ioe); } } return result; } public Lookup getLookup() { throw new UnsupportedOperationException("Not supported yet."); } } |
Press Ctrl-Shift-I to Fix Imports.
The getScenesFolder method and the getImagesFolder method 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.
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; } |
Press Ctrl-Shift-I to Fix Imports. Three lines will still be underlined in red. In the coming steps, we will create the code that those lines reference.
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.
Properties subclass that will mark the project as needing
saving whenever something calls put(). Add the following to
the PovrayProject class and fix imports:
private Properties loadProperties() {
FileObject fob = projectDir.getFileObject(PovrayProjectFactory.PROJECT_DIR +
"/" + PovrayProjectFactory.PROJECT_PROPFILE);
Properties properties = new NotifyProperties(state);
if (fob != null) {
try {
properties.load(fob.getInputStream());
} catch (Exception e) {
Exceptions.printStackTrace(e);
}
}
return properties;
}
private static class NotifyProperties extends Properties {
private final ProjectState state;
NotifyProperties (ProjectState state) {
this.state = state;
}
@Override
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 window in NetBeans IDE.
ActionProvider and ProjectInformation. We will simply
stub these for now: add these two classes as inner classes of PovrayProject:
private final class ActionProviderImpl implements
|
When you fix imports, make sure that the Utilities class you use is org.openide.util.Utilities.
At this point, only one line in the code you entered above should be marked as being an error:
|
LogicalViewProvider logicalView = new PovrayLogicalView(this); |
In the IDE, what you see in the Projects window is a "logical view" of your project. This is a view that may not exactly reflect the structure of files on disk (the Files window 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.
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 Finish to create the new file.
PovrayLogicalView
as follows:
class PovrayLogicalView implements LogicalViewProvider { |
Press Ctrl-Shift-I to Fix Imports.
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 and then fix imports:
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 it 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
|
When you fix imports, make sure to include org.openide.filesystems.FileObject and org.openide.util.ImageUtilities.
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 window: we
will leave that unimplemented for now.
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. Similarly,
earlier we referred to povicon.gif, for which purpose you can use
this one
org.netbeans.examples.modules.povproject.resources:
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.
Choose two (or more!) of the tasks below:
FileUtil class
in section 3.2. Read the related Javadoc very carefully, because it is
a key class, providing a lot of useful functionality that you will be needing
over and over again. Can you use 3 methods from that class within one brand new module? How about 5? 10? How many methods
from this class can you squeeze into one single module?
Next week we will begin to add truly useful functionality to our projects... project templates.
We left off with a module that lets us open POV-Ray projects, and basic support for POV-Ray files, but no way to create a new POV-Ray project.
So the first step is to add the ability to use the New Project Wizard to create POV-Ray projects. NetBeans 5.0 and up has the ability to embed a project in a module as a ZIP file, and add the necessary configuration and code to make it available from the New Project wizard and unpack it in a directory of the user's choice. We will make use of that functionality to create our project templates.
Resources
Before continuing with the lesson, read through and understand (and, where applicable, take the steps described) in the following documents:
Lab
First we need to have a project to ZIP up, so we will create that by hand. You can do this in the IDE, just by creating the appropriate folders and files.
povsamples:
Click Next or press Enter.
org.netbeans.examples.modules.povsamples.
In Module Display Name, type Povray Project Samples.
Click Finish or press Enter. The module suite should now contain three modules:
org.netbeans.examples.modules.povsamples, in the Files window:
templates/ : a root directory for our template projects
EmptyPovrayProject/ : Base directory for an empty project
images/ pvproject/
project.properties scenes/
EmptyPovrayProject.pov SamplePovrayProject/ : Base directory for a project with sample .pov files
images/ pvproject/
project.properties scenes/
SamplePovrayProject.pov You should now see the following in the Files window:
Now we are ready to add our sample projects: but here we have to cheat just a little: the IDE will only package up a sample project that it has open, and in our development IDE we don't have support for POV-Ray projects, so our hand-created projects won't be recognized. But, we already have a module that provides support for POV-Ray projects available. So we will cheat just a little bit and use that to fool the IDE into embedding our new POV-Ray projects in our module:
templates/EmptyPovrayProject
and templates/SamplePovrayProject in that instance of the
IDE. Optionally, you can do so from the Favorites window, as shown here:
org.netbeans.examples.modules.povproject.povsamples and
choose New > Other. In the New File Dialog, select Module Development >
Project Template:
Press Enter or click Next.
Click Next or press Enter.
.empty at the end of the Package name,
so that the package name will become org.netbeans.examples.modules.povsamples.empty:
Click Finish or press Enter.
templates/SamplePovrayProject,
calling it Sample Povray Project, and using the package name org.netbeans.examples.modules.povsamples.sample:
Close the copy of NetBeans that has our modules installed: it's done its job for now.
The above steps created a number of files on disk: there are two new ZIP files in our module that are zipped copies of the projects. The following new files were created. Some of them we can delete, as instructed later below: our two templates can share wizard code for instantiating them from the New Project wizard:
povsamples/
EmptyPovrayProjectProject.zip SamplePovrayProjectProject.zip empty/
EmptyPovrayProjectDescription.html
Description that will be shown when the user selects the project
in the New Project wizardEmptyPovrayProjectPanelVisual.form
Form file for the panel in the wizard that lets the project be
named, which we can customizeEmptyPovrayProjectPanelVisual.java Corresponding
Java file for the templateEmptyPovrayProjectWizardIterator.java A
"wizard iterator" which provides the additional steps in
the wizard after selecting the templateEmptyPovrayProjectWizardPanel.java A
wrapper object that delays creating EmptyProjectPanelVisual
until the user navigates to that page in the wizardsample/
SamplePovrayProjectDescription.html Description
that will be shown when the user selects the sample project in
the New Project wizardSamplePovrayProjectPanelVisual.form can be deleted SamplePovrayProjectPanelVisual.java can be deleted SamplePovrayProjectWizardIterator.java can be deleted SamplePovrayProjectWizardPanel.java can be deleted The layer.xml file has also been modified, to register these two
projects so they are added to the New Project wizard. The following code has been
added to the layer.xml:
<folder name="Templates">
<folder name="Project">
<folder name="Standard">
<file name="EmptyPovrayProjectProject.zip" url="EmptyPovrayProjectProject.zip">
<attr name="SystemFileSystem.localizingBundle"
stringvalue="org.netbeans.examples.modules.povsamples.Bundle"/>
<attr name="instantiatingIterator"
methodvalue="org.netbeans.examples.modules.povsamples.empty.EmptyPovrayProjectWizardIterator.createIterator"/>
<attr name="instantiatingWizardURL"
urlvalue="nbresloc:/org/netbeans/examples/modules/povsamples/empty/EmptyPovrayProjectDescription.html"/>
<attr name="template" boolvalue="true"/>
</file>
<file name="SamplePovrayProjectProject.zip" url="SamplePovrayProjectProject.zip">
<attr name="SystemFileSystem.localizingBundle"
stringvalue="org.netbeans.examples.modules.povsamples.Bundle"/>
<attr name="instantiatingIterator"
methodvalue="org.netbeans.examples.modules.povsamples.sample.SamplePovrayProjectWizardIterator.createIterator"/>
<attr name="instantiatingWizardURL"
urlvalue="nbresloc:/org/netbeans/examples/modules/povsamples/sample/SamplePovrayProjectDescription.html"/>
<attr name="template" boolvalue="true"/>
</file>
</folder>
</folder>
</folder>
|
The above XML looks a lot to chew on, but it's relatively simple. For each
template, we declare a file in Templates/Project/Standard: this
means that these files will be findable at runtime in the System Filesystem
(a kind of virtual configuration data store composed from XML layer files,
which uses the same Filesystem interface you use in NetBeans
modules to access files on disk). The attributes are key/value pairs
that have some special meanings when applied to a template:
WizardIterator: a
factory for additional pages in the New Project wizard.
Since these templates are nearly identical, we don't need separate classes
for the New Project wizard for each one: one class will do nicely for both
templates. So we will make one change to the layer file, and delete some of
the classes that were generated:
layer.xml in the code editor. Find the line:
<attr name="instantiatingIterator"
methodvalue="org.netbeans.examples.modules.povsamples.empty.EmptyPovrayProjectWizardIterator.createIterator"/>
and copy it to the clipboard (Ctrl-C).SamplePovrayProjectProject.zip, which declares the
"instantiatingIterator" and select it.SamplePovrayProjectPanelVisual.java,
SamplePovrayProjectWizardIterator.java and
SamplePovrayProjectWizardPanel.java.
Our initial sample projects are probably not in their final form, so it would be nice to have the build script automatically rebuild the ZIP files of the sample projects whenever we build the Povray Projects module: that way we can simply modify the sample sources at will, and whenever we do a build they will be up-to-date. So we'll make a few changes to the build script:
Add the following targets to the Ant build script for Povray Project Samples:
<target name="netbeans" depends="package-samples,projectized-common.netbeans"/>
<target name="package-samples">
<delete file="${basedir}/src/org/netbeans/examples/modules/povsamples/EmptyPovrayProjectProject.zip"/>
<delete file="${basedir}/src/org/netbeans/examples/modules/povsamples/SamplePovrayProjectProject.zip"/>
<zip compress="9" basedir="src/org/netbeans/examples/modules/povsamples/templates/EmptyPovrayProject/"
zipfile="${basedir}/src/org/netbeans/examples/modules/povsamples/EmptyPovrayProjectProject.zip"/>
<zip compress="9" basedir="src/org/netbeans/examples/modules/povsamples/templates/SamplePovrayProject/"
zipfile="${basedir}/src/org/netbeans/examples/modules/povsamples/SamplePovrayProjectProject.zip"/>
</target>
|
If we were using a version control such as CVS to store our source code, now
would be a good time to mark the two ZIPs as ignored (add them to .cvsignore
or equivalent).
Create a project in NetBeans IDE. It can be any kind of project, either a Swing project, or a web application, or a mobile application, anything at all. Or create a project of the kind you provided support for last week. Now turn that project into a project template! Question to think about: what is the difference between a project type, a project template, and a project sample?
Create a JUnit test for the code created during this week. Which pieces of code does it make sense to test? To help you, read the "Writing tests" section of the NetBeans Developer FAQ.
As discussed when we designed POV-Ray project support, we will need an API: there will be some intercommunication between POV-Ray files and the project.
Resources
Before continuing with the lesson, do everything described in these two tutorials :
Conceptually, the above two tutorials cover the most important concepts relating
to the NetBeans Platform.
Video
Watch this video, which will give you a basis for understanding the Lookup API in the NetBeans Platform:
Then read this article and follow the provided instructions: How Do NetBeans Extension Points Work?
Lab
We will need some interfaces:
MainFileProvider: find the main file of a project: the
one to render when the whole file is built, and allow a POV-Ray
scene file node to find out if it is the main file (so it can
bold-face its display name).RendererService: an API a POV-Ray file node can call
to ask that it be rendered as an image.ViewService: an API a POV-Ray file can call to ask
that its associated image be shown in the IDE, rendering it if necessary.
For this, we will actually create a separate module. That way we avoid a
dependency between file support and project support: either module will be
loadable by itself as long as the module providing the API is there. Also,
this helps in delivering updates: the API presumably will remain stable,
and psychologically having it in a separate module helps the developer to be
aware when they are making API changes. It also means that a completely
different module supporting POV-Ray files could still get them rendered
via this API, and completely different project support could be provided
with no changes to the file support module.
So it's generally healthy for the codebase to do it this way.
Click Next or press Enter.
org.netbeans.api... to
indicate visually that they are intended to be API (and thus kept
backward compatible). Provide the display name "Povray API":
Click Finish or press Enter to create the project.
AbstractFileSystem for a fast way
to find it):
Click OK and then click OK again.
org.netbeans.examples.modules.api.povray
called MainFileProvider, and implement it as follows:
public abstract class MainFileProvider {
public abstract FileObject getMainFile();
public abstract void setMainFile (FileObject file);
public boolean isMainFile (FileObject obj) {
return obj.equals(getMainFile());
}
} |
When you fix imports, make sure to import org.openide.filesystems.FileObject.
org.netbeans.modules.examples.api.povray
called RendererService, and implement it as follows:
public static final String PROJECT_RENDERER_KEY_PREFIX = "renderer.";
public static final String PRODUCTION_RENDERER_SETTINGS_NAME = "production";
public abstract FileObject render(FileObject scene, String propertiesName);
public abstract FileObject render (FileObject scene, Properties renderSettings);
public abstract FileObject render (FileObject scene);
public abstract FileObject render();
public abstract String[] getAvailableRendererSettingsNames();
public abstract Properties getRendererSettings (String name);
public abstract String getPreferredRendererSettingsNames();
public abstract String getDisplayName (String settingsName);
|
When you fix imports, make sure to import org.openide.filesystems.FileObject.
org.netbeans.modules.examples.api.povray
called ViewService, and implement it as follows:
boolean isRendered (FileObject file); boolean isUpToDate (FileObject file); void view (FileObject file); |
When you fix imports, make sure to import org.openide.filesystems.FileObject.
If you are wondering why the first two are abstract classes instead of interfaces,
the answer is simple: In the case of MainFileProvider, it allows
us to implement isMainFile(); in the case of the other
RendererService, it is highly probably that there will be new
requirements for it in the future, and you can add methods [with some sort of
default implementation] to an abstract class semi-backward-compatibly [name
collisions with subclasses are still possible], but not to an interface.
ViewService is simple and well-defined enough that it will probably never
change.
You have now exposed the package containing your API classes to other modules within the suite. Click OK to dismiss the Project Properties dialog.
Now both of the modules that need them can see the API classes, but not each others' classes.
We haven't implemented the API yet, but we can set up some code that will use it: we know we want the node for the file which is the "main file" of our project to be shown in bold text. And having some code that uses the API will help to test it once it is written, which will be a bit of work.

FileOwnerQuery is part
of that API: a class with static methods that will return the project
(if any) which owns a given file. Our Node will need to
look up the project it belongs to, and then query the project's Lookup
to try to find an implementation of our API classes.
PovrayDataNode class:
public class PovrayDataNode extends DataNode { public PovrayDataNode(PovrayDataObject obj, Lookup lookup) { super(obj, Children.LEAF, lookup); } private FileObject getFile() { return getDataObject().getPrimaryFile(); } private Object getFromProject (Class clazz) { Object result; Project p = FileOwnerQuery.getOwner(getFile()); if (p != null) { result = p.getLookup().lookup (clazz); } else { result = null; } return result; } private boolean isMainFile() { MainFileProvider prov = (MainFileProvider)getFromProject (MainFileProvider.class); boolean result; if (prov == null) { result = false; } else { FileObject myFile = getFile(); result = prov.isMainFile(myFile); } return result; } @Override public String getHtmlDisplayName() { return isMainFile() ? "<b>" + getDisplayName() + "</b>" : null; } } |
What the above code does is fairly straightforward:
getFile()
returns a FileObject (NetBeans
virtual
filesystem file) that this Node represents.
getFromProject tries to find the project that owns the file,
and if it finds one, queries its
Lookup,
asking it for an instance of the Class that was passed into
this method (e.g. one of the classes in the API we just defined).
isMainFile() uses the above two methods to decide if this
Node represents the "main file" of the project (the
one that should be rendered by POV-Ray if the user chooses to "build"
the project: POV-Ray supports file includes, so there may be many files in
a project, but only one master image). getHtmlDisplayName() is
where the rubber meets the road: this method will return a boldface
HTML string if this Node represents the main file.
@Override protected Node createNodeDelegate() { return new PovrayDataNode(this, getLookup()); //return new DataNode(this, Children.LEAF, getLookup()); } |
The two tutorials you worked through at the start of this week are completed by these two. They're very important as well, covering essential features of the NetBeans Platform. Do them for homework and try to understand everything you do:
To prove to yourself that you understand the concepts presented inthe ne the two tutorials above, use the Nodes API to create a generic hierarchy over a set of data (any kind of data) and then use the Explorer & Property Sheet API to provide a view on top of your nodes to the end user.
How would you use these APIs outside of the NetBeans Platform? What would be the benefits of doing so? Can you create a scenario that shows the benefits of taking this approach?
This week, you will implement the API that you created in the previous lesson. You will be coding only within the Povray Projects project throughout this week: all the other modules will remain unchanged. (Think about why that might be a good thing and what the benefits of this are in a distributed development environment!)
Resources
Before beginning this lesson, spend some time getting to know the Visual Library API. It is not used within this lesson, but spending some time with this API will give you a good basis for creating visual applications. That's something that the NetBeans Platform helps you with in many ways, as you will find out during these tutorials. Do at least two of the tutorials below and make sure to understand everything you're doing as you're doing it:
Click on the image above to get to the Learning Trail
where the listed tutorials are found.
Video
Watch this video, which will give you a basis for understanding the System Filesystem, which you will be using this week:
Lab
The first class we will implement is MainFileProvider. This
is the class that our Nodes for POV-Ray files will look up,
to see if they represent the main scene file of the project (if so they will
display their name in boldface text, and later this will be used to provide
actions to set which file is the main file).
org.netbeans.examples.modules.povproject, and call it
"MainFileProviderImpl":
class MainFileProviderImpl extends MainFileProvider {
private final PovrayProject proj;
private FileObject mainFile = null;
private boolean checked = false;
MainFileProviderImpl(PovrayProject proj) {
this.proj = proj;
}
public FileObject getMainFile() {
//Try to look up the main file in the project properties
//the first time this is called; no need to look it up every
//time, either it's there or it's not and when the user sets it
//we'll save it when the project is closed
if (mainFile == null && !checked) {
checked = true;
Properties props = (Properties) proj.getLookup().lookup(
Properties.class);
String path = props.getProperty(PovrayProject.KEY_MAINFILE);
if (path != null) {
FileObject projectDir = proj.getProjectDirectory();
mainFile = projectDir.getFileObject(path);
}
}
if (mainFile != null && !mainFile.isValid()) {
return null;
}
return mainFile;
}
public void setMainFile(FileObject file) {
String projPath = proj.getProjectDirectory().getPath();
assert file == null ||
file.getPath().startsWith(projPath) :
"Main file not under project";
boolean change = ((mainFile == null) != (file == null)) ||
(mainFile != null && !mainFile.equals(file));
if (change) {
mainFile = file;
//Get the project properties (loaded from
//$PROJECT/pvproject/project.properties)
Properties props = (Properties) proj.getLookup().lookup(
Properties.class);
//Store the relative path from the project root as the main file
String relPath = file.getPath().substring(projPath.length());
props.put(PovrayProject.KEY_MAINFILE, relPath);
}
}
} |
The code above is simple and quite straightforward: it will look for
a Properties object in the Lookup of the
project. The getter will look for the value of "main.file"
from the Properties object (which was loaded from
$PROJECT/pvproject/project.properties), which will be
a relative path to the main file. If there is a value for that key, try
to find the corresponding file and return it. The setter, in turn, will
write a new relative path to the Properties object. That
in turn, will cause the project to be marked as modified, so the system
will call PovProjectFactory.saveProject() if it is unloading
the project, causing the Properties to be written out to
disk in $PROJECT/pvproject/project.properties.
PovrayProject:
public static final String KEY_MAINFILE = "main.file"; |
new MainFileProviderImpl(this) to the implementation
of getLookup() in PovrayProject, so that it
is included in the array of objects that make up the lookup contents:
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 new MainFileProviderImpl(this), }); } return lkp; } |
Now we have an implementation of some of our API, the next step is to use it.
As discussed earlier, we want the user to be
able to right-click and choose to render any file, not just the main file
of the project. So there should be some menu items available from our
PovrayDataNodes which will allow the user to render the file
with one of our sets of settings.
PovrayDataNode, from the Povray File Support project,
in the code editor.
Press Alt-Insert and override the
getActions(boolean) method. Implement it as follows:
@Override
public Action[] getActions(boolean popup) {
Action[] actions = super.getActions(popup);
Action[] result;
result = new Action[actions.length + 1];
result[0] = actions[0];
result[1] = new SetMainFileAction();
System.arraycopy(actions, 1, result, 3, actions.length - 1);
return result;
} |
This method will add one (yet to be implemented) action into the array
of actions. It positions
it as the second element in the array, since the first element is
what will be invoked when you double click the file, and we want that
to remain for opening the file (although we could also override
getPreferredAction() to determine what happens when
the node is doubled clicked).
PovrayDataNode.
Open PovrayDataNode in the code editor.
Implement it as follows. The only twist is that if our Node
becomes the main file, it needs to tell the former main file that it is
not the main file anymore: more specifically, it needs to force it to
fire a property change in its display name so that it gets redrawn as
non-bold:
private final class SetMainFileAction extends AbstractAction {
public SetMainFileAction() {
//Set a display name
putValue(Action.NAME, NbBundle.getMessage(PovrayDataNode.class,
"LBL_MainFileAction"));
}
public void actionPerformed(ActionEvent ae) {
MainFileProvider provider = (MainFileProvider) getFromProject(MainFileProvider.class);
FileObject oldMain = provider.getMainFile();
provider.setMainFile(getFile());
fireDisplayNameChange(getDisplayName(), getHtmlDisplayName());
if (oldMain != null) {
try {
Node oldMainFilesNode = DataObject.find(oldMain).getNodeDelegate();
if (oldMainFilesNode instanceof PovrayDataNode) {
((PovrayDataNode) oldMainFilesNode).fireDisplayNameChange(null, oldMainFilesNode.getDisplayName());
}
} catch (DataObjectNotFoundException donfe) { //Should never happen
Exceptions.printStackTrace(donfe);
}
}
}
@Override
public boolean isEnabled() {
return !isMainFile() && getFromProject(MainFileProvider.class) != null;
}
}
|
Bundle.properties file in the same package, i.e., in org.netbeans.examples.modules.povfile: add this text to that file:
LBL_MainFileAction=Set Main File |
When you do so, the name of the file is displayed in bold:
When you set a different file as the main file, the original main file no longer has its name displayed in bold:
@Override
public Action[] getActions (boolean popup) {
Action[] actions = super.getActions(popup);
RendererService renderer = (RendererService)getFromProject (RendererService.class);
Action[] result;
if (renderer != null && actions.length > 0) { //should always be > 0
Action rendererAction = new RendererAction (renderer, this);
result = new Action[ actions.length + 2 ];
result[0] = actions[0];
result[1] = new SetMainFileAction();
result[2] = rendererAction;
System.arraycopy(actions, 1, result, 3, actions.length-1);
} else {
//Isolated file in the favorites window or something:
result = actions;
}
return result;
}
|
This method will add another (yet to be implemented) action into the array of actions, if a renderer service for this file can be found.
org.netbeans.examples.modules.povfile package, and create
a new Java class called RendererAction. Define its signature
as follows:
public class RendererAction extends AbstractAction implements Presenter.Popup { |
Implementing Presenter.Popup is an important step: this
is a way in which an action can actually provide whatever component it
wants to insert into the popup menu. It is a one-method interface, with
the method getPopupPresenter which returns a JMenuItem
(remember that in Swing, JMenu is a subclass of JMenuItem, so it's
legal to return whole submenus here).
In our case, we want a submenu:
|
RendererAction:
private final RendererService renderer;
private final PovrayDataNode node;
public RendererAction(RendererService renderer, PovrayDataNode node) {
this.renderer = renderer;
this.node = node;
}
public void actionPerformed(ActionEvent e) {
assert false;
}
public JMenuItem getPopupPresenter() {
JMenu result = new JMenu ();
//Set the menu's label
result.setText (NbBundle.getMessage (RendererAction.class, "LBL_Render"));
//Get the names of all available settings sets:
String[] availableSettings = renderer.getAvailableRendererSettingsNames();
//Get the name of the most recently used setting set:
String preferred = renderer.getPreferredRendererSettingsNames();
for (int i = 0; i < availableSettings.length; i++) {
String currName = availableSettings[i];
RenderWithSettingsAction action = new RenderWithSettingsAction(currName);
JCheckBoxMenuItem itemForSettings = new JCheckBoxMenuItem (action);
//Show our menu item checked if it is the most recently used set of settings:
itemForSettings.setSelected (preferred != null && preferred.equals(currName));
result.add (itemForSettings);
}
return result;
}
}
|
RendererAction called
RenderWithSettingsAction, and implement it as follows:
private class RenderWithSettingsAction extends AbstractAction implements Runnable {
private final String name;
public RenderWithSettingsAction (String name) {
this.name = name;
putValue (NAME, renderer.getDisplayName(name));
}
public void actionPerformed(ActionEvent e) {
RequestProcessor.getDefault().post(this);
}
public void run() {
DataObject ob = node.getDataObject();
FileObject toRender = ob.getPrimaryFile();
FileObject image = renderer.render(toRender, name);
if (image != null) {
try {
//Try to open the file:
DataObject dob = DataObject.find (image);
Node n = dob.getNodeDelegate();
OpenCookie ck = (OpenCookie) n.getLookup().lookup(
OpenCookie.class);
if (ck != null) {
ck.open();
}
} catch (DataObjectNotFoundException e) {
//Should never happen
Exceptions.printStackTrace(e);
}
}
}
}
|
This is relatively straightforward as well. We are using the
renderer field of the outer class, and only storing the
name of which specific Properties file should be used to provide
settings for this class, which we will pass to renderer.render().
The two interesting areas are how we find the file, and how we actually
perform the rendering. We have the instance of PovRayDataNode
that we are operating against. It is a subclass of DataNode,
so we can call its getDataObject() method (another way
would be to call node.getLookup().lookup(DataObject.class),
but since we know its type, calling getDataObject() is
more efficient). From that we may call getPrimaryFile() to
actually get the FileObject that should be rendered into
an image by POV-Ray.
The other item of interest is how we do our rendering. Notice that we implement Runnable. Our action will be, by default, called from the event dispatch thread when the user clicks it in a menu. It would not be good at all if running the action blocked the UI from repainting or anything else until the external POV-Ray process was completed. So instead, we use a handy thread pool NetBeans provides for us, and simply post the work to be done on another thread off of the event queue.
Bundle.properties file in the same package: add this text to that file:
LBL_Render=Render |
The next class to implement is RendererService: this is the
service, belonging to the project, by which a project will be "compiled"
into an image (by executing the POV-Ray program and passing it arguments).
As we discussed earlier, we are not going to
try to implement a complicated dialog that makes every possible POV-Ray
setting adjustable via a GUI widget: this would add a lot of complexity
when many users would be satisfied with a reasonable set of defaults. So
we will have a set of different default combinations of settings that should
satisfy most users. Later we will add the ability to create completely
customized settings by editing the project.properties of a
POV-Ray project, to satisfy the needs of power users.
Right now, we will not worry about the execution part: RendererService
also provides for named sets of settings: combinations of line switches
which should be passed to POV-Ray to determine rendering quality, image size
and speed. Right now we will only implement that part of RendererService.
This is where we will begin dealing directly with the
System
Filesystem: the registry of runtime data supplied by modules. What we
will do is create .properties files for each set of standard
settings we will supply. We will start by creating .properties
files for each set of settings in our module.
org.netbeans.examples.modules.povproject.defaults. This
is where we will put our Properties files. Within that package,
create 6 Properties files with the following name & contents:
|
160x100.properties: W=160
H=100
Q=4
FN=8
A=0.0
|
|
320x200.properties: W=320
H=200
Q=4
FN=8
A=0.0
|
|
640x480.properties: W=640
H=480
Q=4
FN=8
A=0.0
|
|
640x480hq.properties: W=640
H=480
Q=R
FN=8
A=0.9
|
|
1024x768.properties: W=1024
H=768
Q=4
FN=9
A=0.0
|
|
1024x768hq.properties: W=1024
H=768
Q=R
FN=8
A=0.9
|
layer.xml file for the Povray Projects project
in the code editor.
Add the following content anywhere within the <filesystem>
tags in the Povray Projects project's layer file:
<folder name="Povray">
<folder name="RendererSettings">
<file name="1024x768hq.properties" url="defaults/1024x768hq.properties">
<attr name="SystemFileSystem.localizingBundle"
stringvalue="org.netbeans.examples.modules.povproject.defaults.Bundle"/>
<attr name="position" intvalue="100"/>
</file>
<file name="1024x768.properties" url="defaults/1024x768.properties">
<attr name="SystemFileSystem.localizingBundle"
stringvalue="org.netbeans.examples.modules.povproject.defaults.Bundle"/>
<attr name="position" intvalue="200"/>
</file>
<file name="640x480hq.properties" url="defaults/640x480hq.properties">
<attr name="SystemFileSystem.localizingBundle"
stringvalue="org.netbeans.examples.modules.povproject.defaults.Bundle"/>
<attr name="position" intvalue="300"/>
</file>
<file name="640x480.properties" url="defaults/640x480.properties">
<attr name="SystemFileSystem.localizingBundle"
stringvalue="org.netbeans.examples.modules.povproject.defaults.Bundle"/>
<attr name="position" intvalue="400"/>
</file>
<file name="320x200.properties" url="defaults/320x200.properties">
<attr name="SystemFileSystem.localizingBundle"
stringvalue="org.netbeans.examples.modules.povproject.defaults.Bundle"/>
<attr name="position" intvalue="500"/>
</file>
<file name="160x100.properties" url="defaults/160x100.properties">
<attr name="SystemFileSystem.localizingBundle"
stringvalue="org.netbeans.examples.modules.povproject.defaults.Bundle"/>
<attr name="position" intvalue="600"/>
</file>
</folder>
</folder> |
What this XML does is map the Properties files we just created into
the System Filesystem in the folder Povray/RendererSettings,
which is where our code will look for them. Additionally, it specifies
ordering attributes, which are attributes we are adding to the
folder RendererSettings/, which will determine what order
the files will appear in when code asks for the array of children of
the DataFolder (DataObject subclass for
folders) or its Node for this folder.
You may have noticed the attribute SystemFilesystem.localizingBundle
which we added to the RendererSettings folder. NetBeans
FileObjects (which is what the "files" in the
System Filesystem are) can have ad-hoc key-value pairs associated
with them. SystemFilesystem.localizingBundle is a magic
attribute which the system will use to localize the names of
files: all you have to do is get the DataObject for a file in
the System Filesystem, get the Node for that DataObject,
and the return value of Node.getDisplayName() for that
Node to look up its localized display name in the
requested resource bundle: this is how file names for things declared
in the System Filesystem are localized.
So we need one more Properties file in
org.netbeans.examples.modules.povproject.defaults: create
one called "Bundle". This one won't contain renderer
defaults, it will contain mappings from the file names of the files
we declared above, to their localized, human friendly names.
Bundle.properties file
in org.netbeans.examples.modules.povproject.defaults (as specified by the SystemFileSystem.localizingBundle
in the layer file) and add the following contents to it:
Povray/RendererSettings/1024x768.properties=1024 x 768 Povray/RendererSettings/1024x768hq.properties=1024 x 768 High Quality Povray/RendererSettings/640x480hq.properties=640 x 480 High Quality Povray/RendererSettings/640x480.properties=640 x 480 Povray/RendererSettings/320x200.properties=320 x 200 Povray/RendererSettings/160x100.properties=160 x 100 |
Now we have a set of default settings to show, so we can implement the methods of
RendererService that will expose them.
RendererServiceImpl, in
org.netbeans.examples.modules.povproject:
RendererService
and press Ctrl-Shift-I to fix imports.
The first thing we will do is implement the constructor:
final class RendererServiceImpl extends RendererService {
private PovrayProject proj;
public RendererServiceImpl(PovrayProject proj) {
this.proj = proj;
}
PovrayProject getProject() {
return proj;
} |
We will leave the render methods stubbed out: we will implement these later.
private static boolean logged = false;
private FileObject getRendererSettingsFolder() {
String folderName = "Povray/RendererSettings/";
FileSystem systemFileSystem =
Repository.getDefault().getDefaultFileSystem();
FileObject result =
systemFileSystem.getRoot().getFileObject(folderName);
if (result == null && !logged) {
//Corrupted userdir or something is very very wrong.
//Log it and move on.
Exceptions.printStackTrace(new IllegalStateException("Renderer settings dir missing!"));
logged = true;
}
return result;
}
private FileObject fileFor (String settingsName) {
FileObject settingsFolder = getRendererSettingsFolder();
FileObject result;
if (settingsFolder != null) { //should never be null
result = settingsFolder.getFileObject(settingsName);
} else {
result = null;
}
return result;
}
static Preferences getPreferences() {
return NbPreferences.forModule(RendererServiceImpl.class);
} |
getRendererSettingsFolder() does. You'll note that there
is a null check. This folder should not be null, since we are declaring
it in our layer. But it conceivably could be (a corrupted settings
directory or a module that for some reason hides the settings directory: it
should not happen, but it is theoretically possible), so we log an
exception if so, rather than throwing exceptions every time something goes
and looks for a display name for a menu item or similar.
In NetBeans
Platform 7.0, you will be able to use FileUtil.getConfigFile(folderName)
instead of Repository.getDefault().getDefaultFileSystem().getRoot().getFileObject(folderName).
Preferences.userNodeForPackage(RendererServiceImpl.class) here,
but instead we use the NetBeans NbPreferences class, a NetBeans-specific API
that will store and load the setting in the suite's NetBeans user directory,
instead of the JDK's general location for settings.getAvailableRendererSettingsNames(). This method will
return an array of Strings: the localized, human-friendly
names of all of the files which we declared above:
@Override
public String[] getAvailableRendererSettingsNames() {
FileObject settingsFolder = getRendererSettingsFolder();
String[] result;
if (settingsFolder != null) {
//Use a DataFolder here, so our ordering attributes in the layer
//file are applied, and our returned String array will be in the
//order we want
DataFolder fld = DataFolder.findFolder(settingsFolder);
DataObject[] kids = fld.getChildren();
result = new String[ kids.length ];
for (int i = 0; i < kids.length; i++) {
result[i] = kids[i].getPrimaryFile().getNameExt();
}
} else {
result = new String[0];
}
return result;
} |
This is quite straightforward: we just iterate all of the files in the
folder, and return an array of Strings with their names. The
one twist to it is that we don't iterate the FileObject's
children, but rather we get a DataFolder (the DataObject
type for filesystem folders), and iterate its children. The reason we do
it this way is that the order of children of FileObjects is
undefined: we might get the files we declared in any order. The
DataFolder, however, understands ordering attributes: attributes
we can declare in the XML of our layer file, which will determine
what order a folder's children are returned in. So this enables us to sort
our settings files in an intuitive way: yet other modules could still insert
additional settings, with their own ordering attributes, and they would be
included in the sort (for more info on how and why this works, see the
Javadoc for
Utilities.topologicalSort()). Also note that you can localize the name of a DataObject, but not a FileObject.
getRendererSettings(name): this method
will actually get a Properties object with the contents of
whichever file name was passed to it:
@Override
public Properties getRendererSettings(String name) {
Properties result = new Properties();
FileObject settingsFile = fileFor(name);
if (settingsFile != null) {
try {
result.load(new BufferedInputStream(settingsFile.getInputStream()));
} catch (FileNotFoundException ex) {
Exceptions.printStackTrace(ex);
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
} else {
Exceptions.printStackTrace(new IllegalStateException
("Requested non-existent settings " +
"file " + name));
}
return result;
}
|
The code here is also quite straightforward: it simply tries to load a
Properties object from the input stream of the file in question.
private String KEY_PREFERRED_SETTINGS = "";
@Override
public String getPreferredRendererSettingsNames() {
String result = getPreferences().get(KEY_PREFERRED_SETTINGS, null);
if (result == null) {
result = "640x480.properties";
}
return result;
}
|
@Override
public String getDisplayName(String settingsName) {
FileObject file = fileFor (settingsName);
String result;
if (file != null) {
DataObject dob;
try {
dob = DataObject.find(file);
result = dob.getNodeDelegate().getDisplayName();
} catch (DataObjectNotFoundException ex) {
Exceptions.printStackTrace(ex);
result = "[error]";
}
} else {
result = "";
}
return result;
}
|
Human-readable display names are provided by Nodes: a
FileObject is simply a file on disk (or similar storage such
as the System Filesystem via our layer.xml file): it has
no notion of human readability. So if we want the localized name
for a FileObject, we need to get the Node for it.
In this case, the Node will use the hint we provided
in the layer.xml file:
<attr name="SystemFileSystem.localizingBundle" stringvalue="org.netbeans.modules.povproject.defaults.Bundle"/> |
and look up its localized name in
org.netbeans.examples.modules.povproject.defaults.Bundle.properties.
RendererService
via the project's lookup. Modify PovrayProject.getLookup()
as follows:
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 new RendererServiceImpl(this), //Renderer Service Implementation new MainFileProviderImpl(this), //So things can set the main file }); } return lkp; } |
The next step is to write the code that will actually run POV-Ray and send its text output to the Output window, and eventually open a rendered image. Since this involves some complicated code, we will create a separate utility class that will do the actual rendering.
org.netbeans.examples.modules.povproject, called
"Povray":
First we need to implement support for finding the POV-Ray
executable, so that we have something to run. This will simply
be a matter of popping up a JFileChooser to let the
user locate the POV-Ray executable and the directory with the standard
POV-Ray include files: once this has been done once, we will store
the result so we do not have to ask again unless it is deleted.
Since we may need a file chooser twice, once to locate the
executable, and once to locate the standard include file directory
(which contains files that define standard colors, shapes, etc. that
can be used in POV-Ray files), we should provide one method that shows
a file chooser for both cases. Add the following method to Povray:
private static File locate(String key) {
try {
final JFileChooser jfc = new JFileChooser();
jfc.setDialogTitle(NbBundle.getMessage(Povray.class, key));
jfc.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
SwingUtilities.invokeAndWait(new Runnable() {
public void run() {
jfc.showOpenDialog(WindowManager.getDefault().getMainWindow());
}
});
File result = jfc.getSelectedFile();
return result;
} catch (InterruptedException ex) {
Exceptions.printStackTrace(ex);
} catch (InvocationTargetException ex) {
Exceptions.printStackTrace(ex);
}
return null;
}
|
At this point we need to add another dependency, because we are calling
WindowManager. That is part of the Window System API.
We could pass null here, but then there is the risk that on some window
managers, our file chooser would pop up behind the main window.
This makes sure it stays on top.
Add a dependency on the Window System API to Povray Projects using the Project Properties dialog:
Open org.netbeans.examples.modules.povproject.Bundle.properties
in the code editor, and add the following key/value pairs:
TTL_FindPovray=Locate POV-Ray Executable
TTL_FindIncludeDir=Find POV-Ray Standard Include File Dir
MSG_WindowsWarning=<html>POV-Ray for Windows always displays its graphical \
user interface when it runs. You can get a command-line version of \
POV-Ray at <a href="http://www.povray.org/beta/</a></html>
|
Povray:
private static File povray = null;
private static File include = null;
/** Preferences key for the povray executable */
private static final String KEY_POVRAY_EXEC = "povray";
/** Preferences key for the povray standard includes dir */
private static final String KEY_POVRAY_INCLUDES = "include";
private static File getPovray() {
if (povray == null || !povray.exists()) {
Preferences prefs = RendererServiceImpl.getPreferences();
String loc = prefs.get(KEY_POVRAY_EXEC, null);
if (loc != null) {
povray = new File (loc);
}
if (povray == null || !povray.exists()) {
File maybePov = locate("TTL_FindPovray");
if (maybePov.getPath().endsWith("pvengine.exe")) {
//Warn the user to get a command line build
NotifyDescriptor msg = new NotifyDescriptor.Confirmation (
NbBundle.getMessage(RendererServiceImpl.class,
"MSG_WindowsWarning"),
NotifyDescriptor.WARNING_MESSAGE);
Object result = DialogDisplayer.getDefault().notify(msg);
if (result == NotifyDescriptor.CANCEL_OPTION) {
return null;
}
}
povray = maybePov;
if (povray != null) {
//Here we set the povray executable path in a Properties file,
//in the NetBeans user directory, because we are
//using the NbPreferences API class:
prefs.put(KEY_POVRAY_INCLUDES, povray.getPath());
}
}
}
return povray;
}
private static File getStandardIncludeDir (File povray) {
if (include != null) {
return include;
}
Preferences prefs = RendererServiceImpl.getPreferences();
String loc = prefs.get(KEY_POVRAY_INCLUDES, null);
if (loc != null) {
include = new File (loc);
if (!include.exists()) {
include = null;
}
}
if (include == null) {
include = new File (povray.getParent() +
File.separator + "include");
if (!include.exists()) {
include = locate ("TTL_FindIncludeDir");
if (include != null) {
//Here we set the include path in a Properties file,
//in the NetBeans user directory, because we are
//using the NbPreferences API class:
prefs.put(KEY_POVRAY_INCLUDES, include.getPath());
} else {
include = null;
}
}
}
return include;
}
|
Add a dependency on the Dialogs API in the Povray Projects project, using the Project Properties dialog:
Do all of the below:
NotifyDescriptor class for the first time.
Why would you use this class? Why/how is it preferable to using the JDK's JOptionPane
class? Are there disadvantages to using this class?
At this point, we're almost ready to run code, so it would be a good idea to download a copy of POV-Ray. Official builds can be obtained from povray.org.
sudo port install povray.
Resources
Before continuing with the lesson, read through and understand (and, where applicable, take the steps described) in the following documents:
Don't continue until you have a global understanding of POV-Ray, its purpose, and how to work with it.
Lab
Obtaining POV-Ray is described in the Preparation document, as well as in the introduction to this lesson. Before continuining, make sure POV-Ray is set up correctly to avoid grief and frustration later this week.
At this point, we are ready to write the code that will actually invoke the external POV-Ray executable, pass it the correct arguments and display its output. POV-Ray has two kinds of output: it will write out status and success/failure on the command line, in a similar way to what a compiler does, and it will write out an image file on disk, which we will want to display.
The first part is just being able to invoke the external executable. The NetBeans Platform has some API classes that can help with that.
Povray
class:
private final RendererServiceImpl renderService;
private final FileObject toRender;
private final Properties settings;
Povray (RendererServiceImpl renderService, FileObject toRender, Properties settings) {
this.renderService = renderService;
this.toRender = toRender;
this.settings = settings == null ? renderService.getRendererSettings(renderService.getPreferredRendererSettingsNames()) : settings;
} |
FileObject, but now we need an actual
java.io.File to get the path from, to pass on the command
line to POV-Ray. There are two caveats:
FileUtil.toFile() returns null).
So, we will add a method
to Povray as follows:
private File getFileToRender() throws IOException {
FileObject render = toRender;
if (render == null) {
PovrayProject proj = renderService.getProject();
MainFileProvider provider = (MainFileProvider)
proj.getLookup().lookup (MainFileProvider.class);
if (provider == null) {
throw new IllegalStateException ("Main file provider missing");
}
render = provider.getMainFile();
if (render == null) {
ProjectInformation info = (ProjectInformation)proj.getLookup().lookup(ProjectInformation.class);
//XXX let the user choose
throw new IOException (NbBundle.getMessage(Povray.class, "MSG_NoMainFile", info.getDisplayName()));
}
}
assert render != null;
File result = FileUtil.toFile (render);
if (result == null) {
throw new IOException (NbBundle.getMessage(Povray.class, "MSG_VirtualFile", render.getName()));
}
assert result.exists();
assert result.isFile();
return result;
} |
+[some character][somevalue], for example,
+A0.9 sets the anti-aliasing parameter to 0.9 pixels.
So we need to iterate the Properties object passed to the
constructor and assemble from it a set of command line arguments:
private String getCmdLineArgs(File includesDir) {
StringBuffer cmdline = new StringBuffer();
for (Iterator i=settings.keySet().iterator(); i.hasNext();) {
String key = (String) i.next();
String val = settings.getProperty(key);
cmdline.append ('+');
cmdline.append (key);
cmdline.append (val);
cmdline.append (' ');
}
cmdline.append ("+L");
cmdline.append (includesDir.getPath());
return cmdline.toString();
} |
private File getImagesDir() {
PovrayProject proj = renderService.getProject();
FileObject fob = proj.getImagesFolder(true);
File result = FileUtil.toFile(fob);
assert result != null && result.exists();
return result;
}
private String stripExtension(File f) {
String sceneName = f.getName();
int endIndex;
if ((endIndex = sceneName.lastIndexOf('.')) != -1) {
sceneName = sceneName.substring(0, endIndex);
}
return sceneName;
} |
Neither is terribly exciting: one gets the images directory from the
project as a java.io.File, and the other trims the file
extension off a file name (so we can create an image file with the same
name as the scene file).
StatusDisplayer
that lets any code in NetBeans that wants to write to the status bar
(the actual implementation of StatusDisplayer is in the
windowing system implementations, core/windows in NetBeans
CVS).
Implement the following method, and then add a dependency on the UI Utilities API module from the Povray Projects module:
private void showMsg (String msg) {
StatusDisplayer.getDefault().setStatusText(msg);
} |
NbBundle.getMessage(SomeClass.class, "MSG_Something", someStringArgument); |
...to fetch a localized string. NbBundle supports embedding
arguments inside of a localized string: you can either use the above
method, or a variant that takes an array of arguments to embed. So you
can define strings in a resource bundle using the syntax
Could not delete {0} because {1} |
...and {0} and {1} will be replaced by arguments
passed to getMessage().
This is extremely useful, as often
the order in which such strings occur in the result text will be different
in different human languages.
So let's go ahead and add the warning messages we need to
Bundle.properties in the same package as PovrayProject:
MSG_NoMainFile=Main scene file not set for {0} MSG_VirtualFile=Not a file on disk: {0} MSG_Rendering=Rendering {0} MSG_NoPovrayExe=No POV-Ray executable, cannot render MSG_NoPovrayInc=No POV-Ray includes dir, cannot render MSG_Success=Rendered {0} successfully MSG_Failure=Failed to render {0} MSG_CantDelete=Could not delete {0}, it is locked or in use |
Now we are almost ready to get down to the nitty-gritty of actually
invoking POV-Ray from NetBeans. We will do this in the standard Java
way, using Runtime.exec() to start an external process.
We also will want to display the text output from the process as it
reports its progress, in the Output window. This means we will need a
way to write to the Output window. So we will add one more dependency
to Povray Projects: add a dependency on the IO API module. To find it, use the class
name InputOutput in the Add Dependency dialog:
So we will need some kind of Runnable which will wait for
data from each output stream and route it to the Output window in
NetBeans as it becomes available. Writing to the Output window is quite
easy: you get an InputOutput object from
IOProvider.getDefault() and then write to one of its
streams: for example:
InputOutput io = IOProvider.getDefault().getIO ("Hello", true); io.select(); io.getOut().println ("Hello world"); io.getErr().println ("This is the standard error output: it should be red"); |
... that's all it takes to make the Output window pop up and display some output!
Povray class:
static class OutHandler implements Runnable {
private Reader out;
private OutputWriter writer;
public OutHandler (Reader out, OutputWriter writer) {
this.out = out;
this.writer = writer;
}
public void run() {
while (true) {
try {
while (!out.ready()) {
try {
Thread.currentThread().sleep(200);
} catch (InterruptedException e) {
close();
return;
}
}
if (!readOneBuffer() || Thread.currentThread().isInterrupted()) {
close();
return;
}
} catch (IOException ioe) {
//Stream already closed, this is fine
return;
}
}
}
private boolean readOneBuffer() throws IOException {
char[] cbuf = new char[255];
int read;
while ((read = out.read(cbuf)) != -1) {
writer.write(cbuf, 0, read);
}
return read != -1;
}
private void close() {
try {
out.close();
} catch (IOException ioe) {
Exceptions.printStackTrace(ioe);
} finally {
writer.close();
}
}
} |
render() method in Povray that
will invoke POV-Ray. This method should never be invoked from
the event thread, because it would block the UI until POV-Ray is finished.
So the first thing we do is sanity check what thread we're running on.
Then we get the file we need to render, sanity checking that. Then we
call getPovray() which may open a file chooser to let the
user pick it, and similarly get the default include directory which we
will need to pass on the command line. Next, we get the directory where
we will put the output, assemble our output file name (we use PNG format
since NetBeans' Image module supports that). Then we compute the command
line that should be passed to POV-Ray. Then we call
Runtime.exec() with that argument, wire up the Output window
to the output streams from the resulting process, and wait for the
process to exit.
Once it exits, we determine if it succeeded or failed,
show an appropriate message, and if it succeeded, return a
org.openide.filesystems.FileObject representing the file that was created.
public FileObject render() throws IOException { if (EventQueue.isDispatchThread()) { throw new IllegalStateException ("Tried to run povray from the event thread"); } //Find the scene file pass to POV-Ray as a java.io.File File scene; try { scene = getFileToRender(); } catch (IOException ioe) { showMsg (ioe.getMessage()); return null; } //Get the POV-Ray executable File povray = getPovray(); if (povray == null) { //The user cancelled the file chooser w/o selecting showMsg(NbBundle.getMessage(Povray.class, "MSG_NoPovrayExe")); return null; } //Get the include dir, if it isn't under povray's home dir File includesDir = getStandardIncludeDir(povray); if (includesDir == null) { //The user cancelled the file chooser w/o selecting showMsg (NbBundle.getMessage(Povray.class, "MSG_NoPovrayInc")); return null; } //Find the image output directory for the project File imagesDir = getImagesDir(); //Assemble and format the line switches for the POV-Ray process based //on the contents of the Properties object String args = getCmdLineArgs(includesDir); String outFileName = stripExtension (scene) + ".png"; //Compute the name of the output image file File outFile = new File(imagesDir.getPath() + File.separator + outFileName); //Delete the image if it exists, so that any current window viewing the file is //closed and the file will definitely be re-read when it is re-opened if (outFile.exists() && !outFile.delete()) { showMsg (NbBundle.getMessage(Povray.class, "LBL_CantDelete", outFile.getName())); return null; } //Append the input file and output file arguments to the command line String cmdline = povray.getPath() + ' ' + args + " +I" + scene.getPath() + " +O" + outFile.getPath(); System.err.println(cmdline); showMsg (NbBundle.getMessage(Povray.class, "MSG_Rendering", scene.getName())); final Process process = Runtime.getRuntime().exec (cmdline); //Get the standard out of the process InputStream out = new BufferedInputStream (process.getInputStream(), 8192); //Get the standard in of the process InputStream err = new BufferedInputStream (process.getErrorStream(), 8192); //Create readers for each final Reader outReader = new BufferedReader (new InputStreamReader (out)); final Reader errReader = new BufferedReader (new InputStreamReader (err)); //Get an InputOutput to write to the Output window InputOutput io = IOProvider.getDefault().getIO(scene.getName(), false); //Force it to open the Output window/activate our window io.select(); //Print the command line we're calling for debug purposes io.getOut().println(cmdline); //Create runnables to poll each output stream OutHandler processSystemOut = new OutHandler (outReader, io.getOut()); OutHandler processSystemErr = new OutHandler (errReader, io.getErr()); //Get two different threads listening on the output & err //using the system-wide thread pool RequestProcessor.getDefault().post(processSystemOut); RequestProcessor.getDefault().post(processSystemErr); try { //Hang this thread until the process exits process.waitFor(); } catch (InterruptedException ex) { Exceptions.printStackTrace(ex); } //Close the Output window's streams (title will become non-bold) processSystemOut.close(); processSystemErr.close(); if (outFile.exists() && process.exitValue() == 0) { //Try to find the new image file FileObject outFileObject = FileUtil.toFileObject(outFile); showMsg (NbBundle.getMessage(Povray.class, "MSG_Success", outFile.getPath())); return outFileObject; } else { showMsg (NbBundle.getMessage(Povray.class, "MSG_Failure", scene.getPath())); return null; } } |
RendererService to call Povray.render().
Open RendererServiceImpl in the code editor, and modify
the render method:
@Override public FileObject render(FileObject scene, Properties renderSettings) { Povray pov = new Povray (this, scene, renderSettings); try { return pov.render(); } catch (IOException ioe) { Exceptions.printStackTrace(ioe); return null; } } |
OpenCookie on the Node for the image
that was rendered. You might be wondering what a Cookie is. A Cookie is the capability that something has to do something. (See http://wiki.netbeans.org/DevFaqLookupCookie and here for details!)
If you are running a standard configuration of the
NetBeans IDE, you already have the Image module installed: it will
provide support for opening an image, displaying it in the editor area.
So, in the Povray File Support module,
reimplement RendererAction.RenderWithSettingsAction.run() like
this:
public void run() {
DataObject ob = node.getDataObject();
FileObject toRender = ob.getPrimaryFile();
Properties mySettings = renderer.getRendererSettings(name);
FileObject image = renderer.render(toRender, mySettings);
if (image != null) {
try {
DataObject dob = DataObject.find (image);
Node n = dob.getNodeDelegate();
OpenCookie ck = (OpenCookie) n.getLookup().lookup(OpenCookie.class);
if (ck != null) {
ck.open();
}
} catch (DataObjectNotFoundException e) {
//Should never happen
Exceptions.printStackTrace(e);
}
}
} |
images/
subdirectory of your project:
Under Windows, POV-Ray MUST be installed in a directory without spaces AND
the project that you create for POV-Ray using the plugin MUST be
created in a directory without spaces.
Next week we will cover implementing
ViewService and adding actions for that.
The last piece of our API to implement is ViewService. After we implement it, we begin using our API. To do that, we add a "View" action to our POV-Ray node files. Finally, we add code for determining at runtime whether a file has an image or not, and then add appropriate badges.
Resources
Before continuing with the lesson, watch these two videos. Both cover "porting a Java application to the NetBeans Platform". That is not something that will be directly addressed within this week's lesson, but it is important to be aware of the basic principles relating to porting. Also, these two videos tie together several topics that have been touched on in previous weeks.
Now read through these two documents, which both discuss in some detail
the question of porting:
Lab
The last piece of our API to implement is ViewService, which will
allow us to show the most recently rendered image file associated with a
POV-Ray file.
org.netbeans.examples.modules.povproject, called
"ViewServiceImpl":
Povray class in the editor, and modify
the signature of stripExtension() as follows:
static String stripExtension(File f) { |
ViewServiceImpl, add implements ViewService
to the signature and use the
"Implement All Abstract Methods" hint to provide skeleton
implementations of all of the methods.
Now, add the following method to actually find the image file for a given scene file:
private final PovrayProject proj; private FileObject getImageFor (FileObject scene) { FileObject imagesDir = proj.getImagesFolder(false); FileObject result; if (imagesDir != null) { File sceneFile = FileUtil.toFile (scene); if (sceneFile != null) { String imageName = Povray.stripExtension(sceneFile) + ".png"; //Will be null if it doesn't exist: result = imagesDir.getFileObject (imageName); } else { result = null; } } else { //No images dir, there can't be an image result = null; } return result; } |
...and run Fix Imports again to import org.openide.filesystems.FileObject.
public ViewServiceImpl(PovrayProject proj) {
this.proj = proj;
}
public boolean isRendered(FileObject file) {
return getImageFor (file) != null;
}
public boolean isUpToDate(FileObject scene) {
FileObject image = getImageFor (scene);
boolean result;
if (image != null) {
result = scene.lastModified().before(image.lastModified());
} else {
result = false;
}
return result;
}
public void view(FileObject scene) {
FileObject image = getImageFor(scene);
if (image != null) {
DataObject dob;
try {
dob = DataObject.find(image);
OpenCookie open = (OpenCookie)
dob.getNodeDelegate().getLookup().lookup (OpenCookie.class);
if (open != null) {
open.open();
return;
}
} catch (DataObjectNotFoundException ex) {
Exceptions.printStackTrace(ex);
}
}
Toolkit.getDefaultToolkit().beep();
}
|
and run Fix Imports to import the necessary classes.
ViewService
via the project's lookup. Modify PovrayProject.getLookup()
as follows:
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 new RendererServiceImpl(this), //Renderer Service Implementation new MainFileProviderImpl(this), //So things can set the main file new ViewServiceImpl (this), //Allow things to find/open the image associated with a scene file }); } return lkp; } |
The trailing comma in the array definition is not strictly necessary,
but it's a useful technique for reducing the CVS diff if you're using
version control, and so not a bad habit to have: if you add to the
array, you only change the lines you added.
Now of course, we have implemented the API, but there is no code that uses it. So what we will do here is to add a "View" action to our POV-Ray file nodes.
PovRayDataNode
in the org.netbeans.examples.modules.povfile package.
First, we will add one more action into the array of popup menu
actions from PovrayDataNode (modified and new lines in
blue below):
public Action[] getActions (boolean popup) {
Action[] actions = super.getActions(popup);
RendererService renderer =
(RendererService)getFromProject (RendererService.class);
Action[] result;
if (renderer != null && actions.length > 0) { //should always be > 0
Action rendererAction = new RendererAction (renderer, this);
result = new Action[ actions.length + 3 ];
result[0] = actions[0];
result[1] = new SetMainFileAction();
result[2] = rendererAction;
result[3] = new ViewAction();
System.arraycopy(actions, 1, result, 4, actions.length-1);
} else {
//Isolated file in the favorites window or something
result = actions;
}
return result;
} |
ViewAction. This can be an inner
class inside PovrayDataNode:
private class ViewAction extends AbstractAction {
ViewAction() {
putValue (NAME, NbBundle.getMessage(PovrayDataNode.class, "LBL_View"));
}
public void actionPerformed(ActionEvent actionEvent) {
ViewService service = (ViewService) getFromProject (ViewService.class);
FileObject fob = getDataObject().getPrimaryFile();
service.view(fob);
}
@Override
public boolean isEnabled() {
return getFromProject (ViewService.class) != null;
}
} |
and run Fix Imports to import ViewService.
Bundle.properties file in the same package, so that
there is some text for the view action's menu item. Add:
LBL_View=View |
You may have noticed that there are a few methods we are not using on
ViewService, which is within the "Povray API" module, particularly isUpToDate(). In the
NetBeans IDE, the icon for Java classes has a "badge" in the lower
right if the compiled version of it is older than the source file and it
probably needs recompilation.
In an ideal world, we would parse POV-Ray source files, find all off their
include files, and be able to tell if a rendered image is out of date based
on all of that information. However, that would be a bit out of scope for
this course, since we have no POV-Ray file parser at the moment. What we
can do easily enough, though, is use the implementation we already have of
isUpToDate() and mark the PovrayDataNode icon
if it is false.
To do this, we will need to add a method to RendererService
that lets an object listen for events, which should be fired when the
rendered state of a file changes. And this is exactly the sort of case where
it is fortunate that RendererService is an abstract class: we
can add the methods into the base class, with little risk of breaking any
existing code that uses it (in practice there is the remote possibility that
some implementation of RendererService already has a final
method with the same name and signature [in fact exactly this happened to
NetBeans when getCause() was added to Throwable
in JDK 1.3], but it is a reasonable change). In this case, of course, we
know we are the only ones implementing RendererService, but if
this feature were something we were adding after a release, there would be
no way to be sure we wouldn't break existing clients by adding abstract
methods.
RendererService, in the Povray API project's
org.netbeans.modules.examples.api.povray package, in the
code editor.
Add the following field and methods. What this will do is let a listener register for change events against a specific scene file, and provide a method that subclasses may call to fire such changes, and two methods that can be overridden to do any additional work needed when a listener is added or disappears.
Since our PovrayDataNodes
are created by the system on demand, they do not have such a well-defined
lifecycle. So rather than try to find a point at which we can unregister
the listener, we will keep weak references to our listeners, so they can
be disposed as need-be:
private Map scenes2listeners = new HashMap();
public final void addChangeListener (FileObject scene, ChangeListener l) {
//Get the string name of the scene file: there is no need to hold
//the FileObject itself in memory forever, we can let it be garbage
//collected, and just hold the string path, which is less expensive
String scenePath = scene.getName();
//Make sure what we're doing is thread safe
synchronized (scenes2listeners) {
//We will use a weak reference to listeners, rather than have a
//remove listener method. This will allow our nodes to be garbage
//collected if they are hidden
Reference listenerRef = new WeakReference (l);
List listeners = (List) scenes2listeners.get (scenePath);
if (listeners == null) {
listeners = new LinkedList();
//Map the listener list for this path to the path
scenes2listeners.put (scenePath, listeners);
}
//Add the weak reference to the list of listeners interested in
//this scene
listeners.add (listenerRef);
}
//Call our callback method: probably the implementation will start
//listening to deletions of the image file, because we will need to
//fire those too. Do this outside of the synchronized block: never
//call foreign code under a lock
listenerAdded (scene, l);
}
protected void listenerAdded(FileObject scene, ChangeListener l) {
//do nothing, should be overridden. Here we should start listening
//for changes in the image file (particularly deletion)
}
protected void noLongerListeningTo (FileObject scene) {
//detach any listeners for image files being created/destroyed here
}
/**
* Fire a change event to any listeners that care about changes for the
* passed scene file. If the scene file is null, fire changes to all
* listeners for all files.
* @param scene a POV-Ray scene or include file
*/
protected final void fireSceneChange (FileObject scene) {
String scenePath = scene == null ? null : scene.getName();
List fireTo = null;
//Use the 3-state (null, false, true) nature of a Boolean to decide if
//we have really stopped listening
Boolean stillListening = null;
synchronized (scenes2listeners) {
//Get the list of paths -> weak references -> listeners for this
//scene
List listeners;
if (scenePath != null) {
listeners = (List) scenes2listeners.get (scenePath);
} else {
listeners = new ArrayList();
for (Iterator i = scenes2listeners.keySet().iterator(); i.hasNext();) {
String path = (String) i.next();
List curr = (List) scenes2listeners.get(path);
if (curr != null) {
listeners.addAll(curr);
}
}
}
if (listeners != null && !listeners.isEmpty()) {
//Create a list to put the listeners we will fire to into
fireTo = new ArrayList(3);
for (Iterator i = listeners.iterator(); i.hasNext();) {
Reference ref = (Reference) i.next();
//Get the next change listener for this path
ChangeListener l = (ChangeListener) ref.get();
if (l != null) {
//Add it to the list if it still exists
fireTo.add (l);
} else {
//If not, remove the dead reference
i.remove();
}
}
//If there is nothing listening, remove the empty listener list
//and stop paying attention to this path
if (listeners.isEmpty()) {
scenes2listeners.remove (scenePath);
stillListening = Boolean.FALSE;
} else {
stillListening = Boolean.TRUE;
}
}
}
//Call the listener removal method outside the synch block.
//StillListening will be null if we were never listening at all
if (stillListening != null && Boolean.FALSE.equals(stillListening)) {
noLongerListeningTo (scene);
}
//Again, fire changes outside the synch block since we
//are calling foreign code
if (fireTo != null) {
for (Iterator i = fireTo.iterator(); i.hasNext();) {
ChangeListener l = (ChangeListener) i.next();
l.stateChanged(new ChangeEvent(this));
}
}
} |
Next we need to implement the two protected methods we defined above,
in our implementation of RendererService. Open
RendererServiceImpl in the code editor.
RendererServiceImpl, in the "Povray Projects" module, so modify its signature as follows:
final class RendererServiceImpl extends RendererService implements FileChangeListener { |
...and use the editor hint to create skeleton implementations of the methods of these interfaces.
The thing to note here is that, unlike java.io.File,
it is possible to listen for changes on org.openide.filesystems.FileObject,
either folders or files.
RendererService, knows nothing about how
image files map to scene files. However, our implementation of it does
know how to find the corresponding image file to a scene file. So we will
override those methods to listen for changes in the presence, absence, or
timestamp of the image file that corresponds to a POV-Ray file. This involves
a bit of boilerplate listener code and bookkeeping to decide when to start
and stop listening:
//Keep a list of the paths we are currently listening to
private Set scenesListenedTo = new HashSet();
private boolean listeningToImagesFolder = false;
@Override
protected void listenerAdded(FileObject scene, ChangeListener l) {
synchronized (this) {
if (scenesListenedTo.add (scene.getPath())) {
if (scenesListenedTo.size() == 1 || !listeningToImagesFolder) {
//This is the first call, so we should start listening
//on the images folder
startListeningToImagesFolder();
}
listenTo (scene);
}
}
}
@Override
protected void noLongerListeningTo(FileObject scene) {
synchronized (this) {
scenesListenedTo.remove (scene.getPath());
}
}
private void startListeningToImagesFolder() {
FileObject imageFolder = proj.getImagesFolder(false);
listeningToImagesFolder = imageFolder != null;
if (listeningToImagesFolder) {
listenTo (imageFolder);
}
}
private void listenTo (FileObject file) {
//Add ourselves as a weak listener to the file. This way we can still
//be garbage collected if the project is closed
FileChangeListener stub = (FileChangeListener) WeakListeners.create(
FileChangeListener.class, this, file);
file.addFileChangeListener(stub);
}
public void fileFolderCreated(FileEvent fileEvent) {
//Do nothing
}
public void fileDataCreated(FileEvent fileEvent) {
FileObject created = fileEvent.getFile();
fireSceneChange(created);
}
public void fileChanged(FileEvent fileEvent) {
FileObject changed = fileEvent.getFile();
fireSceneChange(changed);
}
public void fileDeleted(FileEvent fileEvent) {
FileObject deleted = fileEvent.getFile();
fireSceneChange(deleted);
if (deleted.isFolder() && "images".equals(deleted.getNameExt())) {
//The images folder was deleted, reset our listening flags
fireSceneChange(null);
listeningToImagesFolder = false;
}
}
public void fileRenamed(FileRenameEvent fileRenameEvent) {
//do nothing
}
public void fileAttributeChanged(FileAttributeEvent fileAttributeEvent) {
//do nothing
} |
render()
method in the RendererServiceImpl class: it is possible that the images/ directory of the project was
simply not there: it can legally be deleted. In that case, there will be
nothing to listen to. The first time we render, it will be recreated if
necessary. So we need to check if we were listening on the images/ folder,
and if not, start now that it's created. So, we need to modify the
implementation of render() slightly:
public FileObject render(FileObject scene, Properties renderSettings) { Povray pov = new Povray (this, scene, renderSettings); FileObject result; try { result = pov.render(); if (!listeningToImagesFolder) { startListeningToImagesFolder(); } } catch (IOException ioe) { Exceptions.printStackTrace(ioe); result = null; } return result; } |
One thing worth noting is our use of the WeakListeners utility
class. This can be used to generate a variant of any event listener which
will only reference the actual listener weakly: so you can add a listener
to a long-lived object (such as the Project or something held strongly by
it), but the listener can still be garbage collected. So, the
FileObjects we listen to can outlive the RendererServiceImpl
or the Project and not force them to be retained in memory
simply because something wanted to listen to changes in a file or folder.
Now we need to actually display different icons depending on the rendered
state of the scene file being represented. The NetBeans Utilities API offers
a handy method for merging multiple images together: ImageUtilities.mergeImages().
PovrayDataNode, in the
"Povray File Support" module, so that it
implements ChangeListener and add the appropriate stateChanged()
method.
PovrayDataNode:
PovrayDataNode(PovrayDataObject obj, Lookup lookup) { super(obj, Children.LEAF, lookup); RendererService serv = (RendererService) getFromProject(RendererService.class); if (serv != null) { //Could be an isolated file outside of a project, in which //case there is no renderer service serv.addChangeListener(obj.getPrimaryFile(), this); } } |
stateChanged() method can be implemented very simply:
@Override
public void stateChanged(ChangeEvent changeEvent) {
fireIconChange();
} |
getIcon() to return different
icons depending on the state of the Node:
private static final String NEEDS_RENDER_BADGE_FILE =
"org/netbeans/examples/modules/povfile/needsRenderBadge.png";
private static final String HAS_IMAGE_BADGE_FILE =
"org/netbeans/examples/modules/povfile/hasImageBadge.png";
private static final String NO_IMAGE_BADGE_FILE =
"org/netbeans/examples/modules/povfile/hasNoImageBadge.png";
@Override
public Image getIcon(int type) {
Image result = super.getIcon(type);
ViewService vs = (ViewService) getFromProject (ViewService.class);
if (vs != null) {
FileObject file = getFile();
boolean hasRender = vs.isRendered(file);
if (hasRender) {
Image badge1 = ImageUtilities.loadImage(HAS_IMAGE_BADGE_FILE);
result = ImageUtilities.mergeImages(result, badge1, 8, 8);
boolean upToDate = vs.isUpToDate(file);
if (!upToDate) {
Image badge2 = ImageUtilities.loadImage(NEEDS_RENDER_BADGE_FILE);
result = ImageUtilities.mergeImages(result, badge2, 8, 0);
}
} else {
Image badge3 = ImageUtilities.loadImage(NO_IMAGE_BADGE_FILE);
result = ImageUtilities.mergeImages(result, badge3, 8, 8);
}
}
return result;
} |
Here we have defined a set of constants that are paths to icons, and depending on the state, we will merge various ones with the base. Each of our badge images is 8x8 pixels, so it can neatly be placed in one of the quadrants of our 16x16 icon.
org.netbeans.examples.modules.povfile package: here are
the ones used in this course:



When you run the suite, all POV-Ray files that have been rendered (that is, for which you have created images), will have one icon, files that have changed since being rendered have a different icon, and yet another icon is displayed for files for which no image file has been created:
FileChangeListener class. Go back to the plugin you created in the 1st week, which lets the NetBeans
Platform distinguish one kind of XML file from all other kinds. Change that plugin so that
a message is printed in the Output window whenever the user changes an XML file of the
that kind.
ImageUtilities class some more. Study the Javadoc for
that class and try to use some of its other methods! Do you think you can use this class outside
the NetBeans Platform? (And what does "outside the NetBeans Platform" mean?) What would you need to do to do so? Experiment and find out
for yourself.
You may recall that early on, when we were writing PovrayProject, we stubbed out the implementation of PovrayProject.ActionsProviderImpl: so in fact the build, clean and run actions are all disabled when a POV-Ray project is active. We implement them here, together with a "Main File" action.
Resources
Before continuing with the lesson, spend some time exploring the NetBeans Platform Homepage:
Answer the following 5 questions, based on what you read above:
Don't continue with this course until you've answered the questions above.
Lab
You may recall that early on, when we were writing PovrayProject,
we stubbed out the implementation of PovrayProject.ActionsProviderImpl: so
in fact the build, clean and run actions are all disabled when a POV-Ray
project is active. We should quickly implement them:
PovrayProject in the code editor, and implement
the methods of ActionProviderImpl as follows:
public String[] getSupportedActions() {
return new String[] { ActionProvider.COMMAND_BUILD,
ActionProvider.COMMAND_CLEAN, ActionProvider.COMMAND_COMPILE_SINGLE };
}
public void invokeAction(String action, Lookup lookup) throws IllegalArgumentException {
int idx = Arrays.asList (getSupportedActions()).indexOf (action);
switch (idx) {
case 0 : //build
final RendererService ren = (RendererService) getLookup().lookup(RendererService.class);
RequestProcessor.getDefault().post (new Runnable() {
public void run() {
FileObject image = ren.render();
//If we succeeded, try to open the image
if (image != null) {
DataObject dob;
try {
dob = DataObject.find(image);
OpenCookie open = (OpenCookie)
dob.getNodeDelegate().getLookup().lookup(
OpenCookie.class);
if (open != null) {
open.open();
}
} catch (DataObjectNotFoundException ex) {
Exceptions.printStackTrace(ex);
}
}
}
});
break;
case 1 : //clean
FileObject fob = getImagesFolder(false);
if (fob != null) {
DataFolder fld = DataFolder.findFolder(fob);
for (Enumeration en=fld.children(); en.hasMoreElements();) {
DataObject ob = (DataObject) en.nextElement();
try {
ob.delete();
} catch (IOException ioe) {
Exceptions.printStackTrace(ioe);
}
}
}
break;
case 2 : //compile-single
final DataObject ob = (DataObject) lookup.lookup (DataObject.class);
if (ob != null) {
final RendererService ren1 = (RendererService) getLookup().lookup(RendererService.class);
RequestProcessor.getDefault().post (new Runnable() {
public void run() {
if (ob.isValid()) { //Could theoretically change before we run
ren1.render(ob.getPrimaryFile());
}
}
});
}
break;
default :
throw new IllegalArgumentException (action);
}
}
public boolean isActionEnabled(String action, Lookup lookup) throws IllegalArgumentException {
int idx = Arrays.asList (getSupportedActions()).indexOf (action);
boolean result;
switch (idx) {
case 0 : //build
result = true;
break;
case 1 : //clean
result = getImagesFolder(false) != null &&
getImagesFolder(false).getChildren().length > 0;
break;
case 2 : //compile-single
DataObject ob = (DataObject) lookup.lookup (DataObject.class);
if (ob != null) {
FileObject file = ob.getPrimaryFile();
result = "text/x-povray".equals(file.getMIMEType());
} else {
result = false;
}
break;
default :
result = false;
}
return result;
} |
isActionEnabled() will always return true for build. But we
implemented the following code in Povray.getFileToRender():
render = provider.getMainFile();
if (render == null) {
|
So if there is no main file set for a project, the build action will be enabled, but if it is invoked, it will throw an exception! The simple choice would be to test if there is a main file, and if not, disable the build action: but this would be rather non-intuitive to the user who might not be able to figure out what is wrong with his or her project. And we would lose an opportunity to explore the Explorer & Property Sheet API, as well as the Dialogs API.
So instead, we will post a dialog which will allow the user to choose which file should be the main file, if none is set when build is called:
DialogDisplayer to add
a dependency on the Dialogs API.ExplorerManager to add a dependency on the Explorer and
Property Sheet API.org.netbeans.examples.modules.povproject
package and chose New > Other > Swing GUI Forms > JPanel Form.
Name it "MainFileChooser"
The GUI Designer (Matisse)
will open.JLabel and add
a JLabel to the top of the form.JLabel, choose
Edit Text, and type "Select Main File".JLabel to the right edge of the
panel.JScrollPane and add
a JScrollPane to the form below the JLabel.JScrollPane down
and to the right until the bottom and right edge alignment guidelines
appear. The result should look like this:
Resize the JPanel, removing the scrollbars, and switch the automatic
horizontal and vertical resizing on. The scrollbars can be removed
by selecting the JScrollPane and setting both
the horizontalScrollBarPolicy and verticalScrollBarPolicy properties
to NEVER, in the Properties sheet. Automatic resizing is enabled by clicking
the small arrow buttons in the GUI Builder's toolbar.
Continue tweaking the JPanel to your needs. Once you have deployed the application,
the above JPanel should look something like this:
BeanTreeView and instantiate it:
BeanTreeView btv = new BeanTreeView(); |
The BeanTreeView we are showing is a UI class from the
Explorer & Property Sheet API: in fact, it is the very same component that you see in
the Projects, Files, Services, and Favorites windows in NetBeans IDE.
What it will do is, when it is added to a component, search through
the hierarchy of parent components until it finds one that implements
ExplorerManager.Provider. That component's ExplorerManager
will then become the place where the tree view gets its root node to
display, and will be what it notifies when the selection changes.
JScrollPane. Right-click it, choose Customize Code,
and edit the initial line of code, passing the BeanTreeView
to the JScrollPane, as shown below:
ExplorerManager.Provider:
public class MainFileChooser extends javax.swing.JPanel implements ExplorerManager.Provider { |
ExplorerManager.Provider:
private final ExplorerManager mgr = new ExplorerManager(); public ExplorerManager getExplorerManager() { return mgr; } |
public MainFileChooser(PovrayProject proj) {
initComponents();
LogicalViewProvider logicalView = (LogicalViewProvider)
proj.getLookup().lookup(LogicalViewProvider.class);
Node projectNode = logicalView.createLogicalView();
mgr.setRootContext (new FilterNode (projectNode,
new ProjectFilterChildren (projectNode)));
btv.setPopupAllowed(false);
btv.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
//BeanTreeView shows no border by default:
btv.setBorder (new LineBorder(UIManager.getColor("controlShadow")));
} |
LogicalViewProvider. It
will not find ProjectFilterNode because we have not yet
written it.
Now we need to implement ProjectFilterNode. The
Nodes API contains a class, FilterNode, which makes it
possible to take one Node, and create another Node
which "filters" the original Node: providing
different children, actions, properties or whatever it chooses to.
In our case, we want a FilterNode that will filter out
any files that do not have the MIME type text/x-povray -
so that, if the user has a text file or an image file or such in their
project, they cannot set that to be the main file and try to pass it
to POV-Ray.
We don't actually need to implement FilterNode, we
simply need to provide an alternate Children object
which filters out files we don't want. Implement this as a nested
class inside MainFileChooser:
private static final class ProjectFilterChildren extends FilterNode.Children { ProjectFilterChildren (Node projectNode) { super (projectNode); } protected Node[] createNodes(Node object) { Node origChild = (Node) object; DataObject dob = (DataObject) origChild.getLookup().lookup (DataObject.class); if (dob != null) { FileObject fob = dob.getPrimaryFile(); if ("text/x-povray".equals(fob.getMIMEType())) { return super.createNodes (object); } else if (dob instanceof DataFolder) { //Allow child folders of the scenes/ dir return new Node[] { new FilterNode (origChild, new ProjectFilterChildren(origChild)) }; } } //Don't create any nodes for non-povray files return new Node[0]; } } |
RendererServiceImpl, before we call
Povray.render(). Reimplement the no-argument version
of the render() method as follows:
public FileObject render() { MainFileProvider mfp = (MainFileProvider) proj.getLookup().lookup( MainFileProvider.class); assert mfp != null; if (mfp.getMainFile() == null) { showChooseMainFileDlg (mfp); } if (mfp.getMainFile() != null) { return render (null); } else { return null; } } |
showChooseMainFileDlg(). This is the method which
will ask the user to pick a main file. It will use the Dialogs API
to show a dialog containing an instance of MainFileChooser,
and enable the OK button once a file is selected. If the user
selects a POV-Ray file, it will be stored in MainFileProvider,
and so it will be non-null when we return to the render()
method, and so render() will proceed:
private void showChooseMainFileDlg (final MainFileProvider mfp) {
final MainFileChooser chooser = new MainFileChooser (proj);
String title = NbBundle.getMessage(RendererServiceImpl.class,
"TTL_ChooseMainFile");
//Create a simple dialog descriptor describing what kind of dialog
//we want and its title and contents
final DialogDescriptor desc = new DialogDescriptor (chooser, title);
//The OK button should be disabled initially
desc.setValid(false);
//Create a property change listener. It will listen on the selection
//in our MainFileChooser, and enable the OK button if an appropriate
//node is selected:
PropertyChangeListener pcl = new PropertyChangeListener() {
public void propertyChange (PropertyChangeEvent pce) {
String propName = pce.getPropertyName();
if (ExplorerManager.PROP_SELECTED_NODES.equals(propName)) {
Node[] n = (Node[]) pce.getNewValue();
boolean valid = n.length == 1;
if (valid) {
DataObject ob = (DataObject)
n[0].getLookup().lookup(DataObject.class);
valid = ob != null;
if (valid) {
FileObject selectedFile = ob.getPrimaryFile();
String mimeType = selectedFile.getMIMEType();
valid = "text/x-povray".equals(mimeType);
}
}
desc.setValid(valid);
}
}
};
chooser.getExplorerManager().addPropertyChangeListener(pcl);
//Show the dialog: dialogResult will be OK or Cancel
Object dialogResult = DialogDisplayer.getDefault().notify(desc);
//If the user clicked OK, try to set the main file
//from the selection
if (DialogDescriptor.OK_OPTION.equals(dialogResult)) {
//Get the selected Node
Node[] n = chooser.getExplorerManager().getSelectedNodes();
//If it's > 1, explorer is broken: we set
//single selection mode
assert n.length <= 1;
DataObject ob = (DataObject) n[0].getLookup().lookup (
DataObject.class);
//Get the file from the data object
FileObject selectedFile = ob.getPrimaryFile();
//And save it as the main file
mfp.setMainFile(selectedFile);
}
} |
org.netbeans.examples.modules.povproject contains all
of the needed strings: add:
TTL_ChooseMainFile=No Main File Set in Project LBL_ChooseMainFile=Select Main File |
We now have POV-Ray support working fairly well: you can create a new project, render it, settings are remembered, images are opened and output goes to the Output window. There are just a few remaining problems: these are details, but small details are sometimes the most important ones:
These are things we should take care of now.
Resources
We're now in the final lesson of the course. There are many 'wrap up' tasks that you need to be aware of, beyond those that relate directly to this course. Before continuing with the lesson, read through and understand (and, where applicable, take the steps described) in the following:
Lab
The first thing we can do is improve the popup menu for POV-Ray projects: we just need to add a couple of menu items to those already returned.
NewAction in the Libraries panel. Rather than just using
the array of standard folder actions we are getting from the parent
folder, we will provide just those actions that we want.PovrayLogicalView in the editor, and find the
inner ScenesNode class.
Override getActions() as follows:
@Override
public Action[] getActions (boolean popup) {
Action[] result = new Action[] {
new ProjectAction (ActionProvider.COMMAND_BUILD,
NbBundle.getMessage(PovrayLogicalView.class, "LBL_Build"),
project),
new ProjectAction (ActionProvider.COMMAND_CLEAN,
NbBundle.getMessage(PovrayLogicalView.class, "LBL_Clean"),
project),
new OtherProjectAction (project, false),
SystemAction.get (NewTemplateAction.class),
SystemAction.get (FileSystemAction.class),
new OtherProjectAction(project, true),
};
return result;
} |
This gives us two classes to implement: ProjectAction and
OtherProjectAction.
The former will simply be an action class which delegates to the action
provider of the project, and the latter will use the OpenProjects
class from the Project UI API to close the project.
ProjectAction as follows:
private static class ProjectAction extends AbstractAction {
private final PovrayProject project;
private final String command;
public ProjectAction (String cmd, String displayName, PovrayProject prj) {
this.project = prj;
putValue (NAME, displayName);
this.command = cmd;
}
public void actionPerformed(ActionEvent actionEvent) {
ActionProvider prov = (ActionProvider)
project.getLookup().lookup(ActionProvider.class);
prov.invokeAction(command, null);
}
public boolean isEnabled() {
ActionProvider prov = (ActionProvider)
project.getLookup().lookup(ActionProvider.class);
return prov.isActionEnabled(command, null);
}
} |
OtherProjectAction
this way, also as a nested class inside PovrayLogicalView. What we're
doing here is saving the overhead of one more class to do something
simple, and writing one action that either closes the project or makes
it the main project, depending on a flag. While not beautiful, it is
short enough to be readable: and additional classes to come with a
memory penalty, so for trivial things, this approach is not necessarily
a bad idea: as long as the result is readable:
private static class OtherProjectAction extends AbstractAction {
private final PovrayProject project;
private final boolean isClose;
OtherProjectAction(PovrayProject project, boolean isClose) {
putValue (NAME, NbBundle.getMessage (PovrayLogicalView.class,
isClose ? "LBL_CloseProject" : "LBL_SetMainProject"));
this.project = project;
this.isClose = isClose;
}
public void actionPerformed(ActionEvent actionEvent) {
if (isClose) {
OpenProjects.getDefault().close (new Project[] { project });
} else {
OpenProjects.getDefault().setMainProject(project);
}
}
} |
LBL_Build=Render Project LBL_Clean=Clean Project LBL_CloseProject=Close Project LBL_SetMainProject=Set Main Project |
And we now have a much improved popup menu:
layer.xml file for the Povray Project Samples project.
Templates/Project/Standard,
our project templates will go into Templates/Project/Povray.
Copy the SystemFilesystem.localizingBundle entry from the
zip file, and paste a copy of it inside the folder tags for the
Povray folder. The end result should look like this:
<folder name="Templates"> <folder name="Project"> <folder name="Povray"> <attr name="SystemFileSystem.localizingBundle" stringvalue="org.netbeans.examples.modules.povproject.templates.Bundle"/> <file name="EmptyPovrayProjectProject.zip" url="EmptyPovrayProjectProject.zip"> <attr name="SystemFileSystem.localizingBundle" stringvalue="org.netbeans.examples.modules.povproject.templates.Bundle"/> <attr name="instantiatingIterator" methodvalue="org.netbeans.examples.modules.povproject.templates.EmptyPovrayProjectWizardIterator.createIterator"/> <attr name="instantiatingWizardURL" urlvalue="nbresloc:/org/netbeans/examples/modules/povproject/templates/EmptyPovrayProjectDescription.html"/> <attr name="template" boolvalue="true"/> </file> <file name="SamplePovrayProjectProject.zip" url="SamplePovrayProjectProject.zip"> <attr name="SystemFileSystem.localizingBundle" stringvalue="org.netbeans.examples.modules.povproject.templates.Bundle"/> <attr name="instantiatingIterator" methodvalue="org.netbeans.examples.modules.povproject.templates.EmptyPovrayProjectWizardIterator.createIterator"/> <attr name="instantiatingWizardURL" urlvalue="nbresloc:/org/netbeans/examples/modules/povproject/templates/SamplePovrayProjectDescription.html"/> <attr name="template" boolvalue="true"/> </file> </folder> </folder> </folder> |
org/netbeans/examples/modules/povproject/templates/Bundle.properties,
there are actually no localized file names in defined in that file. So
one other thing we need to do is define them:
Templates/Project/Povray=POV-Ray Templates/Project/Povray/EmptyPovrayProjectProject.zip=Empty Povray Project Templates/Project/Povray/SamplePovrayProjectProject.zip=Sample Povray Project |
org.netbeans.examples.modules.povproject.templates to
provide better descriptions.You have now extended NetBeans IDE to provide support for POV-Ray. Think back to some of the earlier tutorials you compeleted and... change your project to turn it into a standalone NetBeans Platform application, instead of an extension to NetBeans IDE. Think about these requirements and solve at least 3 of them completely:
The course, either directly or indirectly through its accompanying tutorials and other references, has made use of all of the following NetBeans API classes. Are you able to write 1 sentence about each of them? Can you use each of them in your code?
Are there any you can't remember? Don't worry! Look in the NetBeans API Javadoc and in the course notes above. Remind yourself about the class in question. Then make an appointment with yourself, 3 days later, to write 1 sentence about the API class that you forgot. Next, use the class in your code, while making sure that it ends up doing what you're expecting it to do.
Verify your understanding of the above classes by mapping a single API class to each of the statements below:
This NetBeans API class is especially useful when...
JFrame to the NetBeans Platform.
In general, when you hear the word "capability", which group of classes should you be thinking of when using the NetBeans Platform?
Finally, look through the list of classes above and identify 3 that you'd like to get to know better. Then use the resources, especially the Javadoc, FAQ, and NetBeans Platform tutorials, to explore your chosen classes. Make sure to also actually use them in a small/large plugin!
Congratulations. If everything worked for you as expected, you will now have completed the main part of the on-line training course. You can continue and become a certified NetBeans Platform engineer or a certified NetBeans Platform committer, by proving your knowledge of NetBeans module development. Do so, by taking one of the following steps:
When you have done so, write to the course mailing list and someone will verify that the plugin works correctly. You will then receive a certificate stating that you are a NetBeans Platform Certified Engineer.
You will then have a list of issues that have been specifically marked as being suitable for external contributors to fix:
If you would like to fix one of these issues, add a comment to it saying that you want to fix it. Then fix it and add the patch to the issue. The owner of the issue will review your code and, if the code is accepted, you will become a certified NetBeans Platform committer.
Click here to jump to an issue in Issuezilla that will give you a good example: Lorenz Weber, a student from Wuerzburg in Germany, is fixing an issue from Issuezilla and, as you can see, the NetBeans engineers are reviewing his code and giving him helpful feedback.
Add your idea for a project to the above exchange table. Potentially, someone else somewhere in the world will want to collaborate with you! When you have completed your project, write to the course mailing list and someone will verify that the plugin works correctly. You will then receive a certificate stating that you are a NetBeans Platform Certified Engineer.
Go to the List of Desirable Tutorials and pick a topic. Alternatively, pick your own topic, but make sure that it hasn't been covered already. If it's been covered already, maybe you can extend one of the existing tutorials with new information.
You might also want to create a screencast of a plugin you've created. Or the screencast could be about an interesting area of the NetBeans APIs. For example, maybe you've started using the Visual Library and have some interesting insights to share with others, via a screencast?
In all cases above, you will receive a certificate upon successful completion of the task, if you write to the course mailing list stating you have completed your task, describing what you did, and including your postal address. (Details about the mailing list are here.) Good luck!