Writing BlueJ Extensions


Introduction

The BlueJ extensions mechanism is a way of adding new functionality to BlueJ as and when it is needed, avoiding user interface clutter and user confusion.

The BlueJ Extensions API provides access for extensions to the BlueJ application via a proxy object, and to the classes and objects which BlueJ is manipulating via a number of wrapper classes.

An extension can add a menu item to the BlueJ Tools menu, and to Class and Object menus, add a preference panel to the Tools/Preferences/Extensions panel, and interact with the BlueJ editor to retrieve and modify the text in source files. The BlueJ proxy object generates events when the user performs significant actions within BlueJ, and provides access to its projects, packages, classes and objects via a set of wrapper classes which behave in a similar way to the java.lang.reflect classes.

A Simple Extension

The following example implements an extension which logs the name of every BlueJ project opened by the user to System.out and demonstrates the use of the other extension features. Once it is installed you should also see:

The full source code for the extension is here.

How to build and install the Simple Extension

Note to Mac users: To navigate to the lib directory of your BlueJ installation, right-click (or control-click) on the BlueJ application icon, select "Show Package Contents" and then "Contents/Resources/Java". Here you should find the bluejext.jar file you need to include in your compiler classpath, and the extensions folder into which you should place your extension's Jar file.

The main class of the Simple Extension

An object of this type will be constructed by the BlueJ extension manager, so this class must have a no-args constructor (which this one does by default). The class registers itself as a listener for BlueJ package events.

import bluej.extensions.*;
import bluej.extensions.event.*;
import java.net.URL;

/*
 * This is the starting point of a BlueJ Extension
 */
public class SimpleExtension extends Extension implements PackageListener
{
    /*
     * When this method is called, the extension may start its work.
     */
    public void startup(BlueJ bluej)
    {
        // Listen for BlueJ events at the "package" level
        bluej.addPackageListener(this);
    }

    /*
     * A package has been opened. Print the name of the project it is part of.
     * System.out is redirected to the BlueJ debug log file.
     * The location of this file is given in the Help/About BlueJ dialog box.
     */
    public void packageOpened(PackageEvent ev)
    {
        try
        {
            System.out.println("Project " + ev.getPackage().getProject().getName() 
                               + " opened.");
        }
        catch (ExtensionException e)
        {
            System.out.println("Project closed by BlueJ");
        }
    } 
               
    /*
     * A package is closing.
     */
    public void packageClosing(PackageEvent ev)
    {
    } 
               
    /*
     * This method must decide if this Extension is compatible with the                
     * current release of the BlueJ Extensions API
     */
    public boolean isCompatible()
    { 
        return true; 
    }

    /*
     * Returns the version number of this extension
     */
    public String getVersion ()
    { 
        return ("2004.09"); 
    }

    /*
     * Returns the user-visible name of this extension
     */
    public String getName ()
    { 
        return ("Simple Extension"); 
    }

    public void terminate()
    {
        System.out.println ("Simple extension terminates");
    }
               
    public String getDescription ()
    {
        return ("A simple extension");
    }

    /*
     * Returns a URL where you can find info on this extension.
     * The real problem is making sure that the link will still be alive 
     * in three years...
     */
    public URL getURL ()
    {
        try
        {
            return new URL("http://www.bluej.org/doc/writingextensions.html");
        }
        catch ( Exception e )
        {
            // The link is either dead or otherwise unreachable
            System.out.println ("Simple extension: getURL: Exception="+e.getMessage());
            return null;
        }
    }
}
Adding menus to BlueJ's menus

Extensions which wish to add a menu item to BlueJ's menus should register an instance of MenuGenerator with the BlueJ proxy object. A MenuGenerator provides a set of functions which can be called back by BlueJ to request the actual menu items which will be displayed, and to indicate that a particular menu item is about to be displayed, so that an extension can (e.g.) enable or disable appropriate items. Note that the JMenuItem which is returned by the extension can itself be a JMenu, allowing extensions to build more complex menu structures, but that the "notify" methods below will only be called for the item which has actually been added, and not any subsidiary items. Below is a simple example which creates menus for Tools, Classes and Objects. To activate the menus you instantiate an object of the MenuGenerator class and then register it with the BlueJ proxy object, e.g.:

    MenuBuilder myMenus = new MenuBuilder();
    bluej.setMenuGenerator(myMenus);


Note that the MenuGenerator's get*MenuItem() methods:

The source code for this example MenuGenerator is:

import bluej.extensions.*;
import javax.swing.*;
import java.awt.event.*;

class MenuBuilder extends MenuGenerator
{
    private BPackage curPackage;
    private BClass curClass;
    private BObject curObject;
               
    public JMenuItem getToolsMenuItem(BPackage aPackage)
    {
        return new JMenuItem(new SimpleAction("Click Tools", "Tools menu:"));
    }
               
    public JMenuItem getClassMenuItem(BClass aClass)
    {
        return new JMenuItem(new SimpleAction("Click Class", "Class menu:"));
    }
               
