Adding a file node type and a viewer

This tutorial creates OpenTools that create a new file node type and a viewer that can display the new node type. In this case, the new node type is a batch file node. The new batch node viewer is a text viewer that displays the contents of a batch file on a bright yellow background. The viewer has a Batch tab at the bottom of the content pane.

Note that the a giant button replaces the structure pane. When the user clicks the button, a comment line is added to both the top and bottom of the batch file and the background color changes to cyan.

This tutorial also adds a menu item to context menus. A View in Notepad menu option appears in the project pane's context menu and in the content pane's context menu when the Source tab is selected.

This tutorial teaches these primary skills:

You can find the source code for this OpenTool sample in the samples/OpenToolsAPI/NodeDemo directory.


Getting started

To begin creating the OpenTool,

  1. Create a new project called NodeDemo.jpx.
  2. Add the OpenTools SDK library as a required library to your project.

If you need help performing these tasks, see JBuilder OpenTools basics.

This tutorial requires you to create four classes:


Creating the BatchFileNode class

Use the Class wizard to begin the new class:

  1. Choose File|New Class to display the Class wizard.
  2. Enter the name of the class as BatchFileNode.
  3. For the base class, specify com.borland.primetime.node.TextFileNode.
  4. Verify that the Public and Generate Default Constructor options are checked, unchecking all other options.
  5. Choose OK.

Your code in the editor should look like this:

package NodeDemo;

import com.borland.primetime.node.TextFileNode;

public class BatchFileNode extends TextFileNode {

  public BatchFileNode() {
  }
}

Because the batch file node displays text, it extends the com.borland.node.TextFileNode, which in turn extends com.borland.node.FileNode.

Adding import statements

Add a few import statements to your class. Modify the import section of your class so that it resembles this:
import javax.swing.*;

import com.borland.primetime.node.*;
import com.borland.primetime.vfs.*;

Modifying the constructor

You added a default constructor to the BatchFileNode class, but all subclasses of FileNode must have a constructor that takes three parameters: the name of the project, the parent node, and the storage available to this node. Modify BatchFileNode() so it looks like this:
  public BatchFileNode(Project project, Node parent, Url url)
      throws DuplicateNodeException {
    super(project, parent, url);
  }

The method calls the constructor of the parent class (TextFileNode) and throws an exception if a batch file node of the same type is already registered.

Adding an icon

The new file node requires an icon to display in the project pane, so define one by adding this code to the class:
public static final Icon ICON = new 
   ImageIcon(BatchFileNode.class.getResource("Batch.gif"));

To provide a way to display the icon, add this method:

public Icon getDisplayIcon() {
  return ICON;
}

Add a Batch.gif file to your project. You can find one in the samples/OpentoolsAPI/NodeDemo directory.

Registering the batch file node type

Each OpenTool must have an initOpenTool() method that is called as JBuilder is loading. The initOpenTool() method of BatchFileNode registers the new file node type with JBuilder. Add this method to the class:
public static void initOpenTool(byte major, byte minor) {
    FileNode.registerFileNodeClass("bat", "Batch file", BatchFileNode.class, ICON);
}

After JBuilder loads this new OpenTool and the user adds a file with a .bat extension to a project, the new file node is represented with the icon in the project pane. At this point, there is no viewer associated with the new node type.

For more information about creating a new node type, see Registering a new file node type and other topics in JBuilder content manager concepts.


Creating the BatchViewerFactory class

Each node viewer is created by a node viewer factory, which decides whether it is able to create a viewer to display a particular node type. If the node viewer factory thinks it can create the node viewer, it attempts to do so. You need a new node viewer factory that can create a viewer for displaying batch files. For more information about node viewers and the node viewer factories that create them, see JBuilder content manager concepts.

Use the Class wizard to begin the new class:

  1. Choose File|New Class to display the Class wizard.
  2. Enter the name of the class as BatchViewerFactory.
  3. For the base class, choose java.lang.Object.
  4. Verify that the Public option is checked, unchecking all other options.
  5. Choose OK.

Use the Implement Interface wizard to have the class implement the com.borland.primetime.ide.NodeViewerFactory interface:

  1. Choose Wizards|Implement Interface.
  2. Navigate to the com.borland.primetime.ide.NodeViewerFactory interface.
  3. Choose OK.

Your code in the editor should look like this:

package NodeDemo;

import com.borland.primetime.ide.NodeViewer;
import com.borland.primetime.ide.Context;
import com.borland.primetime.node.Node;
import com.borland.primetime.ide.NodeViewerFactory;

