Adding an editor statistics OpenTool to the Tools menu

This tutorial creates an OpenTool that adds a menu item to the JBuilder Tools menu. When the user chooses the menu item, a simple dialog box appears that displays the number of characters, the number of words, and the number of lines in the current active file in the editor.

When the user selects a new file name in the combo box, the selected file becomes the active file and the dialog box displays the statistics for the new file in the editor.

This tutorial teaches these primary skills:

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


Getting started

To begin creating the OpenTool,

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

For more information on performing these tasks, see JBuilder OpenTools basics.

This OpenTool requires you to create two classes:


Creating the EditorStatsDialog

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 EditorStatsDialog.
  3. For the base class, specify javax.swing.JDialog.
  4. Verify that the Public and Generate Default Constructor options are checked, unchecking all other options.
  5. Choose OK.

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

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

Your code in the editor should look like this:

package EditorStats;

import javax.swing.JDialog;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class EditorStats extends JDialog implements ActionListener {

  public EditorStats() {
  }

  public void actionPerformed(ActionEvent e) {
    //TODO: Implement this java.awt.event.ActionListener method
  }
}

The class now extends JDialog and implements the ActionListener interface. Note that the Implement Interface wizard imported the required classes and interfaces.

Implementing the ActionListener interface makes the dialog box capable of responding when the user selects a different file in the combo box. You'll see the code to add to the actionPerformed() method in Listening for file name changes in the combo box.

Modifying the import statements

Modify the import statements so that they look like this:
import com.borland.primetime.editor.EditorManager;
import com.borland.primetime.editor.EditorAction;
import com.borland.primetime.editor.EditorPane;
import com.borland.primetime.ide.ProjectView;
import com.borland.primetime.node.Project;
import com.borland.primetime.node.Node;
import com.borland.primetime.viewer.TextNodeViewer;

import javax.swing.text.Document;
import javax.swing.text.Element;

Designing the dialog box

Click the Design tab to display the UI designer and design the dialog box so that it resembles this dialog box and contains these components:

All of the components you use must be Swing components or components derived from Swing such as JBuilder's dbSwing components.

The EditorStats OpenTool sample uses a GridBagLayout to place the components on the dialog box. You can do the same, or you can lay out the components any way you see fit for this tutorial. If you want more information about using GridBagLayout, see the GridBagLayout tutorial.

Using the Inspector, make these changes for these components:

jLabel1

jComboBox1

jLabel2

jLabel3

jLabel4

jButton1

To make the dialog box modal, first select this in the structure pane. Then, in the Inspector, set the modal property value to true.

Keeping track of the number of open files

The dialog box needs a way to keep track of the number of open files. In your source code, add an openFileCount instance variable after the component declarations but before the constructor:
// The components on the dialog
JPanel panel1 = new JPanel();
JLabel ProjectName = new JLabel();
...

int openFileCount;

Defining the constructors

The dialog box has two constructors. One is a default constructor that creates a non-modal dialog box that has no owner and no title on the dialog's title bar. The second constructs a modal dialog with an owner and a title.

Create the default constructor first. If you used the Class wizard to begin the EditorStatsDialog class, you'll already have the outline of a default constructor. Modify it so it looks like this code:

  public EditorStatsDialog() {
    this(null, "", false);
  }
The owner parameter is null, the string to display on the title bar is empty, and the modal parameter is false.

The second constructor is far more interesting. It accomplishes these tasks:

The code that creates a modal dialog calls its superclass and then calls the jbInit() method. jbInit() was created as you used the UI designer and modified property values in the Inspector. If jbInit() fails, an exception occurs. Begin creating the second constructor:

  public EditorStatsDialog(Frame frame, String title, boolean modal) {
    super(frame, title, modal);
    try {
      jbInit();
      pack();
    }
    catch(Exception ex) {
      ex.printStackTrace();
    }

Next, make the doneButton the default button. Although doneButton is the only button in the dialog box, making it the default lets the user quickly close the dialog box by pressing Enter. Add this code to the constructor:

    doneButton.setDefaultCapable(true);
    getRootPane().setDefaultButton(doneButton);

To actually close the dialog box, however, you must create a close action and link it with the doneButton so the event is triggered when the user clicks the button:

    ActionListener closeAction = new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        EditorStatsDialog.this.setVisible(false);
        EditorStatsDialog.this.dispose();
      }
    };
    doneButton.addActionListener(closeAction);

The next step is to have the constructor call a method that returns all the current open files in the browser:

  getOpenFiles();

You'll implement getOpenFiles() later.

Next, you need a way to determine when the selection in the combo box changes, so this code adds a listener for events that occur in the combo box:

  fileNamesCombo.addActionListener(this);

The final step in the constructor is to call the method that gathers and reports the number of characters, words, and lines are in the current open file:

  getCurrentStats();