    public JMenuItem getObjectMenuItem(BObject anObject)
    {
        return new JMenuItem(new SimpleAction("Click Object", "Object menu:"));
    }
               
    // These methods will be called when
    // each of the different menus are about to be invoked.
    public void notifyPostToolsMenu(BPackage bp, JMenuItem jmi)
    {
        System.out.println("Post on Tools menu");
        curPackage = bp ; curClass = null ; curObject = null;
    }
               
    public void notifyPostClassMenu(BClass bc, JMenuItem jmi)
    {
        System.out.println("Post on Class menu");
        curPackage = null ; curClass = bc ; curObject = null;
    }
               
    public void notifyPostObjectMenu(BObject bo, JMenuItem jmi)
    {
        System.out.println("Post on Object menu");
        curPackage = null ; curClass = null ; curObject = bo;
    }
               
    // A utility method which pops up a dialog detailing the objects                
    // involved in the current (SimpleAction) menu invocation.
    private void showCurrentStatus(String header)
    {
        try
        {
            if (curObject != null)
                curClass = curObject.getBClass();
            if (curClass != null)
                curPackage = curClass.getPackage();
               
            String msg = header;
            if (curPackage != null)
                msg += "\nCurrent Package = " + curPackage;
            if (curClass != null)
                msg += "\nCurrent Class = " + curClass;
            if (curObject != null)
                msg += "\nCurrent Object = " + curObject;
            JOptionPane.showMessageDialog(null, msg);
        }
        catch (Exception exc)
        {
        }
    }
               
    // The nested class that instantiates the different (simple) menus.
    class SimpleAction extends AbstractAction {
        private String msgHeader;
               
        public SimpleAction(String menuName, String msg)
        {
            putValue(AbstractAction.NAME, menuName);
            msgHeader = msg;
        }
        public void actionPerformed(ActionEvent anEvent)
        {
            showCurrentStatus(msgHeader);
        }
    }
}
Adding items to BlueJ's Preferences panel

Extensions which wish to add preference items to BlueJ's Tools/Preferences/Extensions panel should register an instance of PreferenceGenerator with the BlueJ proxy object. The PreferenceGenerator allows the creation of a Panel to contain preference data, and the loading and saving of that data. Below is a simple example to create a preference panel with a single text item to record a user's favourite colour. To activate the preference panel you instantiate an object of the Preferences class and then register it with the BlueJ proxy object (see above).

class Preferences implements PreferenceGenerator
{
    private JPanel myPanel;
    private JTextField color;
    private BlueJ bluej;
    public static final String PROFILE_LABEL="Favorite-Colour";

    // Construct the panel, and initialise it from any stored values
    public Preferences(BlueJ bluej)
    {
        this.bluej = bluej;
        myPanel = new JPanel();
        myPanel.add (new JLabel ("Favorite Colour"));
        color = new JTextField (40);
        myPanel.add (color);
        // Load the default value
        loadValues();
    }

    public JPanel getPanel () { return myPanel; }

    public void saveValues ()
    {
        // Save the preference value in the BlueJ properties file
        bluej.setExtensionPropertyString(PROFILE_LABEL, color.getText());
    }

    public void loadValues ()
    {
        // Load the property value from the BlueJ proerties file, 
        // default to an empty string
        color.setText(bluej.getExtensionPropertyString(PROFILE_LABEL,""));
    }
}
Interacting with the BlueJ editor

A proxy for a particular class's editor can be obtained from the getEditor() method of BClass.

This proxy can be used to retrieve the text currently associated with the class, to determine the user's current cursor position and text selection, and to modify the text being edited. A simple example of how to use these facilities is given below.

Extension authors who need more sophisticated interaction between their own, modified, version of the BlueJ editor and their extension can use the set/getProperty() mechanism in the Editor class.

Note that there is no way to determine whether the BlueJ user has an existing open editor for a class. Partly this is because there is no way (in general) to tell whether an editor is being actively used: it may be iconised, or obscured by other windows on the users screen, or otherwise not the focus of their attention. It is also because BlueJ does not guarantee to release the resources of editors which have been closed by the user: an editor may be present for a particular class, even if there is no window representing it.

Changes which affect the editor GUI (e.g. selecting or modifying text) should be performed from a Swing thread.

As a simple example, the following method adds a comment before the last source line of the given class:

import bluej.extensions.*;
import bluej.extensions.editor.*;


private void addComment(BClass curClass)
{
    Editor classEditor = null;
    try
    {
        classEditor = curClass.getEditor();
    }
    catch (Exception e) { }
    if(classEditor == null)
    {
        System.out.println("Can't create Editor for " + curClass);
        return;
    }
               
    int textLen = classEditor.getTextLength();
    TextLocation lastLine = classEditor.getTextLocationFromOffset(textLen);
    lastLine.setColumn(0);
    // The TextLocation now points before the first character of the last 
    // line of the current text
    // which we'll assume contains the closing } bracket for the class
    classEditor.setText(lastLine, lastLine, "// Comment added by SimpleExtension\n");
}