FeaturesPluginsDocs & SupportCommunityPartners

Writing POV-Ray Support for NetBeans IX - Build Support

Tim Boudreau
10 June 2006

Feedback

This is a DRAFT!

This is a continuation of the tutorial for building POV-Ray support for NetBeans. If you have not read the first or second, third, fourth fifth sixth, seventh, and eighth parts of this tutorial, you may want to start there.

Project Action Support

You may recall that early on, when we were writing PovrayProject, we stubbed out the implementation of PovrayProject.ActionsProviderImpl - so in fact the build, clean and run actions are all disabled when a POV-Ray project is active. We should quickly implement them:
  1. Open PovrayProject in the code editor, and implement the methods of ActionProviderImpl as follows:
            public String[] getSupportedActions() {
                return new String[] { ActionProvider.COMMAND_BUILD,
                    ActionProvider.COMMAND_CLEAN, ActionProvider.COMMAND_COMPILE_SINGLE };
            }
    
            public void invokeAction(String string, Lookup lookup) throws IllegalArgumentException {
                int idx = Arrays.asList (getSupportedActions()).indexOf (string);
                switch (idx) {
                    case 0 : //build
                        final RendererService ren = (RendererService) getLookup().lookup(RendererService.class);
                        RequestProcessor.getDefault().post (new Runnable() {
                            public void run() {
                                FileObject image = ren.render();
    
                                //If we succeeded, try to open the image
                                if (image != null) {
                                    DataObject dob;
                                    try {
                                        dob = DataObject.find(image);
                                        OpenCookie open = (OpenCookie)
                                            dob.getNodeDelegate().getLookup().lookup(
                                            OpenCookie.class);
                                        if (open != null) {
                                            open.open();
                                        }
                                    } catch (DataObjectNotFoundException ex) {
                                        ErrorManager.getDefault().notify(ex);
                                    }
                                }
                            }
                        });
                        break;
                    case 1 : //clean
                        FileObject fob = getImagesFolder(false);
                        if (fob != null) {
                            DataFolder fld = DataFolder.findFolder(fob);
                            for (Enumeration en=fld.children(); en.hasMoreElements();) {
                                DataObject ob = (DataObject) en.nextElement();
                                try {
                                    ob.delete();
                                } catch (IOException ioe) {
                                    ErrorManager.getDefault().notify(ioe);
                                }
                            }
                        }
                        break;
                    case 2 : //compile-single
                        final DataObject ob = (DataObject) lookup.lookup (DataObject.class);
                        if (ob != null) {
                            final RendererService ren1 = (RendererService) getLookup().lookup(RendererService.class);
                            RequestProcessor.getDefault().post (new Runnable() {
                                public void run() {
                                    if (ob.isValid()) { //Could theoretically change before we run
                                        ren1.render(ob.getPrimaryFile());
                                    }
                                }
                            });
                        }
                        break;
                    default :
                        throw new IllegalArgumentException (string);
                }
            }
    
            public boolean isActionEnabled(String string, Lookup lookup) throws IllegalArgumentException {
                int idx = Arrays.asList (getSupportedActions()).indexOf (string);
                boolean result;
                switch (idx) {
                    case 0 : //build
                        result = true;
                        break;
                    case 1 : //clean
                        result = getImagesFolder(false) != null &&
                                getImagesFolder(false).getChildren().length > 0;
                        break;
                    case 2 : //compile-single
                        DataObject ob = (DataObject) lookup.lookup (DataObject.class);
                        if (ob != null) {
                            FileObject file = ob.getPrimaryFile();
                            result = "text/x-povray".equals(file.getMIMEType());
                        } else {
                            result = false;
                        }
                        break;
                    default :
                        result = false;
                }
                return result;
            }

Ensuring There Is A Main File

We now have handling implemented for all of the standard project actions that make sense for a POV-Ray project. Now, if you've been following very carefully, you may have noticed that there is one bug we need to fix: isActionEnabled() will always return true for build. But we implemented the following code in Povray.getFileToRender():
            render = provider.getMainFile();
            if (render == null) {
                ProjectInformation info = (ProjectInformation)
                        proj.getLookup().lookup(ProjectInformation.class);

                throw new IOException (NbBundle.getMessage(Povray.class,
                        "MSG_NoMainFile", info.getDisplayName()));
            }
    