Getting all the open files

Now you must implement the getOpenFiles() method that asks the browser for all the files that are currently open and displays them in the combo box. getOpenFiles() also sets the openFileCount instance variable to the number of open files. Create the method with this code:
  void getOpenFiles() {
    Node[] openFiles = Browser.getActiveBrowser().getOpenNodes();
    for (int i=0; i < openFiles.length; i++) {
      fileNamesCombo.addItem(openFiles[i].getDisplayName());
      if (openFiles[i].equals(Browser.getActiveBrowser().getActiveNode()))
        fileNamesCombo.setSelectedItem(openFiles[i].getDisplayName());
      openFileCount++;
    }
  }
 

To enable getOpenFiles() to access the Browser class, add this import statement to the EditorStatsDialog class:

 import com.borland.primetime.ide.Browser;

Counting the number of characters, lines, and words

Next implement the getCurrentStats() method, which counts the number of characters, lines, and words and displays the totals in the dialog box.

Begin the method by declaring variables to hold the totals:

  void getCurrentStats() {

    int characterCount = 0, wordCount = 0, lineCount = 0;

To get access to the document in the editor so the dialog can count its elements, you first need to obtain the editor of the current active node in the active browser. This code does that:

    Node node = Browser.getActiveBrowser().getActiveNode();
    TextNodeViewer viewer = (TextNodeViewer)
      Browser.getActiveBrowser().getViewerOfType(node, TextNodeViewer.class);
    EditorPane editor = (viewer == null) ? null : viewer.getEditor();

The first line obtains the active node in the active browser. The second line returns the first TextNodeViewer found for that active node. Finally, if a TextNodeViewer was successfully found, the viewer's editor is retrieved.

When you have the right editor for the current node, you need access to the document itself, starting at the beginning. Here is the code to do that:

    // If there is an editor
    if (editor != null) {
      Document doc = editor.getDocument();
      Element rootElement = doc.getDefaultRootElement();

There are existing methods to return the number of characters and the number of lines in the document, so use them to store the character and line count in the instance variables you created earlier:

      characterCount = doc.getLength();
      lineCount = rootElement.getElementCount();

Getting the number of words in the document requires more effort. Add the following code to your getCurrentStats() method implementation:

      int elementCount = rootElement.getElementCount();
      for (int i = 0; i < elementCount; i++) {
        try {
          // For each line, count the words.
          // We assume words are bordered by whitespace
          Element element = rootElement.getElement(i);
          int startOffset = element.getStartOffset();
          int endOffset = element.getEndOffset();
          String text = doc.getText(startOffset, endOffset - startOffset);
          char[] textArray = text.toCharArray();
          boolean inWhiteSpace = true;
          for (int j = 0; j < textArray.length - 1; j++) {
            boolean isWhiteSpace = Character.isWhitespace(textArray[j]);
            if (isWhiteSpace != inWhiteSpace) {
              if (inWhiteSpace == true)
                wordCount++;
              inWhiteSpace = isWhiteSpace;
            }
          }
        }
        catch(Exception b) {
          break;
        }
      }
    }

The code contains two for loops, one nested within the other because the words are counted one line at a time. The logic assumes that a word is a series of characters surrounded by whitespace. So the first for loop begins by establishing the beginning and the end of a line and then retrieving the text of the line. The second for loop then takes over and examines each character of that text and increments the word count each time it encounters another whitespace character. Lines are counted until the end of the document is reached.

Now that you have the number of characters, lines, and words in the document, all you need to do is to display them in the dialog box using the JLabel fields. The first field, which you named ProjectName, displays the name of the current project. Here is the logic to retrieve the current project name and display it in the ProjectName field:

    ProjectView pv = Browser.getActiveBrowser().getProjectView();
    Project project = pv.getActiveProject();
    ProjectName.setText("Current project: " + project.getDisplayName());

To complete the getCurrentStats() method, call the setText() methods of the TotalLines, TotalWords, and TotalCharacters fields:

    TotalLines.setText("Total Lines: " + lineCount);
    TotalWords.setText("Total Words: " + wordCount);
    TotalCharacters.setText("Total Characters: " + characterCount);
  }

Listening for file name changes in the combo box