public class BatchViewerFactory implements NodeViewerFactory {
  public NodeViewer createNodeViewer(Context parm1) {
    /**@todo: Implement this com.borland.primetime.ide.NodeViewerFactory method*/
    throw new java.lang.UnsupportedOperationException("Method createNodeViewer() 
	   not yet implemented.");
  }
  public boolean canDisplayNode(Node parm1) {
    /**@todo: Implement this com.borland.primetime.ide.NodeViewerFactory method*/
    throw new java.lang.UnsupportedOperationException("Method canDisplayNode() 
	   not yet implemented.");
  }
}

Modifying the import statements

Replace the import statements in your class with these statements so that JBuilder finds all the classes and interfaces it needs:
import com.borland.primetime.ide.*;
import com.borland.primetime.node.*;

Examining the file node type

The NodeViewerFactory interface has just two methods you must implement: canDisplayNode() and createNodeViewer(). Start with canDisplayNode() first.

The JBuilder IDE must quickly poll the registered node viewer factories to determine whether a factory exists that can create the right type of viewer for the selected file node. It does this by calling the canDisplayNode() method of the registered node viewer factories.

The canDisplayNode() method simply determines whether the file node passed to it is the right type of file node; in this case, whether it is an instance of a BatchFileNode. Modify the canDisplayNode() method in your code so that it looks like this:

public boolean canDisplayNode(Node node) {
  return node instanceof BatchFileNode;
}

canDisplayNode() returns true if the node in question is an instance of BatchFileNode.

Creating a node viewer

The createNodeViewer() method attempts to create a viewer to display the node. In this case, it tries to create a BatchViewer instance, which is a viewer you'll create later for displaying batch files in the content pane. Modify the createNodeViewer() method in your code so that it looks like this:
public NodeViewer createNodeViewer(Context context) {
  if (context.getNode() instanceof BatchFileNode)
    return new BatchViewer(context);
  return null;
}

The context parameter specifies a particular browser/node pair.

Registering a node viewer factory

Each OpenTool must have an initOpenTool() method that is called as JBuilder is loading. The initOpenTool() method of BatchViewerFactory registers the new file node type with JBuilder. Add this method to the class:
public static void initOpenTool(byte major, byte minor) {
  Browser.registerNodeViewerFactory(new BatchViewerFactory());
}


Creating the BatchViewer class

Now you need a viewer class that your new BatchViewerFactory can create. The BatchViewer class you are going to create displays the contents of the a batch file in the content pane. It has its own Batch tab the user can use to switch to this view if it's not the one currently displayed. BatchViewer also replaces the usual hierarchy in the structure pane with a button the user can use to modify the contents of the buffer the viewer is presenting.

Use the Class wizard to begin the new class:

  1. Choose File|New Class to display the Class wizard.
  2. Enter the name of the class as BatchViewer.
  3. For the base class, choose com.borland.primetime.viewer.AbstractBufferNodeViewer.
  4. Verify that the Public, Generate Default Constructor, and Override Abstract Methods options are checked, unchecking all other options.
  5. Choose OK.

Use the Implement Interface wizard to have the class implement the java.awt.event.ActionListener interface:

  1. Choose Wizards|Implement Interface.
  2. Navigate to the java.awt.event.ActionListener interface.
  3. Choose OK.

Your code should look like this:

package NodeDemo;

import com.borland.primetime.viewer.*;
import com.borland.primetime.vfs.Buffer;
import javax.swing.JComponent;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;


public class BatchViewer extends AbstractBufferNodeViewer implements ActionListener {

  public BatchViewer() {
  }
  public String getViewerTitle() {
    /**@todo: implement this com.borland.primetime.viewer.AbstractNodeViewer 
	   abstract method*/
  }
  public byte[] getBufferContent(Buffer parm1) {
    /**@todo: implement this com.borland.primetime.viewer.AbstractBufferNodeViewer  
	   abstract method*/
  }
  public JComponent createStructureComponent() {
    /**@todo: implement this com.borland.primetime.viewer.AbstractNodeViewer 
	   abstract method*/
  }
  protected void setBufferContent(byte[] parm1) {
    /**@todo: implement this com.borland.primetime.viewer.AbstractBufferNodeViewer 
	   abstract method*/
  }
  public JComponent createViewerComponent() {
    /**@todo: implement this com.borland.primetime.viewer.AbstractNodeViewer 
	   abstract method*/
  }
  public void actionPerformed(ActionEvent e) {
    /**@todo: Implement this java.awt.event.ActionListener method*/
       throw new java.lang.UnsupportedOperationException("Method actionPerformed() 
	   not yet implemented.");
  }
}
Most node viewers refresh each time a buffer change takes place, even if the viewer isn't visible or it doesn't have the focus. BatchViewer, however, extends AbstractBufferNodeViewer, which is a node viewer that can cache buffer changes and update the view only when necessary.

Modifying the import statements

Modify your import statement section at the top of your file so it contains these statements:
import javax.swing.*;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.UnsupportedEncodingException;

import com.borland.primetime.vfs.*;
import com.borland.primetime.ide.Context;
import com.borland.primetime.viewer.AbstractBufferNodeViewer;

Modifying the constructor

Modify the default constructor so that a context parameter that identifies a unique browser/node pair is sent to it and the parent class is informed that the viewer buffer should update only when it is visible:
public BatchViewer(Context context) {
  super(context, UPDATE_WHEN_VISIBLE);
}

UPDATE_WHEN_VISIBLE is a class constant defined in AbstractBufferNodeViewer.

Creating the viewer component

Modify the createViewerComponent() method in your class so that it creates an instance of JTextArea, makes the text area read only, and sets its background color to yellow. First define a JTextArea field in the class:
private JTextArea area;

Then implement the createViewerComponent() method:

public JComponent createViewerComponent() 
  area = new JTextArea();
  area.setEditable(false);
  area.setBackground(Color.yellow);
  return area;
}

Also implement the getViewerTitle() method so that it returns the string "Batch". This string is used as the label of the tab for the viewer:

public String getViewerTitle() {
  return "Batch";
}

Adding a structure pane component

Usually the structure pane displays information relating to the node being viewed, often in a hierarchical structure. Instead, you can place any component in that space. In this case, you add a JButton the user can click to modify the buffer displayed in the viewer.

First add a new field to the class for the button:

private JButton button;
Then implement the createStructureComponent() method with this code:
public JComponent createStructureComponent() {
  button = new JButton("Press me to modify the Batch File");
  button.addActionListener(this);
  return button;
}

As the new button instance is created, it's passed the "Press me to modify the Batch File" string. When the button is "pressed," an actionPerformed() method is called. You must implement that method in the BatchViewer class. Modify actionPerformed() so that it looks like this:

  public void actionPerformed(ActionEvent e) {

    area.setText("REM -- Comment at top --\n" + area.getText() +
                 "REM -- Comment at bottom\n");

    try {
      setBufferModified();
    }
    catch (ReadOnlyException ex) {
      setBufferContent();
    }
  }

Working with the buffer

The actionPerformed() method adds a comment to the top and bottom of the text in the viewer component. Then setBufferModified() is called, which the Virtual File System (VFS) takes care of. setBufferModified() throws a ReadOnlyException, however, so you must handle it. Remember you specified the viewer as read only. In this case, the catch clause calls setBufferContent(), a method of the AbstractBufferNodeViewer class.

Modify the setBufferContent() method with this code, making sure you change the parm1 parmeter name to content:

protected void setBufferContent(byte[] content) {
  try {
    area.setText(new String(content, getEncoding()));
  }
  catch (UnsupportedEncodingException ex) {
  }
}
The AbstractBufferNodeViewer, the parent class of your BatchViewer class, decides when to call the setBufferContent() method. This allows AbstractBufferNodeViewer to save up changes to the buffer when the viewer isn't active or visible. When setBufferContent() is called, the viewer is refreshed if the buffer has changed. So the setBufferContent() you implemented attempts to modify the viewer contents by setting the new text. It also calls the getEncoding() method to discover the appropriate encoding to use when converting any binary data.

Responding to buffer changes

The AbstractBufferNodeViewer implements the BufferListener interface, which contains a bufferStateChanged() method that is called by the Buffer object when the buffer changes. You must implement bufferStateChanged(). Add a bufferStateChanged() method to your class that changes the background color to cyan, and, if the buffer state is ready only, changes the state so it can edited. Here is the code to do that:
public void bufferStateChanged(Buffer buffer, int oldState, int newState) {
  area.setBackground(
      (newState & Buffer.STATE_MODIFIED) == Buffer.STATE_MODIFIED ?
      Color.cyan : Color.yellow);
  button.setEnabled(
      (newState & Buffer.STATE_READONLY) != Buffer.STATE_READONLY);
}

Returning the buffer content

The AbstractBufferNodeViewer also implements the BufferUpdater interface, which defines a getBufferContent() method. Your derived class must supply an implementation of this method. Here getBufferContent() returns the current content of the buffer as an array of bytes:
public byte[] getBufferContent(Buffer buffer) {
  return area.getText().getBytes();
}


Adding menu items to a context menu

When the user right-clicks a batch file node in the project pane, the popup (or context) menu that appears contains a View in Notepad menu command the user can choose to display the batch file in the notepad.exe utility. The same menu command appears on the popup menu that appears when the user right-clicks a batch file displayed in the Source pane. Although the menu commands look the same, how they are implemented differs. The BatchMenu class shows you how to use both methods.

To begin the BatchMenu class,

  1. Choose File|New Class to display the Class wizard.
  2. Enter the name of the class as BatchMenu.
  3. For the base class, specify java.lang.Object.
  4. Verify that the Public option is checked, unchecking all other options.
  5. Choose OK.

BatchMenu implements the ContextActionProvider interface that defines just one method, getContextAction().

Use the Implement Interface wizard to have the class implement the com.borland.primetime.ide.ContextActionProvider interface:

  1. Choose Wizards|Implement Interface.
  2. Navigate to the com.borland.primetime.ide.ContextActionProvider interface.
  3. Choose OK.

The wizard adds the getContextAction() method to your code. Your code should look like this:

package NodeDemo;

import javax.swing.Action;
import com.borland.primetime.ide.Browser;
import com.borland.primetime.node.Node;
import com.borland.primetime.ide.ContextActionProvider;

public class BatchMenu implements ContextActionProvider {

  public Action getContextAction(Browser parm1, Node[] parm2) {
    /**@todo: Implement this com.borland.primetime.ide.ContextActionProvider method*/
    throw new java.lang.UnsupportedOperationException("Method getContextAction() 
	   not yet implemented.");
  }
}

Modifying the import statements

The BatchMenu class requires several import statements. Modify the import section of your class so that it looks like this:
import javax.swing.*;
import java.awt.event.*;

import com.borland.primetime.ide.*;
import com.borland.primetime.node.Node;
import com.borland.primetime.editor.*;
import com.borland.primetime.viewer.*;
import com.borland.primetime.actions.*;

Providing an action

The getContextAction() method specifies an action that occurs within a particular context; in this case, when the user right-clicks a batch file node in the project pane.

Modify getContextAction() so that if the currently selected node is a single batch file node, an action that opens the file in the notepad.exe utility occurs. (Be sure you change the name of the parameters passed to getContextAction() to browser and nodes.) Here is the code:

public Action getContextAction(Browser browser, Node[] nodes) {
  if (nodes.length == 1 && nodes[0] instanceof BatchFileNode)
    return ACTION_VIEW_NOTEPAD;
  return null;
}

You must now define the ACTION_VIEW_NOTEPAD action that getContextAction() returns. Here is the action code in its entirety:

public static final Action ACTION_VIEW_NOTEPAD = new BrowserAction( "View 
    in Notepad") {
  public void actionPerformed(Browser browser) {
    Node node = browser.getProjectView().getSelectedNode();
    if (node instanceof BatchFileNode) {
      BatchFileNode batchNode = (BatchFileNode)node;
      try {
        String path = batchNode.getUrl().getFileObject().getAbsolutePath();
        Runtime.getRuntime().exec("notepad " + path);
      }
      catch (Exception ex) {
      }
    }
  }
};

The action created is a BrowserAction given the text string "View in Notepad" that appears on the context menu. The current Browser instance is passed to the actionPerformed() method, which begins by obtaining the selected node in the project pane and then determing whether that node is a batch file node type. If it is, the method calls notepad.exe, giving it to open the fully-qualified name of the batch file the node represents.

Writing the initOpenTool() method

In the initOpenTool() method you must create to register the new action with JBuilder, you must declare a menu field of type BatchMenu, and then register it as a context action provider for the project pane. Here is the code to do that:
public static void initOpenTool(byte major, byte minor) {
  BatchMenu menu = new BatchMenu();

  ProjectView.registerContextActionProvider(menu);
}

Your BatchMenu class now adds a menu item to the context menu of the project pane when the user selects a batch file node in the project pane.

Doing it another way

You still need to add the code that adds a menu item to the context menu of the editor when a batch file is open. Instead of implementing the EditorContextActionProvider interface in the BatchMenu class and then registering the whole class as the provider, you could choose instead to create and register a local class as the provider.

Begin by starting the definition of a ViewNotepad class, which implements the EditorContextActionProvider interface:

static EditorContextActionProvider ViewNotepad = new EditorContextActionProvider() {
};

Classes that implement EditorContextActionProvider must also implement a getContextAction() method, but this one is passed an instance of an EditorPane that appears in the Source pane when the batch file is opened in the code editor. Below is the code for getContextAction(); place it in the ViewNotepad class definition.

public Action getContextAction(EditorPane editor) {
  Node node = Browser.getActiveBrowser().getActiveNode();
  if (node instanceof BatchFileNode)
    return GROUP_ViewNotePad;
  return null;
}

getContextAction() obtains the selected node, and if the node is an instance of a batch file node, it returns the ActionGroup GROUP_ViewNotePad. By specifying a separate ActionGroup for the menu command, it will appear in its own group, separate from the other menu items on the editor context menu.

An implementation of EditorContextActionProvider must also include a getPriority() method:

public int getPriority() {
  return 4;
}

The priority of a menu item determines where the item appears on a menu. Possible priorities range from 1 - 100. A priority less than 5 usually results in the menu item appearing at the bottom of the menu, while a priority of 99 usually makes it appear at the top. Priorities are shared with other menu entries, so no priority value guarantees a specific position on the menu.

For clarity, the entire ViewNotepad implementation is presented here:

static EditorContextActionProvider ViewNotepad = new EditorContextActionProvider() {

  public Action getContextAction(EditorPane editor) {
 
    Node node = Browser.getActiveBrowser().getActiveNode();
    if (node instanceof BatchFileNode)
      return GROUP_ViewNotePad;
    return null;
  }

  public int getPriority() {
    return 4;
  }
};

You have yet to define the GROUP_ViewNotePad that getContextAction() returns. The group contains just one action. Define the ActionGroup like this in your class:

protected static final ActionGroup GROUP_ViewNotePad = new ActionGroup();
static {
  GROUP_ViewNotePad.add(ACTION_EDITOR_VIEW_NOTEPAD);
}

Finally, define the action that is called when the user-right clicks the batch file in the editor; place it above the GROUP_ViewNotePad definition you just added in the class:

public static final AbstractAction ACTION_EDITOR_VIEW_NOTEPAD =
    new UpdateAction("View in Notepad",
                     'v',
                     "View in Notepad") {

      public void update(Object source) {
        Node node = Browser.getActiveBrowser().getActiveNode();
        setEnabled(node instanceof BatchFileNode);
      }

      public void actionPerformed(ActionEvent e) {
        Node node = Browser.getActiveBrowser().getActiveNode();
        if (node instanceof BatchFileNode) {
          // Show the batch file in Notepad.
          BatchFileNode batchNode = (BatchFileNode)node;
          try {
            String path = batchNode.getUrl().getFileObject().getAbsolutePath();
            Runtime.getRuntime().exec("notepad " + path);
          }
          catch (Exception ex) {
          }
        }
      }
    };

The ACTION_EDITOR_VIEW_NOTEPAD action is defined as an UpdateAction that contains two methods: update() and actionPerformed().

The update() method determines whether the menu item text "View in Notepad" appears on the menu. The method obtains the active node and enables the menu item if the node is a BatchFileNode instance.

The actionPerformed() method also gets the currently selected file node and determines whether it's a BatchFileNode instance. If it is, actionPerformed() passes the fully-qualified name of the file to the notepad.exe utility to open.

Registering the ViewNotepad class as a ContextActionProvider

The final step is to register your new ViewNotepad class as a ContextActionProvider with the EditorManager. Just as before, you do this in the initOpenTool() method. Once you've added the necessary code to initOpenTool(), the initOpenTool() code should look like this, with the new code shown in bold:
  public static void initOpenTool(byte major, byte minor) {
    BatchMenu menu = new BatchMenu();

    ProjectView.registerContextActionProvider(menu);

    EditorManager.registerContextActionProvider(ViewNotepad);
  }

Finishing up

To finish your OpenTools, follow these steps:

  1. Choose Project|Make Project to compile the classes and fix any syntax errors that might have crept in.

  2. Create a manifest file for your project that contains this text:
    OpenTools-Core: NodeDemo.BatchFileNode
    OpenTools-UI: NodeDemo.BatchViewerFactory
      NodeDemo.BatchMenu
    

  3. Save the manifest file with the name NodeDemo\classes.opentools.

  4. Exit JBuilder and edit JBuilder's launch script in JBuilder's bin directory. Add NodeDemo\classes to the Java classpath.

  5. Start up JBuilder, open a batch file in your current project, and look for the new batch node and its viewer, and for the menu items on the project and editor context menus.

For more detailed information on these final steps, see OpenTools basics.