blob: 1ee73444caabbabf7b2d0c7c9cf2a1a819df266f [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 VIII&#8212;Implementing ViewService and its Actions</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 VIII&#8212;Implementing the last part of our API and using it from file nodes"/>
<!-- Copyright (c) 2006 Sun Microsystems, Inc. All rights reserved. -->
<!-- Use is subject to license terms.-->
</head>
<body>
<h1>Writing POV-Ray Support for NetBeans VIII&#8212;Implementing ViewService and its Actions</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>, <a href="nbm-povray-5.html">fifth</a>,
<a href="nbm-povray-6.html">sixth</a>, and <a href="nbm-povray-7.html">seventh</a>
parts of this tutorial, you may want to start there.</p>
<h2 class="tutorial"><a name="viewservice-impl"></a>ViewService&#8212;the Final API Piece</h2>
<p>The last piece of our API to implement is <code>ViewService</code>, which will
allow us to show the most recently rendered image file associated with a
POV-Ray file.</p>
<div class="indent">
<ol>
<li>Create a new Java class in
<code>org.netbeans.examples.modules.povproject</code>, called
&quot;ViewServiceImpl&quot;.</li>
<li><p>We have one utility method we created earlier, for stripping the
extension from a file name. We might as well reuse it here, since here
we will also need to compute the image name given a scene file. So
open the <code>Povray</code> class in the editor, and modify
the signature of <code>stripExtension()</code> as follows, so that
it is changed from <tt>private</tt> to <tt>public static</tt>:</p>
<pre class="examplecode"><b>public static</b> String stripExtension(File f) {</pre>
</li>
<li><p>Returning to <code>ViewServiceImpl</code>, implement <tt>ViewService</tt>
and invoke Fix Imports and use the
&quot;Implement All Abstract Methods&quot; hint to provide skeleton
implementations of all of the methods:</p>
<pre class="examplecode">package org.netbeans.examples.modules.povproject;
import org.netbeans.examples.api.povray.ViewService;
import org.openide.filesystems.FileObject;
public class ViewServiceImpl implements ViewService {
@Override
public boolean isRendered(FileObject file) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public boolean isUpToDate(FileObject file) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public void view(FileObject file) {
throw new UnsupportedOperationException("Not supported yet.");
}
}</pre>
</li>
<li><p>Now, add the following method to actually find the image file
for a given scene file:</p>
<pre class="examplecode">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;
}</pre>
</li>
<li><p>Implement the constructor and API methods as follows:</p>
<pre class="examplecode">private final PovrayProject proj;
public ViewServiceImpl(PovrayProject proj) {
this.proj = proj;
}
@Override
public boolean isRendered(FileObject file) {
return getImageFor (file) != null;
}
@Override
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;
}
@Override
public void view(FileObject scene) {
FileObject image = getImageFor(scene);
if (image != null) {
DataObject dob;
try {
dob = DataObject.find(image);
OpenCookie open = dob.getNodeDelegate().getLookup().lookup(OpenCookie.class);
if (open != null) {
open.open();
return;
}
} catch (DataObjectNotFoundException ex) {
Exceptions.printStackTrace(ex);
}
}
Toolkit.getDefaultToolkit().beep();
}</pre>
</li>
<li><p>Now we just need to expose our implementation of <code>ViewService</code>
via the project's lookup. Modify <code>PovrayProject.getLookup()</code>
as follows:</p>
<pre class="examplecode">private Lookup lkp;
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 RendererServiceImpl(this), //Renderer Service Implementation
new MainFileProviderImpl(this), //So things can set the main file
<b>new ViewServiceImpl(this), //Allow things to find/open the image associated with a scene file</b>
});
}
return lkp;
}</pre>
<p class="tips"> 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&#8212;if you add to the
array, you only change the lines you added.</p>
</li>
</ol>
</div>
<h2 class="tutorial"><a name="view-action"></a>Adding a View action to POV-Ray File Nodes</h2>
<p>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 &quot;View&quot; action to our POV-Ray file
nodes.</p>
<div class="indent">
<ol>
<li>In the Povray File Support project, open <code>PovRayDataNode</code>
in the <code>org.netbeans.examples.modules.povfile</code> package.</li>
<li><p>First, we will add one more action into the array of popup menu
actions from <code>PovrayDataNode</code> (modified and new lines in
<b>bold</b>):</p>
<pre class="examplecode">public Action[] getActions (boolean popup) {
Action[] actions = super.getActions(popup);
RendererService renderer =
(RendererService)getFromProject (RendererService.class);
Action[] result;
if (renderer != null && actions.length &gt; 0) { //should always be &gt; 0
Action rendererAction = new RendererAction (renderer, this);
<b>result = new Action[ actions.length + 3 ];</b>
result[0] = actions[0];
result[1] = new SetMainFileAction();
result[2] = rendererAction;
<b>result[3] = new ViewAction();</b>
} else {
//Isolated file in the favorites window or something
result = actions;
}
return result;
}</pre>
</li>
<li><p>Now we need to implement ViewAction. This can be an inner
class inside <code>PovrayDataNode</code>:</p>
<pre class="examplecode">@NbBundle.Messages("LBL_View=View")
private class ViewAction extends AbstractAction {
ViewAction() {
putValue(Action.NAME, Bundle.LBL_View());
}
@Override
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;
}
}</pre>
</li>
</ol>
</div>
<p><p>At this point, we are ready to run the code. Note that POV-Ray files now
have a working View menu item:</p>
<p><img alt="" src="../../images/tutorials/povray/71/ch8/pic1.png"/></p>
</p>
<h2 class="icon-badging"><a name="next"></a>Icon-Badging&#8212;Adding File Listening Support</h2>
<p>You may have noticed that there are a few methods we are not using on
<code>ViewService</code>, particularly <code>isUpToDate()</code>. In the
NetBeans IDE, the icon for Java classes has a &quot;badge&quot; in the lower
right if the compiled version of it is older than the source file and it
probably needs recompilation.</p>
<p>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 tutorial, 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
<code>isUpToDate()</code> and mark the <code>PovrayDataNode</code> icon
if it is false.</p>
<p>To do this, we will need to add a method to <code>RendererService</code>
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 <code>RendererService</code> is an abstract class&#8212;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 <code>RendererService</code> already has a final
method with the same name and signature [in fact exactly this happened to
NetBeans when <code>getCause()</code> was added to <code>Throwable</code>
in JDK 1.3], but it is a reasonable change). In this case, of course, we
know we are the only ones implementing <code>RendererService</code>, 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.</p>
<div class="indent">
<ol>
<li>Open <code>RendererService</code>, in the Povray API project's
<code>org.netbeans.modules.examples.api.povray</code> package, in the
code editor.</li>
<li><p>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. Note that since our <code>PovrayDataNodes</code>
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.</p>
<pre class="examplecode">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));
}
}
}</pre>
<p class="notes"> At this stage, the import statement block
at the top of the above class should be as follows:
<pre class="java">import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.util.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.openide.filesystems.FileObject;</pre>
</p>
</li>
<li><p>Next we need to implement the two protected methods we defined above,
in our implementation of <code>RendererService</code>. In the
Povray File Support project, open
<code>RendererServiceImpl</code> in the code editor.</p></li>
<li><p>Now, we will need to implement a listener interface on
<code>RendererServiceImpl</code>, so modify its signature as follows:</p>
<pre class="examplecode">final class RendererServiceImpl extends RendererService <b>implements FileChangeListener</b> {</pre>
<p class="tips">Use the editor hint to create skeleton implementations of the methods
of these interfaces. The thing to note here is that, unlike <code>java.io.File</code>,
it is possible to listen for changes on <code>org.openide.filesystems.FileObject</code>,
either folders or files.</p>
</li>
<li>
<p>The API class, <code>RendererService</code>, 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:</p>
<pre class="examplecode">//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);
}
@Override
public void fileFolderCreated(FileEvent fileEvent) {
//Do nothing
}
@Override
public void fileDataCreated(FileEvent fileEvent) {
FileObject created = fileEvent.getFile();
fireSceneChange(created);
}
@Override
public void fileChanged(FileEvent fileEvent) {
FileObject changed = fileEvent.getFile();
fireSceneChange(changed);
}
@Override
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;
}
}
@Override
public void fileRenamed(FileRenameEvent fileRenameEvent) {
//do nothing
}
@Override
public void fileAttributeChanged(FileAttributeEvent fileAttributeEvent) {
//do nothing
}</pre>
</li>
<li><p>One last change we need to make is to the <code>render()</code> method in
the <tt>RenderServiceImpl</tt> class&#8212;it
is possible that the <code>images/</code> directory of the project was
simply not there&#8212;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 <code>images/</code> folder,
and if not, start now that it's created. So, we need to modify the
implementation of <code>render()</code> slightly:</p>
<pre class="examplecode">@Override
public FileObject render(FileObject scene, Properties renderSettings) {
Povray pov = new Povray(this, scene, renderSettings);
<b>FileObject result;</b>
try {
result = pov.render();
<b>if (!listeningToImagesFolder) {
startListeningToImagesFolder();
}</b>
} catch (IOException ioe) {
Exceptions.printStackTrace(ioe);
<b>result = null;</b>
}
<b>return result;</b>
}
</pre></li>
</ol>
</div>
<p class="notes"> One thing worth noting is our use of the <code>WeakListeners</code> utility
class. This can be used to generate a variant of any event listener which
will only reference the actual listener weakly&#8212;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
<code>FileObject</code>s we listen to can outlive the <code>RendererServiceImpl</code>
or the <code>Project</code> and not force them to be retained in memory
simply because something wanted to listen to changes in a file or folder.</p>
<h2 class="icon-badging"><a name="next"></a>Icon-Badging&#8212;Implementing Icon Badging</h2>
<p>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&#8212;<code>ImageUtilities.mergeImages()</code>.</p>
<div class="indent">
<ol>
<li>In the Povray File support module,
edit the class declaration of <code>PovrayDataNode</code> so that it
implements <code>ChangeListener</code> and add the appropriate <code>stateChanged()</code>
method.</li>
<li><p>Add the highlighted code below, in the constructor for <code>PovrayDataNode</code>:</p>
<pre class="examplecode">public PovrayDataNode(PovrayDataObject obj) {
super(obj, Children.LEAF);
<b>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);
}</b>
}</pre>
</li>
<li><p>The <code>stateChanged()</code> method can be implemented very simply:</p>
<pre class="examplecode">public void stateChanged(ChangeEvent changeEvent) {
<b>fireIconChange();</b>
}</pre>
</li>
<li><p>Now we need to override <code>getIcon()</code> to return different
icons depending on the state of the <code>Node</code>:</p>
<pre class="examplecode">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;
}</pre>
<p>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.</p>
</li>
<li><p>Create the necessary image files in the
<code>org.netbeans.examples.modules.povfile</code> package&#8212;here are
the ones used in the tutorial:</p>
<ul>
<li><b>hasImageBadge.png</b> <img alt="" src="../../images/tutorials/povray/hasImageBadge.png"/></li>
<li><b>hasNoImageBadge.png</b> <img alt="" src="../../images/tutorials/povray/hasNoImageBadge.png"/></li>
<li><b>needsRenderBadge.png</b> <img alt="" src="../../images/tutorials/povray/needsRenderBadge.png"/></li>
</ul>
</li>
<li><p>Run the application. Notice the icon badging, and the changes when you
render or remove rendered images:</p>
<p><img alt="" src="../../images/tutorials/povray/71/ch8/pic2.png"/></p>
</li>
</ol>
</div>
<h2 class="tutorial"><a name="next"></a>Next Steps</h2>
<p>We're almost done. The <a href="nbm-povray-9.html">next step</a>
will be adding project build support and putting some finishing
touches on our UI and code.</p>
</body>
</html>