When the user selects a new file using the dialog's combo box, the selected file should appear in the content pane (becoming the active node) and the dialog should report the current statistics for the new file. To have that happen, you must fill in the actionPerformed() method that is required to implement the ActiveListener interface. A skeleton of the actionPerformed() method was added when you used the Implement Interface wizard. Now is the time to fill it in. Because you're interested in events that occur only in the combo box, the code begins by screening for just those events:
  public void actionPerformed(ActionEvent e) {

    Object source = e.getSource();
    if (source == fileNamesCombo) {
      try {

Then the method attempts to find the open file in the browser whose name matches the user's select in the combo box. If it's found, it makes that file the active file and updates the statistics on it.

  
        // Try to find the open file in the browser whose name
        // matches the current selection in the ComboBox, and if
        // it is found, make that file the active file in the
        // Browser and update the statistics.
        Node[] openFiles = Browser.getActiveBrowser().getOpenNodes();
        for (int i=0; i < openFiles.length; i++) {
          if (openFiles[i].getDisplayName().equals(fileNamesCombo.getSelectedItem())) {
            Browser.getActiveBrowser().setActiveNode(openFiles[i], true);
            getCurrentStats();
            break;
          }
        }
      }
      catch(Exception ex) {
      }
    }
  }


Creating the EditorStats class

The EditorStats class is the class loaded by JBuilder that adds a new menu item to the Tools menu. When the user chooses the menu item, the action defined in EditorStats is called, displaying the EditorStatsDialog dialog box.

To begin creating the EditorStats class, use the Class wizard:

  1. Choose File|New Class to display the Class wizard.
  2. Enter the name of the class as EditorStats.
  3. Verify that the Public and Generate Default Constructor options are checked, unchecking all other options.
  4. Choose OK.
Your code in the editor should look like this:
package EditorStats;

public class EditorStats {

  public EditorStats() {
  }

The EditorStats class accomplishes two primary tasks:

Modifying the import statements

To enable the action to access all the necessary classes, add the following import statements to the class:

import com.borland.primetime.ide.Browser;
import com.borland.primetime.ide.BrowserAction;
import com.borland.primetime.PrimeTime;

import java.awt.Rectangle;
import java.awt.Dimension;
import java.awt.Toolkit;

Adding a new menu item

Each OpenTool must have an initOpenTool() method that is called as JBuilder is loading. The initOpenTool() method of EditorStats adds an Action to the JBuilder Tools menu group:
  public static void initOpenTool(byte majorVersion, byte minorVersion) {
    if (majorVersion == PrimeTime.CURRENT_MAJOR_VERSION)
      JBuilderMenu.GROUP_Tools.add(ACTION_StatsDialog);
  }

The JBuilderMenu class defines the menu groups that make up the menu system in the JBuilder IDE. The initOpenTool() method adds a single action to the GROUP_Tools group, which is the Tools menu. Because the action isn't added to one of the subgroups, such as GROUP_ToolsOptions, the new menu item will appear at the top of the Tools menu. If you did add an action to a subgroup, the menu item would appear within specified subgroup on the menu.

If your OpenTool requires several new menu items, you might want to create a new menu group, add actions for each menu item, and then add the new group to an existing menu.

For more information about adding and removing menu items, see Adding and removing individual menu Actions or ActionGroups.

You can also add a new menu to the JBuilder menu bar. For information on how to do that, see Adding and removing menu bar groups.

To provide access to the JBuilderMenu class, add this import statement to the EditorStats class:

import com.borland.jbuilder.JBuilderMenu;

Displaying the EditorStats dialog box

Now that you've written the code to add a new menu item, you must define the action that occurs when the user selects the menu item. ACTION_StatsDialog is a new BrowserAction that creates the EditorStatsDialog dialog box and displays it, centering it onscreen:
  public static /*final*/ BrowserAction ACTION_StatsDialog =

    // A new action with short menu string, mnemonic, and long menu string
    new BrowserAction("Editor Statistics", 'E',
                      "Displays statistics about an editor file") {

      // The function called when the menu is selected
      public void actionPerformed(Browser browser) {

        // Create the modal dialog box and center it on the screen
        EditorStatsDialog dialog = new EditorStatsDialog(browser,
                                                         "Editor Statistics",
                                                         true);
        Rectangle screenBounds =
          new Rectangle(Toolkit.getDefaultToolkit().getScreenSize());

        Dimension dialogSize = dialog.getSize();
        dialog.setLocation(
          screenBounds.x + (screenBounds.width - dialogSize.width) / 2,
          screenBounds.y + (screenBounds.height - dialogSize.height) / 2);
        dialog.setVisible(true);
      }
    };

A new BrowserAction requires three parameters, the string that appears on the menu, a mnemonic for the accelerator key for the menu option, and a long menu string.

The definition of the dialog variable calls the constructor of EditorStatsDialog and passes along three parameters: the owner of the dialog (the browser), the title bar text (Editor Statistics), and a boolean value that indicates whether the dialog is modal.


Finishing up

To finish the EditorStats OpenTool, follow these steps:

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

  2. Create a manifest file for your project that contains this text:
    OpenTools-UI: 
      EditorStats.EditorStats
    

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

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

  5. Start up JBuilder and look for the EditorStats menu item on the Tools menu.

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