Writing POV-Ray Support for NetBeans IX - Build Support
Tim Boudreau
10 June 2006
Feedback
This is a DRAFT!
This is a continuation of the tutorial for building POV-Ray support for
NetBeans. If you have not read the
first
or
second,
third,
fourth fifth
sixth,
seventh,
and
eighth
parts of this tutorial, you may want to start there.
Project Action Support
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:
- Open
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 string, Lookup lookup) throws IllegalArgumentException {
int idx = Arrays.asList (getSupportedActions()).indexOf (string);
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) {
ErrorManager.getDefault().notify(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) {
ErrorManager.getDefault().notify(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 (string);
}
}
public boolean isActionEnabled(String string, Lookup lookup) throws IllegalArgumentException {
int idx = Arrays.asList (getSupportedActions()).indexOf (string);
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;
}
Ensuring There Is A Main File
We now have handling implemented for all of the standard project actions that make
sense for a POV-Ray project. Now, if you've been following very carefully,
you may have noticed that there is one bug we need to fix:
isActionEnabled() will always return true for build. But we
implemented the following code in
Povray.getFileToRender():
render = provider.getMainFile();
if (render == null) {
ProjectInformation info = (ProjectInformation)
proj.getLookup().lookup(ProjectInformation.class);
throw new IOException (NbBundle.getMessage(Povray.class,
"MSG_NoMainFile", info.getDisplayName()));
}
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 and Dialogs APIs.
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:
- First we will set up two more dependencies. Right click the
Povray Projects project and select Properties.
- On the Libraries page of the Project Properties dialog, click the
Add button. Search for the class
DialogDisplayer to add
a dependency on the Dialogs API.
- Click the Add button again and search for the class
ExplorerManager to add a dependency on the Explorer and
Property Sheet API.
- Right click the
org.netbeans.examples.modules.povproject
package and chose New > JPanel Form. Name it "MainFileChooser"
The GUI Designer (Matisse)
will open.
- In the Palette, click the item for
JLabel and add
a JLabel to the top of the form.
- Drag the right-hand edge of the JLabel to the right edge of the
panel
- In the Palette, click the item for
JScrollPane and add
a JScrollPane to the form below the JLabel.
- Drag the bottom right corner of the
JScrollPane down
and to the right until the bottom and right edge alignment guidelines
appear. The result should look like this:
- Select the
JLabel and click the [...] button for its
text property in the Property Sheet. A custom editor
will open:
- Click the Format button. We will change the code that will be
generated from the default, which uses
ResourceBundle,
to the more NetBeans-friendly NbBundle pattern - change
the text format to NbBundle.getMessage(getClass(), "{key}"):
- Enter
LBL_ChooseMainFile for the key and
"Select Main File" for the value, as shown in the screen
shot above.
- Select the JScrollPane. Click
the Code button at the top of the Property Sheet to display code
generation properties.
- Edit the "Custom Creation Code" property, changing its
value to
new BeanTreeView();:
- Edit the signature of the class so that it implements the interface
ExplorerManager.Provider:
public class MainFileChooser extends javax.swing.JPanel implements ExplorerManager.Provider {
- Click the Source button to switch to the code editor, then press
Alt-Shift-F (Ctrl-Shift-F on Macintosh) to import
NbBundle,
ExplorerManager.Provider and BeanTreeView.
- Add the following code to implement
ExplorerManager.Provider:
private final ExplorerManager mgr = new ExplorerManager();
public ExplorerManager getExplorerManager() {
return mgr;
}
- Modify the constructor so it reads as follows:
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)));
BeanTreeView btv = (BeanTreeView) jScrollPane1;
btv.setPopupAllowed(false);
btv.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
//BeanTreeView shows no border by default:
btv.setBorder (new LineBorder(UIManager.getColor("controlShadow")));
}
The BeanTreeView we are showing is a UI class from the
Explorer API - in fact, it is the very same component that you see in
the Projects, Files, Runtime and Favorites tabs in the 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.
- Run Fix Imports to import
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(Object 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];
}
}
- Now we just need some code to use this panel. That code will
go in
RenderServiceImpl, 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;
}
}
- Now we need to implement the method we are calling,
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);
}
}
- Lastly we need to make sure that the resource bundle in
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