So if there is no main file set for a project, the build action will be enabled, but if it is invoked, it will throw an exception! The simple choice would be to test if there is a main file, and if not, disable the build action - but this would be rather non-intuitive to the user who might not be able to figure out what is wrong with his or her project. And we would lose an opportunity to explore the Explorer and Dialogs APIs.

So instead, we will post a dialog which will allow the user to choose which file should be the main file, if none is set when build is called:

  1. First we will set up two more dependencies. Right click the Povray Projects project and select Properties.

  2. On the Libraries page of the Project Properties dialog, click the Add button. Search for the class DialogDisplayer to add a dependency on the Dialogs API.

  3. Click the Add button again and search for the class ExplorerManager to add a dependency on the Explorer and Property Sheet API.

  4. Right click the org.netbeans.examples.modules.povproject package and chose New > JPanel Form. Name it "MainFileChooser" The GUI Designer (Matisse) will open.

  5. In the Palette, click the item for JLabel and add a JLabel to the top of the form.

  6. Drag the right-hand edge of the JLabel to the right edge of the panel

  7. In the Palette, click the item for JScrollPane and add a JScrollPane to the form below the JLabel.

  8. Drag the bottom right corner of the JScrollPane down and to the right until the bottom and right edge alignment guidelines appear. The result should look like this:

  9. Select the JLabel and click the [...] button for its text property in the Property Sheet. A custom editor will open:

  10. Click the Format button. We will change the code that will be generated from the default, which uses ResourceBundle, to the more NetBeans-friendly NbBundle pattern - change the text format to NbBundle.getMessage(getClass(), "{key}"):

  11. Enter LBL_ChooseMainFile for the key and "Select Main File" for the value, as shown in the screen shot above.

  12. Select the JScrollPane. Click the Code button at the top of the Property Sheet to display code generation properties.

  13. Edit the "Custom Creation Code" property, changing its value to new BeanTreeView();:

  14. Edit the signature of the class so that it implements the interface ExplorerManager.Provider:
    public class MainFileChooser extends javax.swing.JPanel implements ExplorerManager.Provider {
            
  15. Click the Source button to switch to the code editor, then press Alt-Shift-F (Ctrl-Shift-F on Macintosh) to import NbBundle, ExplorerManager.Provider and BeanTreeView.

  16. Add the following code to implement ExplorerManager.Provider:
    private final ExplorerManager mgr = new ExplorerManager();
    public ExplorerManager getExplorerManager() {
        return mgr;
    }
  17. Modify the constructor so it reads as follows:
        public MainFileChooser(PovrayProject proj) {
            initComponents();
            LogicalViewProvider logicalView = (LogicalViewProvider)
                proj.getLookup().lookup(LogicalViewProvider.class);
    
            Node projectNode = logicalView.createLogicalView();
            mgr.setRootContext (new FilterNode (projectNode,
                    new ProjectFilterChildren (projectNode)));
    
            BeanTreeView btv = (BeanTreeView) jScrollPane1;
            btv.setPopupAllowed(false);
            btv.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
            //BeanTreeView shows no border by default:
            btv.setBorder (new LineBorder(UIManager.getColor("controlShadow")));
        }
    The BeanTreeView we are showing is a UI class from the Explorer API - in fact, it is the very same component that you see in the Projects, Files, Runtime and Favorites tabs in the NetBeans IDE.

    What it will do is, when it is added to a component, search through the hierarchy of parent components until it finds one that implements ExplorerManager.Provider. That component's ExplorerManager will then become the place where the tree view gets its root node to display, and will be what it notifies when the selection changes.

  18. Run Fix Imports to import LogicalViewProvider. It will not find ProjectFilterNode because we have not yet written it.

  19. Now we need to implement ProjectFilterNode. The Nodes API contains a class, FilterNode, which makes it possible to take one Node, and create another Node which "filters" the original Node - providing different children, actions, properties or whatever it chooses to.

    In our case, we want a FilterNode that will filter out any files that do not have the MIME type text/x-povray - so that, if the user has a text file or an image file or such in their project, they cannot set that to be the main file and try to pass it to POV-Ray.

    We don't actually need to implement FilterNode, we simply need to provide an alternate Children object which filters out files we don't want. Implement this as a nested class inside MainFileChooser:

    private static final class ProjectFilterChildren extends FilterNode.Children {
        ProjectFilterChildren (Node projectNode) {
            super (projectNode);
        }
    
        protected Node[] createNodes(Object object) {
            Node origChild = (Node) object;
            DataObject dob = (DataObject)
                origChild.getLookup().lookup (DataObject.class);
    
            if (dob != null) {
                FileObject fob = dob.getPrimaryFile();
                if ("text/x-povray".equals(fob.getMIMEType())) {
                    return super.createNodes (object);
                } else if (dob instanceof DataFolder) {
                    //Allow child folders of the scenes/ dir
                    return new Node[] {
                        new FilterNode (origChild,
                                new ProjectFilterChildren(origChild))
                    };
                }
            }
            //Don't create any nodes for non-povray files
            return new Node[0];
        }
    }
  20. Now we just need some code to use this panel. That code will go in RenderServiceImpl, before we call Povray.render(). Reimplement the no-argument version of the render() method as follows:
        public FileObject render() {
            MainFileProvider mfp = (MainFileProvider) proj.getLookup().lookup(
                    MainFileProvider.class);
            assert mfp != null;
            if (mfp.getMainFile() == null) {
                showChooseMainFileDlg (mfp);
            }
            if (mfp.getMainFile() != null) {
                return render (null);
            } else {
                return null;
            }
        }
  21. Now we need to implement the method we are calling, showChooseMainFileDlg(). This is the method which will ask the user to pick a main file. It will use the Dialogs API to show a dialog containing an instance of MainFileChooser, and enable the OK button once a file is selected. If the user selects a POV-Ray file, it will be stored in MainFileProvider, and so it will be non-null when we return to the render() method, and so render() will proceed:
    
        private void showChooseMainFileDlg (final MainFileProvider mfp) {
            final MainFileChooser chooser = new MainFileChooser (proj);
            String title = NbBundle.getMessage(RendererServiceImpl.class,
                    "TTL_ChooseMainFile");
    
            //Create a simple dialog descriptor describing what kind of dialog
            //we want and its title and contents
            final DialogDescriptor desc = new DialogDescriptor (chooser, title);
    
            //The OK button should be disabled initially
            desc.setValid(false);
    
            //Create a property change listener.  It will listen on the selection
            //in our MainFileChooser, and enable the OK button if an appropriate
            //node is selected:
            PropertyChangeListener pcl = new PropertyChangeListener() {
                public void propertyChange (PropertyChangeEvent pce) {
    
                    String propName = pce.getPropertyName();
    
                    if (ExplorerManager.PROP_SELECTED_NODES.equals(propName)) {
                        Node[] n = (Node[]) pce.getNewValue();
    
                        boolean valid = n.length == 1;
    
                        if (valid) {
                            DataObject ob = (DataObject)
                                n[0].getLookup().lookup(DataObject.class);
    
                            valid = ob != null;
    
                            if (valid) {
                                FileObject selectedFile = ob.getPrimaryFile();
                                String mimeType = selectedFile.getMIMEType();
                                valid = "text/x-povray".equals(mimeType);
                            }
                        }
                        desc.setValid(valid);
                    }
                }
            };
            chooser.getExplorerManager().addPropertyChangeListener(pcl);
    
            //Show the dialog - dialogResult will be OK or Cancel
            Object dialogResult = DialogDisplayer.getDefault().notify(desc);
    
            //If the user clicked OK, try to set the main file
            //from the selection 
            if (DialogDescriptor.OK_OPTION.equals(dialogResult)) {
    
                //Get the selected Node
                Node[] n = chooser.getExplorerManager().getSelectedNodes();
    
                //If it's > 1, explorer is broken - we set
                //single selection mode
                assert n.length <= 1;
                DataObject ob = (DataObject) n[0].getLookup().lookup (
                    DataObject.class);
    
                //Get the file from the data object
                FileObject selectedFile = ob.getPrimaryFile();
    
                //And save it as the main file
                mfp.setMainFile(selectedFile);
            }
        }
  22. Lastly we need to make sure that the resource bundle in org.netbeans.examples.modules.povproject contains all of the needed strings - add:
    TTL_ChooseMainFile=No Main File Set in Project
    LBL_ChooseMainFile=Select Main File
            

Companion
Projects:
MySQL Database Server   Open JDK: an Open SourceJDK   GlassFish Community: an Open Source Application Server    Mobile & Embedded Community    Open Solaris   java.net - The Source for Java Technology Collaboration   Virtual Box - full virtualizer  Open ESB - The Open Enterprise Service Bus Powered by