blob: 06d6ec6647d5da28b674ffde12e2c3e516d73fa3 [file] [log] [blame]
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Writing POV-Ray Support for NetBeans VI&#8212;Implementing the API</title>
<link rel="stylesheet" type="text/css" href="https://netbeans.org/netbeans.css"/>
<meta name="AUDIENCE" content="NBUSER"/>
<meta name="TYPE" content="ARTICLE"/>
<meta name="EXPIRES" content="N"/>
<meta name="developer" content="geertjan.wielenga@oracle.com"/>
<meta name="indexed" content="y"/>
<meta name="description"
content="NetBeans POV-Ray Support Tutorial Part VI&#8212;Implementing our API and making implementations of it available from our custom project type"/>
<!-- Copyright (c) 2006 Sun Microsystems, Inc. All rights reserved. -->
<!-- Use is subject to license terms.-->
</head>
<body>
<h1>Writing POV-Ray Support for NetBeans VI&#8212;Implementing the API</h1>
<p>This is a continuation of the tutorial for building a POV-Ray rendering application on
the NetBeans Platform. If you have not read the <a href="nbm-povray-1.html">first</a>,
<a href="nbm-povray-2.html">second</a>, <a href="nbm-povray-3.html">third</a>,
<a href="nbm-povray-4.html">fourth</a>, and <a href="nbm-povray-5.html">fifth</a>
parts of this tutorial, you may want to start there.</p>
<h2 class="tutorial"><a name="setup"></a>Implementing MainFileProvider</h2>
<p>The first class we will implement is <code>MainFileProvider</code>. This
is the class that our <code>Node</code>s 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).</p>
<div class="indent">
<ol>
<li><p>In the Povray Project module, add the key for the main file to the top of the <tt>PovrayProject</tt>
class:</p>
<pre class="examplecode">public static final String KEY_MAINFILE = "main.file";</pre>
</li>
<li><p>In the Povray Project module, create a new Java class in the package
<code>org.netbeans.examples.modules.povproject</code>, and call it
"MainFileProviderImpl".</p></li>
<li>Implement it as follows:
<pre class="examplecode">class MainFileProviderImpl extends MainFileProvider {
private final PovrayProject proj;
private FileObject mainFile = null;
private boolean checked = false;
MainFileProviderImpl(PovrayProject proj) {
this.proj = proj;
}
@Override
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;
}
@Override
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);
}
}
}</pre>
<p class="notes">The code above is simple and quite straightforward&#8212;it will look for
a <code>Properties</code> object in the <code>Lookup</code> of the
project. The getter will look for the value of "main.file"
from the <code>Properties</code> object (which was loaded from
<code>$PROJECT/pvproject/project.properties</code>), 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 <code>Properties</code> object. That
in turn, will cause the project to be marked as modified, so the system
will call <code>PovProjectFactory.saveProject()</code> if it is unloading
the project, causing the <code>Properties</code> to be written out to
disk in <code>$PROJECT/pvproject/project.properties</code>.</p>
</li>
<li><p>Add <code>new MainFileProviderImpl(this)</code> to the implementation
of <code>getLookup()</code> in <code>PovrayProject</code>, so that it
is included in the array of objects that make up the lookup contents:</p>
<pre class="examplecode">@Override
public Lookup getLookup() {
if (lkp == null) {
lkp = Lookups.fixed(new Object[]{
this, //handy to expose a project 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
<b>new MainFileProviderImpl(this)</b>
});
}
return lkp;
}</pre>
</li>
</ol>
</div>
<h2 class="tutorial"><a name="setup"></a>Implementing RendererService&#8212;Providing Default Renderer Settings</h2>
<p>The next class to implement is <code>RendererService</code>&#8212;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).</p>
<p>
As we discussed <a href="nbm-povray-2.html">earlier</a>, we are not going to
try to implement a complicated dialog that makes every possible POV-Ray
setting adjustable via a GUI widget&#8212;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 <code>project.properties</code> of a
POV-Ray project, to satisfy the needs of power users.</p>
<p>
Right now, we will not worry about the execution part&#8212;<code>RendererService</code>
also provides for named sets of settings&#8212;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 <code>RendererService</code>.</p>
<p>
This is where we will begin dealing directly with the System
Filesystem&#8212;the registry of runtime data supplied by modules. What we
will do is create <code>.properties</code> files for each set of standard
settings we will supply. We will start by creating <code>.properties</code>
files for each set of settings in our module.</p>
<div class="indent">
<ol>
<li>Create a new Java Package in the Povray Project project,
<code>org.netbeans.examples.modules.povproject.defaults</code>. This
is where we will put our properties files.
</li>
<li>
<p>Create six properties files with the following contents in that package:</p>
<ul>
<li>160x100.properties
<pre class="examplecode">
W=160
H=100
Q=4
FN=8
A=0.0
</pre>
</li>
<li>320x200.properties
<pre class="examplecode">
W=320
H=200
Q=4
FN=8
A=0.0
</pre>
</li>
<li>640x480.properties
<pre class="examplecode">
W=640
H=480
Q=4
FN=8
A=0.0
</pre>
</li>
<li>640x480hq.properties
<pre class="examplecode">
W=640
H=480
Q=R
FN=8
A=0.9
</pre>
</li>
<li>1024x768.properties
<pre class="examplecode">
W=1024
H=768
Q=4
FN=9
A=0.0
</pre>
</li>
<li>1024x768hq.properties
<pre class="examplecode">
W=1024
H=768
Q=R
FN=8
A=0.9
</pre>
</li>
</ul></li>
<li><p>Next, we will want to actually add these to the
System Filesystem, so our module can find them at runtime, and more
importantly, so other modules can modify and save, or add additional,
sets of default settings by adding more properties files to the
same folder we put these files in, in the System Filesystem.</p>
<p>Right-click the Povray Project project and choose
New | Other | Module Development | XML Layer. Then click
Next and Finish. The IDE creates the <tt>layer.xml</tt> file
and registers it in the project's manifest. Open the newly created
<tt>layer.xml</tt> file in the code editor.</p>
</li>
<li>
<p>Replace the content of the <tt>layer.xml</tt> file with the content below.</p>
<pre class="examplecode">&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;!DOCTYPE filesystem PUBLIC "-//NetBeans//DTD Filesystem 1.2//EN" "https://netbeans.org/dtds/filesystem-1_2.dtd"&gt;
&lt;filesystem&gt;
&lt;folder name="Povray"&gt;
&lt;folder name="RendererSettings"&gt;
&lt;!-- Declare a file, with its content provided by the URL. This
will be the command line arguments for 1024x768 high quality
rendering --&gt;
&lt;file name="1024x768hq.properties" url="defaults/1024x768hq.properties"&gt;
&lt;attr name="SystemFileSystem.localizingBundle"
stringvalue="org.netbeans.examples.modules.povproject.defaults.Bundle"/&gt;
&lt;/file&gt;
&lt;!-- This is an ordering attribute, it determines that the
DataFolder (but *not* the FileObject) for this folder will return
its child DataObjects (and thus also its Node's children) in
a specific order—in this case we are specifying that
1024x768hq must come before 1024x768.properties. --&gt;
&lt;attr name="1024x768hq.properties/1024x768.properties" boolvalue="true"/&gt;
&lt;file name="1024x768.properties" url="defaults/1024x768.properties"&gt;
&lt;attr name="SystemFileSystem.localizingBundle"
stringvalue="org.netbeans.examples.modules.povproject.defaults.Bundle"/&gt;
&lt;/file&gt;
&lt;attr name="1024x768.properties/640x480hq.properties" boolvalue="true"/&gt;
&lt;file name="640x480hq.properties" url="defaults/640x480hq.properties"&gt;
&lt;attr name="SystemFileSystem.localizingBundle"
stringvalue="org.netbeans.examples.modules.povproject.defaults.Bundle"/&gt;
&lt;/file&gt;
&lt;attr name="640x480hq.properties/640x480.properties" boolvalue="true"/&gt;
&lt;file name="640x480.properties" url="defaults/640x480.properties"&gt;
&lt;attr name="SystemFileSystem.localizingBundle"
stringvalue="org.netbeans.examples.modules.povproject.defaults.Bundle"/&gt;
&lt;/file&gt;
&lt;attr name="640x480.properties/320x200.properties" boolvalue="true"/&gt;
&lt;file name="320x200.properties" url="defaults/320x200.properties"&gt;
&lt;attr name="SystemFileSystem.localizingBundle"
stringvalue="org.netbeans.examples.modules.povproject.defaults.Bundle"/&gt;
&lt;/file&gt;
&lt;attr name="320x200.properties/160x100.properties" boolvalue="true"/&gt;
&lt;file name="160x100.properties" url="defaults/160x100.properties"&gt;
&lt;attr name="SystemFileSystem.localizingBundle"
stringvalue="org.netbeans.examples.modules.povproject.defaults.Bundle"/&gt;
&lt;/file&gt;
&lt;/folder&gt;
&lt;/folder&gt;
&lt;/filesystem&gt;</pre>
<p class="notes">What this XML does is map the properties files we just created into
the system filesystem in the folder <code>Povray/RendererSettings</code>,
which is where our code will look for them. Additionally, it specifies
<i>ordering attributes</i>, which are attributes we are adding to the
folder <code>RendererSettings/</code>, which will determine what order
the files will appear in when code asks for the array of children of
the <code>DataFolder</code> (<code>DataObject</code> subclass for
folders) or its Node for this folder.</p>
</li>
<li>
<p>You may have noticed the attribute <code>SystemFilesystem.localizingBundle</code>
which we added to the <code>RendererSettings</code> folder. NetBeans
<code>FileObject</code>s (which is what the "files" in the
System Filesystem are) can have ad-hoc key-value pairs associated
with them. <code>SystemFilesystem.localizingBundle</code> is a magic
attribute which the system will use to localize the names of files&#8212;all
you have to do is get the <code>DataObject</code> for a file in
the system filesystem, get the <code>Node</code> for that <code>DataObject</code>,
and the return value of <code>Node.getDisplayName()</code> for that
<code>Node</code> will look up its localized display name in the
requested resource bundle&#8212;this is how file names for things declared
in the System Filesystem are localized.</p>
<p>
So we need one <i>more</i> properties file in
<code>org.netbeans.examples.modules.povproject.defaults</code>&#8212;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.</p>
</li>
<li>
<p>Add the following contents to <code>Bundle.properties</code>:</p>
<p><pre class="examplecode">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</pre></p>
</li>
<li><p>Make sure that you have the new files in the correct places, reflecting
the structure in the image below:</p>
<p><img alt="" src="../../images/tutorials/povray/71/ch6/pic1.png"/></p>
</li>
</ol>
</div>
<h2 class="tutorial"><a name="rendererservice1"></a>Implementing RendererService&#8212;Basic Implementation</h2>
<p>Now we have a set of default settings to show, so we can implement the methods of
<code>RendererService</code> that will expose them.</p>
<div class="indent">
<ol>
<li>Create a new class, <code>RendererServiceImpl</code>, in
<code>org.netbeans.examples.modules.povproject</code>.</li>
<li>Modify the class declaration to say that it extends <code>RendererService</code>
and press Ctrl-Shift-I to fix imports and to generate stub implementations
of the abstract methods. The result should be as follows:
<pre class="examplecode">package org.netbeans.examples.modules.povproject;
import java.util.Properties;
import org.netbeans.examples.api.povray.RendererService;
import org.openide.filesystems.FileObject;
public class RendererServiceImpl extends RendererService {
@Override
public FileObject render(FileObject scene, String propertiesName) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public FileObject render(FileObject scene, Properties renderSettings) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public FileObject render(FileObject scene) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public FileObject render() {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public String[] getAvailableRendererSettingsNames() {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Properties getRendererSettings(String name) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public String getPreferredRendererSettingsNames() {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public String getDisplayName(String settingsName) {
throw new UnsupportedOperationException("Not supported yet.");
}
}</pre>
</li>
<li>The first things we will do are implement the constructor and
leave the render methods stubbed out&#8212;we will implement these later:
<pre class="examplecode">private PovrayProject proj;
public RendererServiceImpl(PovrayProject proj) {
this.proj = proj;
}
PovrayProject getProject() {
return proj;
}</pre>
</li>
<li><p>Next, we will implement some private utility methods that the other
methods will do. This should help to give some of the flavor of working
with things in the system filesystem.</p>
<pre class="examplecode"> private FileObject getRendererSettingsFolder() {
String folderName = "Povray/RendererSettings";
FileObject result = FileUtil.getConfigFile(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 static boolean logged = false;
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;
}
private void setPreferredRendererSettingsName(String val) {
getPreferences().put(KEY_PREFERRED_SETTINGS, val);
}
private static final String KEY_PREFERRED_SETTINGS = "preferredSettings";
static Preferences getPreferences() {
return Preferences.userNodeForPackage(RendererServiceImpl.class);
}</pre>
<p>The first thing we have is a utility method that finds the folder we
declared in our XML layer, in the System Filesystem&#8212;that is what
<code>getRendererSettingsFolder()</code> 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.</p>
<p>The next method is a utility method that just fetches the file corresponding
to a file name&#8212;we are returning the names of all files in the settings
folder, so this will allow us to find a corresponding properties file.</p>
<p>The last two methods are simply for saving the last-used set of renderer
settings, and simply use the standard Java Preferences API.</p>
</li>
<li>
<p>The next method we want to implement is
<code>getAvailableRendererSettingsNames()</code>. This method will
return an array of <code>String</code>s&#8212;the localized, human-friendly
names of all of the files which we declared above:</p>
<pre class="examplecode"> @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;
}</pre>
<p>This is quite straightforward&#8212;we just iterate all of the files in the
folder, and return an array of <code>String</code>s with their names. The
one twist to it is that we don't iterate the <i><code>FileObject</code></i>'s
children, but rather we get a <code>DataFolder</code> (the <code>DataObject</code>
type for filesystem folders), and iterate its children. The reason we do
it this way is that the order of children of <code>FileObjects</code> is
undefined&#8212;we might get the files we declared in any order. The
<code>DataFolder</code>, however, understands <i>ordering attributes</i> -
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&#8212;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
<a href="https://netbeans.org/download/dev/javadoc/org-openide-util/org/openide/util/Utilities.html#topologicalSort(java.util.Collection,%20java.util.Map)">Utilities.topologicalSort()</a>).</p>
</li>
<li><p>Next we will implement <code>getRendererSettings(name)</code>&#8212;this method
will actually get a <code>Properties</code> object with the contents of
whichever file name was passed to it:</p>
<pre class="examplecode"> @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 NullPointerException("Requested non-existent settings " +
"file " + name));
}
return result;
}</pre>
<p>The code here is also quite straightforward&#8212;it simply tries to load a
<code>Properties</code> object from the input stream of the file in question.</p>
</li>
<li>
<p>Next we will implement the method that fetches the name of the preferred
set of settings&#8212;this will be the most recently used settings, fetched from
the Preferences API, with a fallback if none has yet been chosen:</p>
<pre class="examplecode"> @Override
public String getPreferredRendererSettingsName() {
String result = getPreferences().get(KEY_PREFERRED_SETTINGS, null);
if (result == null) {
result = "640x480.properties";
}
return result;
}</pre>
</li>
<li>
<p>The last method we will implement takes a settings <i>file name</i> and
converts it to a localized, human-readable name:</p>
<pre class="examplecode"> @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;
}</pre>
<p>Human-readable display names are provided by <code>Nodes</code>&#8212;a
<code>FileObject</code> is simply a file on disk (or similar storage such
as the System Filesystem via our <code>layer.xml</code> file)&#8212;it has
no notion of human readability. So if we want the <i>localized</i> name
for a <code>FileObject</code>, we need to get the <code>Node</code> for it.
In this case, the <code>Node</code> will use the hint we provided
in the <code>layer.xml</code> file:</p>
<pre class="examplecode">&lt;attr name="SystemFileSystem.localizingBundle" stringvalue="org.netbeans.modules.povproject.defaults.Bundle"/&gt;</pre>
<p>and look up its localized name in
<code>org.netbeans.examples.modules.povproject.defaults.Bundle.properties</code>.</p>
</li>
<li><p>As we did earlier with the <tt>MainFileProviderImpl</tt>, we now need to
expose our implementation of <tt>RendererService</tt> via the
project's lookup. Modify <tt>PovrayProject.getLookup()</tt> as follows: </p>
<pre class="examplecode">public Lookup getLookup() {
if (lkp == null) {
lkp = Lookups.fixed(new Object[] {
this, //handy to expose a project 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), //So things can set the main file
<b>new RendererServiceImpl(this), //Renderer Service Implementation</b>
});
}
return lkp;
}</pre>
</li>
</ol>
</div>
<h2 class="tutorial"><a name="setup"></a>Providing Render Actions on POV-Ray Files</h2>
<p>Now we have an implementation of some of our API, the next step is to use it.
As <a href="nbm-povray-2.html">discussed earlier</a>, we want a 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
<code>PovrayDataNode</code>s which will allow the user to render the file
with one of our sets of settings.</p>
<div class="indent">
<ol>
<li>Open <code>PovrayDataNode</code>, from the Povray File Support project,
in the code editor</li>
<li><p>Press Ctrl-I (Command-I on Macintosh) and override the
<code>getActions(boolean)</code> method. Implement it as follows:</p>
<pre class="examplecode">@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;
} else {
//Isolated file in the favorites window or something
result = actions;
}
return result;
}</pre>
<p>This method will add two (yet to be implemented) actions into the array
of actions, if a renderer service for this file can be found. It positions
them as the second and third elements in the array, since the first element is
what will be invoked when you double click the file, and we want that
to remain opening the file (we could also override
<code>getPreferredAction()</code> to determine what happens when
the node is doubled clicked).</p>
</li>
<li><p>Now we need to implement RendererAction. Right-click the
<code>org.netbeans.examples.modules.povfile</code> package, and create
a new Java Class called <code>RendererAction</code>. Define it
as follows:
<pre class="examplecode">public class RendererAction extends AbstractAction implements Presenter.Popup {</pre></p>
<p>Implementing <code>Presenter.Popup</code> is an important step&#8212;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 <code>getPopupPresenter</code> which returns a JMenuItem
(remember that in Swing, JMenu is a subclass of JMenuItem, so it's
legal to return whole submenu here).
In our case, we want a submenu:</p>
<ul>
<li>Render
<ul>
<li>1024 x 768 High Quality</li>
<li>1024 x 768</li>
<li>640 x 480 High Quality</li>
<li>640 x 480 High Quality</li>
<li>320 x 200</li>
<li>160 x 120</li>
</ul>
</li>
<li>Standard file menu items...</li>
</ul>
</li>
<li><p>Now we will provide the body of <code>RendererAction</code>:</p>
<pre class="examplecode">package org.netbeans.examples.modules.povfile;
import java.awt.event.ActionEvent;
import javax.swing.AbstractAction;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import org.netbeans.examples.api.povray.RendererService;
import org.openide.util.NbBundle;
import org.openide.util.actions.Presenter;
public class RendererAction extends AbstractAction implements Presenter.Popup {
private final RendererService renderer;
private final PovrayDataNode node;
public RendererAction(RendererService renderer, PovrayDataNode node) {
this.renderer = renderer;
this.node = node;
}
@Override
public void actionPerformed(ActionEvent e) {
assert false;
}
@NbBundle.Messages("LBL_Render=Render")
@Override
public JMenuItem getPopupPresenter() {
JMenu result = new JMenu();
//Set the menu's label
result.setText(Bundle.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;
}
}</pre>
</li>
<li><p>The one thing missing here, of course, is the individual actions
that will run the renderer with different sets of settings. Create an
inner class of <code>RendererAction</code> called
<code>RenderWithSettingsAction</code>, and implement it as follows:</p>
<pre class="examplecode">private class RenderWithSettingsAction extends AbstractAction implements Runnable {
private final String name;
public RenderWithSettingsAction(String name) {
this.name = name;
putValue(NAME, renderer.getDisplayName(name));
}
@Override
public void actionPerformed(ActionEvent e) {
RequestProcessor.getDefault().post(this);
}
@Override
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);
}
}
}
}</pre>
<p>This is relatively straightforward as well. We are using the
<code>renderer</code> 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 <code>renderer.render()</code>.</p>
<p>The two interesting areas are how we find the file, and how we actually
perform the rendering. We have the instance of <code>PovRayDataNode</code>
that we are operating against. It is a subclass of <code>DataNode</code>,
so we can call its <code>getDataObject()</code> method (another way
would be to call <code>node.getLookup().lookup(DataObject.class)</code>,
but since we know its type, calling <code>getDataObject()</code> is
more efficient). From that we may call <code>getPrimaryFile()</code> to
actually get the <code>FileObject</code> that should be rendered into
an image by POV-Ray.</p>
<p>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.</p>
</li>
</ol>
</div>
<h2 class="tutorial"><a name="pov-exe"></a>Implementing SetMainFileAction</h2>
<p>The other action we added to the array of actions on the popup menu for
POV-Ray files will set the main file of the project to be whatever file
was clicked.</p>
<div class="indent">
<ol>
<li>We will simply implement this as an inner class of <code>PovrayDataNode</code>.
Open <code>PovrayDataNode</code> in the code editor.</li>
<li>Implement it as follows. The only twist is that if our <code>Node</code>
becomes the main file, it needs to tell the former main file that it is
not the main file anymore&#8212;more specifically, it needs to force it to
fire a property change in its display name so that it gets redrawn as
non-bold:
<pre class="examplecode">@NbBundle.Messages("CTL_SetMainFile=Set Main File")
private final class SetMainFileAction extends AbstractAction {
public SetMainFileAction() {
putValue(NAME, Bundle.CTL_SetMainFile());
}
@Override
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;
}
}</pre>
</li>
<li><p>Run the application, create or open a POV-Ray project,
right-click on a .pov file, and you should see your
new Actions on the Node:</p>
<p><img alt="" src="../../images/tutorials/povray/71/ch6/pic2.png"/></p>
<p class="notes"><b>Note:</b> Though the Set Main File
action should work correctly, the rendering Actions
do not work yet because we have not implemented them
yet. That will be done later in this tutorial.</p>
</li>
</ol>
</div>
<h2 class="tutorial"><a name="pov-exe"></a>Locating the POV-Ray Executable</h2>
<p>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.</p>
<div class="indent">
<ol>
<li>Create a new Java class in the Povray Project project, in
<code>org.netbeans.examples.modules.povproject</code>, called
"Povray".</li>
<li>
<p>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 <code>JFileChooser</code> to let the
user locate the POV-Ray executable and the directory with the standard
POV-Ray include files&#8212;once this has been done once, we will store
the result so we do not have to ask again unless it is deleted.</p>
<p>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 <code>Povray</code>:
<pre class="examplecode">private static File locate(String key) {
JFileChooser jfc = new JFileChooser();
jfc.setDialogTitle(NbBundle.getMessage(Povray.class, key));
jfc.setFileSelectionMode (JFileChooser.FILES_ONLY);
jfc.showOpenDialog(WindowManager.getDefault().getMainWindow());
File result = jfc.getSelectedFile();
return result;
}</pre>
<p class="notes">At this point we need to add another dependency, because we are calling
<code>WindowManager</code> above. 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 <i>behind</i> the main window.
This makes sure it stays on top. Add a dependency on the Window System API to Povray Project, by
right-clicking the project's Libraries node and choosing
Add Module Dependency.</p>
</li>
<li><p>As you can see in the above code, we will be fetching a localized
string from a resource bundle&#8212;a different one depending on whether
we're looking for the executable or include directory. So let's add
those strings to the resource bundle for this package via Bundle
annotations. We will also
add one warning message we will need later.</p>
<p>Next, we will add the two methods for fetching the POV-Ray executable
and the include directory, which will automatically ask the user if they
are unknown or unavailable. Add the following two methods, and their
associated fields to
<code>Povray</code>:</p>
<pre class="examplecode">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";
@NbBundle.Messages({"TTL_FindPovray=Locate POV-Ray Executable",
"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 &lt;a href=\"http://www.imagico.de/files/povcyg_350c.zip\"&gt;"
+ "http://www.imagico.de/files/povcyg_350c.zip&lt;/a&gt;</html>"
})
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(Bundle.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,
Bundle.MSG_WindowsWarning()),
NotifyDescriptor.WARNING_MESSAGE);
Object result = DialogDisplayer.getDefault().notify(msg);
if (result == NotifyDescriptor.CANCEL_OPTION) {
return null;
}
}
povray = maybePov;
if (povray != null) {
prefs.put(KEY_POVRAY_EXEC, povray.getPath());
}
}
}
return povray;
}
@NbBundle.Messages("TTL_FindIncludeDir=Find POV-Ray Standard Include File Dir")
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.getParentFile().getParent()
+ File.separator + "include");
if (!include.exists()) {
include = locate(Bundle.TTL_FindIncludeDir());
if (include != null) {
prefs.put(KEY_POVRAY_INCLUDES, include.getPath());
} else {
include = null;
}
}
}
return include;
}</pre>
</li>
</ol>
</div>
<h2 class="tutorial"><a name="executing"></a>Next Steps</h2>
<p>The <a href="nbm-povray-7.html">next section</a> will cover actually
executing POV-Ray and piping its output to the Output window of our application.</p>
</body>
</html>