blob: 232a0cb1bd3501459a101373fa722b6ea052ca54 [file] [log] [blame]
<!doctype html>
<html class="no-js" lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>Writing POV-Ray Support for NetBeans VIII—Implementing ViewService and its Actions</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Writing POV-Ray Support for NetBeans VIII—Implementing ViewService and its Actions - Apache NetBeans">
<meta name="author" content="Apache NetBeans">
<meta name="description" content="Writing POV-Ray Support for NetBeans VIII—Implementing ViewService and its Actions - Apache NetBeans">
<meta name="keywords" content="Apache NetBeans Platform, Platform Tutorials, Writing POV-Ray Support for NetBeans VIII—Implementing ViewService and its Actions">
<meta name="generator" content="Apache NetBeans">
<link rel="stylesheet" href="../../../../_/css/font-awesome.min.css">
<link rel="alternate" type="application/atom+xml" title="Apache NetBeans Blog" href="https://netbeans.apache.org/blogs/atom" />
<link rel="stylesheet" href="../../../../_/css/highlightjs/default.min.css">
<link rel="stylesheet" href="../../../../_/css/netbeans.css">
<link rel="apple-touch-icon" sizes="180x180" href="../../../../_/images/fav/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="../../../../_/images/fav/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="../../../../_/images/fav/favicon-16x16.png">
<link rel="manifest" href="../../../../_/images/fav/site.webmanifest">
<link rel="mask-icon" href="../../../../_/images/fav/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#ffc40d">
<meta name="theme-color" content="#ffffff">
<link href="../../../../_/css/font-open-sans.css" rel="stylesheet">
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
</head>
<body>
<div class="title-bar" data-responsive-toggle="responsive-menu" data-hide-for="medium">
<button type="button" data-toggle="responsive-menu"><i style='font-size: 32px; color: #fff; padding: 8px' class='fa fa-bars'></i></button>
<div class="title-bar-title">Apache NetBeans</div>
</div>
<div class="top-bar" id="responsive-menu">
<div class='top-bar-left'>
<a class='title' href="../../../../index.html"><img src='../../../../_/images/apache-netbeans.svg' style='padding: 8px; height: 48px;'> Apache NetBeans</a>
</div>
<div class="top-bar-right">
<ul class="vertical medium-horizontal menu" data-responsive-menu="drilldown medium-dropdown">
<li> <input id="search-input" type="text" placeholder="Search the docs"> </li>
<li> <a href="../../../../front/main/community">Community</a> </li>
<li> <a href="../../../../front/main/participate">Participate</a> </li>
<li> <a href="../../../../front/main/blogs">Blog</a></li>
<li> <a href="../../../../front/main/help">Get Help</a> </li>
<li> <a href="https://plugins.netbeans.apache.org/">Plugins</a> </li>
<li> <a href="../../../../front/main/download">Download</a> </li>
</ul>
</div>
</div>
<!-- src/templates/news -->
<section class="hero news alternate">
<div class='grid-container'>
<div class='cell'>
<div class="annotation">Latest release</div>
<h1>Apache NetBeans 28</h1>
<p><a class="button success" href="../../../../front/main/download/nb28">Download</a></p>
</div>
</div>
</section>
<div class='grid-container main-content tutorial'>
<h1 class="sect0">Writing POV-Ray Support for NetBeans VIII—Implementing ViewService and its Actions</h1>
<div class="sectionbody">
<div class="admonitionblock note">
<table>
<tbody><tr>
<td class="icon"><i class="fa icon-note" title="Note"></i></td>
<td class="content">This tutorial needs a review.
You can <a href="https://github.com/apache/netbeans-antora-tutorials/edit/main/modules/ROOT/pages/tutorials/nbm-povray-8.adoc" title="Edit this tutorial in github">edit it in GitHub </a>
following these <a href="../../../../tutorial/main/kb/docs/contributing">contribution guidelines.</a></td>
</tr></tbody>
</table>
</div>
</div>
<div id="toc" class="toc">
<div id="toctitle"></div>
<ul class="sectlevel1">
<li><a href="#_viewservicethe_final_api_piece">ViewService—the Final API Piece</a></li>
<li><a href="#_adding_a_view_action_to_pov_ray_file_nodes">Adding a View action to POV-Ray File Nodes</a></li>
<li><a href="#_icon_badgingadding_file_listening_support">Icon-Badging—Adding File Listening Support</a></li>
<li><a href="#_icon_badgingimplementing_icon_badging">Icon-Badging—Implementing Icon Badging</a></li>
<li><a href="#_next_steps">Next Steps</a></li>
</ul>
</div>
<div id="preamble">
<div class="sectionbody">
<div class="paragraph">
<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/" class="xref page">first</a>, <a href="../nbm-povray-2/" class="xref page">second</a>, <a href="../nbm-povray-3/" class="xref page">third</a>, <a href="../nbm-povray-4/" class="xref page">fourth</a>, <a href="../nbm-povray-5/" class="xref page">fifth</a>, <a href="../nbm-povray-6/" class="xref page">sixth</a>, and <a href="../nbm-povray-7/" class="xref page">seventh</a> parts of this tutorial, you may want to start there.</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_viewservicethe_final_api_piece"><a class="anchor" href="#_viewservicethe_final_api_piece"></a>ViewService—the Final API Piece</h2>
<div class="sectionbody">
<div class="paragraph">
<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>
<div class="olist arabic">
<ol class="arabic" start="1">
<li>
<p>Create a new Java class in <code>org.netbeans.examples.modules.povproject</code>, called "ViewServiceImpl".</p>
</li>
</ol>
</div>
<div class="paragraph">
<p>1.
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 <code>private</code> to <code>public static</code> :</p>
</div>
<div class="paragraph">
<p><strong>public static</strong></p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-java hljs" data-lang="java"> String stripExtension(File f) {</code></pre>
</div>
</div>
<div class="olist arabic">
<ol class="arabic" start="3">
<li>
<p>Returning to <code>ViewServiceImpl</code>, implement <code>ViewService</code> and invoke Fix Imports and use the "Implement All Abstract Methods" hint to provide skeleton implementations of all of the methods:</p>
</li>
</ol>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-java hljs" data-lang="java">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.");
}
}</code></pre>
</div>
</div>
<div class="olist arabic">
<ol class="arabic" start="4">
<li>
<p>Now, add the following method to actually find the image file for a given scene file:</p>
</li>
</ol>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-java hljs" data-lang="java">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;
}</code></pre>
</div>
</div>
<div class="olist arabic">
<ol class="arabic" start="5">
<li>
<p>Implement the constructor and API methods as follows:</p>
</li>
</ol>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-java hljs" data-lang="java">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();
}</code></pre>
</div>
</div>
<div class="olist arabic">
<ol class="arabic" start="6">
<li>
<p>Now we just need to expose our implementation of <code>ViewService</code> via the project&#8217;s lookup. Modify <code>PovrayProject.getLookup()</code> as follows:</p>
</li>
</ol>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-java hljs" data-lang="java">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
*new ViewServiceImpl(this), //Allow things to find/open the image associated with a scene file*
});
}
return lkp;
}</code></pre>
</div>
</div>
<div class="paragraph">
<p>The trailing comma in the array definition is not strictly necessary, but it&#8217;s a useful technique for reducing the CVS diff if you&#8217;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.</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_adding_a_view_action_to_pov_ray_file_nodes"><a class="anchor" href="#_adding_a_view_action_to_pov_ray_file_nodes"></a>Adding a View action to POV-Ray File Nodes</h2>
<div class="sectionbody">
<div class="paragraph">
<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 "View" action to our POV-Ray file nodes.</p>
</div>
<div class="olist arabic">
<ol class="arabic" start="1">
<li>
<p>In the Povray File Support project, open <code>PovRayDataNode</code> in the <code>org.netbeans.examples.modules.povfile</code> package.</p>
</li>
</ol>
</div>
<div class="paragraph">
<p>1.
First, we will add one more action into the array of popup menu actions from <code>PovrayDataNode</code> (modified and new lines in <strong>bold</strong>):</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-java hljs" data-lang="java">public Action[] getActions (boolean popup) {
Action[] actions = super.getActions(popup);
RendererService renderer =
(RendererService)getFromProject (RendererService.class);
Action[] result;
if (renderer != null &amp;amp;&amp;amp; actions.length &gt; 0) { //should always be &gt; 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();*
} else {
//Isolated file in the favorites window or something
result = actions;
}
return result;
}</code></pre>
</div>
</div>
<div class="olist arabic">
<ol class="arabic" start="3">
<li>
<p>Now we need to implement ViewAction. This can be an inner class inside <code>PovrayDataNode</code>:</p>
</li>
</ol>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-java hljs" data-lang="java">@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;
}
}</code></pre>
</div>
</div>
<div class="paragraph">
<p>At this point, we are ready to run the code. Note that POV-Ray files now have a working View menu item:</p>
</div>
<div class="imageblock">
<div class="content">
<img src="../../_images/tutorials/povray_71_ch8_pic1.png" alt="povray 71 ch8 pic1">
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_icon_badgingadding_file_listening_support"><a class="anchor" href="#_icon_badgingadding_file_listening_support"></a>Icon-Badging—Adding File Listening Support</h2>
<div class="sectionbody">
<div class="paragraph">
<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 "badge" in the lower right if the compiled version of it is older than the source file and it probably needs recompilation.</p>
</div>
<div class="paragraph">
<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>
</div>
<div class="paragraph">
<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—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&#8217;t break existing clients by adding abstract methods.</p>
</div>
<div class="olist arabic">
<ol class="arabic" start="1">
<li>
<p>Open <code>RendererService</code>, in the Povray API project&#8217;s <code>org.netbeans.modules.examples.api.povray</code> package, in the code editor.</p>
</li>
</ol>
</div>
<div class="paragraph">
<p>1.
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>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-java hljs" data-lang="java">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 -&gt; weak references -&gt; 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 &amp;amp;&amp;amp; !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 &amp;amp;&amp;amp; 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));
}
}
}</code></pre>
</div>
</div>
<div class="paragraph">
<p>At this stage, the import statement block at the top of the above class should be as follows:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-java hljs" data-lang="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;</code></pre>
</div>
</div>
<div class="olist arabic">
<ol class="arabic" start="3">
<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>
</ol>
</div>
<div class="olist arabic">
<ol class="arabic" start="4">
<li>
<p>Now, we will need to implement a listener interface on <code>RendererServiceImpl</code>, so modify its signature as follows:</p>
</li>
</ol>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-java hljs" data-lang="java">final class RendererServiceImpl extends RendererService *implements FileChangeListener* {</code></pre>
</div>
</div>
<div class="paragraph">
<p>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>
</div>
<div class="olist arabic">
<ol class="arabic" start="5">
<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>
</li>
</ol>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-java hljs" data-lang="java">//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() &amp;amp;&amp;amp; "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
}</code></pre>
</div>
</div>
<div class="olist arabic">
<ol class="arabic" start="6">
<li>
<p>One last change we need to make is to the <code>render()</code> method in the <code>RenderServiceImpl</code> class—it is possible that the <code>images/</code> 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 <code>images/</code> folder, and if not, start now that it&#8217;s created. So, we need to modify the implementation of <code>render()</code> slightly:</p>
</li>
</ol>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-java hljs" data-lang="java">@Override
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;*
}</code></pre>
</div>
</div>
<div class="paragraph">
<p>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—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`s we listen to can outlive the `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>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_icon_badgingimplementing_icon_badging"><a class="anchor" href="#_icon_badgingimplementing_icon_badging"></a>Icon-Badging—Implementing Icon Badging</h2>
<div class="sectionbody">
<div class="paragraph">
<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—<code>ImageUtilities.mergeImages()</code>.</p>
</div>
<div class="olist arabic">
<ol class="arabic" start="1">
<li>
<p>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.</p>
</li>
</ol>
</div>
<div class="paragraph">
<p>1.
Add the highlighted code below, in the constructor for <code>PovrayDataNode</code>:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-java hljs" data-lang="java">public PovrayDataNode(PovrayDataObject obj) {
super(obj, Children.LEAF);
*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);
}*
}</code></pre>
</div>
</div>
<div class="olist arabic">
<ol class="arabic" start="3">
<li>
<p>The <code>stateChanged()</code> method can be implemented very simply:</p>
</li>
</ol>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-java hljs" data-lang="java">public void stateChanged(ChangeEvent changeEvent) {
*fireIconChange();*
}</code></pre>
</div>
</div>
<div class="olist arabic">
<ol class="arabic" start="4">
<li>
<p>Now we need to override <code>getIcon()</code> to return different icons depending on the state of the <code>Node</code>:</p>
</li>
</ol>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-java hljs" data-lang="java">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;
}</code></pre>
</div>
</div>
<div class="paragraph">
<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>
</div>
<div class="olist arabic">
<ol class="arabic" start="5">
<li>
<p>Create the necessary image files in the <code>org.netbeans.examples.modules.povfile</code> package—here are the ones used in the tutorial:</p>
<div class="ulist">
<ul>
<li>
<p><strong>hasImageBadge.png</strong>
<span class="image"><img src="../../_images/tutorials/povray_hasImageBadge.png" alt="povray hasImageBadge"></span></p>
</li>
<li>
<p><strong>hasNoImageBadge.png</strong>
<span class="image"><img src="../../_images/tutorials/povray_hasNoImageBadge.png" alt="povray hasNoImageBadge"></span></p>
</li>
<li>
<p><strong>needsRenderBadge.png</strong>
<span class="image"><img src="../../_images/tutorials/povray_needsRenderBadge.png" alt="povray needsRenderBadge"></span></p>
</li>
</ul>
</div>
</li>
</ol>
</div>
<div class="paragraph">
<p>1.
Run the application. Notice the icon badging, and the changes when you render or remove rendered images:</p>
</div>
<div class="imageblock">
<div class="content">
<img src="../../_images/tutorials/povray_71_ch8_pic2.png" alt="povray 71 ch8 pic2">
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_next_steps"><a class="anchor" href="#_next_steps"></a>Next Steps</h2>
<div class="sectionbody">
<div class="paragraph">
<p>We&#8217;re almost done. The <a href="../nbm-povray-9/" class="xref page">next step</a> will be adding project build support and putting some finishing touches on our UI and code.</p>
</div>
</div>
</div>
<section class='tools'>
<ul class="menu align-center">
<li><a title="Facebook" href="https://www.facebook.com/NetBeans"><i class="fa fa-md fa-facebook"></i></a></li>
<li><a title="Twitter" href="https://twitter.com/netbeans"><i class="fa fa-md fa-twitter"></i></a></li>
<li><a title="Github" href="https://github.com/apache/netbeans"><i class="fa fa-md fa-github"></i></a></li>
<li><a title="YouTube" href="https://www.youtube.com/user/netbeansvideos"><i class="fa fa-md fa-youtube"></i></a></li>
<li><a title="Atom Feed" href="https://netbeans.apache.org/blogs/atom"><i class="fa fa-mf fa-rss"></i></a></li>
<li><a title="Slack" href="https://tinyurl.com/netbeans-slack-signup/"><i class="fa fa-md fa-slack"></i></a></li>
<li><a title="Issues" href="https://github.com/apache/netbeans/issues"><i class="fa fa-mf fa-bug"></i></a></li>
</ul>
<ul class="menu align-center">
<li><a href="https://github.com/apache/netbeans-antora-tutorials/edit/main/modules/ROOT/pages/tutorials/nbm-povray-8.adoc" title="See this page in github"><i class="fa fa-md fa-edit"></i> See this page in GitHub.</a></li>
</ul>
</section>
</div>
<div class='grid-container incubator-area' style='margin-top: 64px'>
<div class='grid-x grid-padding-x'>
<div class='large-auto cell text-center'>
<a href="https://www.apache.org/">
<img style="height: 60px" title="Apache Software Foundation" src="../../../../_/images/asf_logo_wide.svg" />
</a>
</div>
<div class='large-auto cell text-center'>
<a href="https://www.apache.org/events/current-event.html">
<img style="width:234px; height: 60px;" title="Apache Software Foundation current event" src="https://www.apache.org/events/current-event-234x60.png"/>
</a>
</div>
</div>
</div>
<footer>
<div class="grid-container">
<div class="grid-x grid-padding-x">
<div class="large-auto cell">
<h1><a href="../../../../front/main/about">About</a></h1>
<ul>
<li><a href="../../../../front/main/community/who">Who's Who</a></li>
<li><a href="https://www.apache.org/foundation/thanks.html">Thanks</a></li>
<li><a href="https://www.apache.org/foundation/sponsorship.html">Sponsorship</a></li>
<li><a href="https://www.apache.org/security/">Security</a></li>
</ul>
</div>
<div class="large-auto cell">
<h1><a href="../../../../front/main/community">Community</a></h1>
<ul>
<li><a href="../../../../front/main/community/mailing-lists">Mailing lists</a></li>
<li><a href="../../../../front/main/community/committer">Becoming a committer</a></li>
<li><a href="../../../../front/main/community/events">NetBeans Events</a></li>
<li><a href="https://www.apache.org/events/current-event.html">Apache Events</a></li>
</ul>
</div>
<div class="large-auto cell">
<h1><a href="../../../../front/main/participate">Participate</a></h1>
<ul>
<li><a href="../../../../front/main/participate/submit-pr">Submitting Pull Requests</a></li>
<li><a href="../../../../front/main/participate/report-issue">Reporting Issues</a></li>
<li><a href="../../../../front/main/participate/#documentation">Improving the documentation</a></li>
</ul>
</div>
<div class="large-auto cell">
<h1><a href="../../../../front/main/help">Get Help</a></h1>
<ul>
<li><a href="../../../../front/main/help/#documentation">Documentation</a></li>
<li><a href="../../../../wiki/main/wiki">Wiki</a></li>
<li><a href="../../../../front/main/help/#support">Community Support</a></li>
<li><a href="../../../../front/main/help/commercial-support">Commercial Support</a></li>
</ul>
</div>
<div class="large-auto cell">
<h1><a href="../../../../front/main/download">Download</a></h1>
<ul>
<li><a href="../../../../front/main/download">Releases</a></li>
<li><a href="https://plugins.netbeans.apache.org/">Plugins</a></li>
<li><a href="../../../../front/main/download/#_daily_builds_and_building_from_source">Building from source</a></li>
<li><a href="../../../../front/main/download/#_older_releases">Previous releases</a></li>
</ul>
</div>
</div>
</div>
</footer>
<div class='footer-disclaimer'>
<div class="footer-disclaimer-content">
<p>Copyright &copy; 2017-2025 <a href="https://www.apache.org">The Apache Software Foundation</a>.</p>
<p>Licensed under the Apache <a href="https://www.apache.org/licenses/">license</a>, version 2.0</p>
<div style='max-width: 40em; margin: 0 auto'>
<p>Apache, Apache NetBeans, NetBeans, the Apache feather logo and the Apache NetBeans logo are trademarks of <a href="https://www.apache.org">The Apache Software Foundation</a>.</p>
<p>Oracle and Java are registered trademarks of Oracle and/or its affiliates.</p>
<p>The Apache NetBeans website conforms to the <a href="https://privacy.apache.org/policies/privacy-policy-public.html">Apache Software Foundation Privacy Policy</a></p>
</div>
</div>
</div>
<script src="../../../../_/js/vendor/lunr.js"></script>
<script src="../../../../_/js/search-ui.js" id="search-ui-script" data-site-root-path="../../../.." data-snippet-length="100" data-stylesheet="../../../../_/css/search.css"></script>
<script async src="../../../../search-index.js"></script>
<script src="../../../../_/js/vendor/jquery.min.js"></script>
<script src="../../../../_/js/vendor/what-input.min.js"></script>
<script src="../../../../_/js/vendor/foundation.min.js"></script>
<script src="../../../../_/js/vendor/jquery.colorbox-min.js"></script>
<script src="../../../../_/js/netbeans.js"></script>
<script>
$(function(){ $(document).foundation(); });
</script>
<script src="../../../../_/js/vendor/highlight.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', (event) => {
document.querySelectorAll('pre code').forEach((el) => {
hljs.highlightElement(el);
});
});
</script>
</body>
</html>