Writing BlueJ 5+ 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 Package, 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 (for Java only). 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.
BlueJ 5.0.1 (with the extensions API v3.1) includes an new feature for extensions to expose a launcher for external file resources.

As from BlueJ 5.0.0, extensions need to be updated because of the changes in the Extensions API breaking backward compatibility. The details of the required changes are provided in the Extensions upgrade to BlueJ 5.0.0 changes guidelines section.

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 bluej.jar file you need to include in your compiler classpath, and the extensions2 folder into which you should place your extension's Jar file.

This example covers the following functionalities for an extension:

The main class of the Simple Extension

An extension needs to extend the Extension class exposed by the Extensions API.

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.extensions2.*;
import bluej.extensions2.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 (getExtensionsAPIVersionMajor() >= 3); 
    }

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

    /*
     * 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 wishing to add a menu item to BlueJ's menus should extends the MenuGenerator class exposed by the Extensions API, and register this instance of MenuGenerator with the BlueJ proxy object. For example, this code can be added to the SimpleExtension class described above (where MenuBuilder is a subclass of MenuGenerator and is documented below):

    MenuBuilder myMenus = new MenuBuilder();
    bluej.setMenuGenerator(myMenus);
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 MenuItem which is returned by the extension can itself be a Menu, 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, Packages, Classes and Objects.


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

The source code for this example MenuGenerator is:

import bluej.extensions2.*;
import javafx.event.*;
import javafx.scene.control.*;
import javafx.stage.StageStyle;

class MenuBuilder extends MenuGenerator
{
    private BPackage curPackage;
	private BClass curClass;
	private BObject curObject;
	private EventHandler menAction = menuAction();
	public MenuItem getToolsMenuItem(BPackage aPackage)
	{
		MenuItem mi = new MenuItem();
		mi.setText("Click Tools");
		mi.setId("Tools menu:");
		mi.setOnAction(menAction);
		return mi;
	}

	public MenuItem getPackageMenuItem(BPackage bPackage) {
		MenuItem mi = new MenuItem();
		mi.setText("Click Package");
		mi.setId("Package menu:");
		mi.setOnAction(menAction);
		return mi;
	}

	public MenuItem getClassMenuItem(BClass aClass)
	{
		MenuItem mi = new MenuItem();
		mi.setText("Click Class");
		mi.setId("Class menu:");
		mi.setOnAction(menAction);		
		return mi;
	}

	public MenuItem getObjectMenuItem(BObject anObject)
	{
		MenuItem mi = new MenuItem();
		mi.setText("Click Object");
		mi.setId("Object menu:");
		mi.setOnAction(menAction);
		return mi;
	}

	// These methods will be called when
	// each of the different menus are about to be invoked.
	public void notifyPostToolsMenu(BPackage bp, MenuItem mi)
	{
		System.out.println("Post on Tools menu");
		curPackage = bp;
		curClass = null;
		curObject = null;
	}

	public void notifyPostClassMenu(BClass bc, MenuItem mi)
	{
		System.out.println("Post on Class menu");
		curPackage = null;
		curClass = bc;
		curObject = null;
	}

	public void notifyPostObjectMenu(BObject bo, MenuItem mi)
	{
		System.out.println("Post on Object menu");
		curPackage = null;
		curClass = null;
		curObject = bo;
	}

	public void notifyPostPackageMenu(BPackage bp, MenuItem menuItem) {
		System.out.println("Post on Package menu");
		curPackage = bp;
		curClass = null;
		curObject = null;
	}

	// A utility method which pops up a dialog detailing the objects
	public EventHandler menuAction()
	{
		return actionEvent ->
		{
			try
			{
				if (curObject != null)
					curClass = curObject.getBClass();
				if (curClass != null)
					curPackage = curClass.getPackage();

				String msg = ((MenuItem) actionEvent.getSource()).getId();
				if (curPackage != null)
					msg += "\nCurrent Package = " + curPackage;
				if (curClass != null)
					msg += "\nCurrent Class = " + curClass;
				if (curObject != null)
					msg += "\nCurrent Object = " + curObject;

				Alert dlg = new Alert(Alert.AlertType.NONE, msg, ButtonType.OK);
				dlg.initStyle(StageStyle.UTILITY);
				dlg.showAndWait();
			} catch (Exception exc)
			{
				exc.printStackTrace();
			}
		};
	}
}
Adding UI to BlueJ's Preferences panel

Extensions wishing to add UI to BlueJ's Tools/Preferences/Extensions panel should implement the PreferenceGenerator interface exposed by the Extensions API and register this instance of PreferenceGenerator with the BlueJ proxy object. For example, this code can be added to the SimpleExtension class described above (where Preferences is an implementation of PreferenceGenerator and is documented below):

    Preferences myPrefs = new Preferences(bluej);
    bluej.setPreferenceGenerator(myPrefs);
The PreferenceGenerator allows the creation of a JavaFX Pane 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.

import bluej.extensions2.BlueJ;
import bluej.extensions2.PreferenceGenerator;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;

class Preferences implements PreferenceGenerator
{
    private Pane myPane;
    private TextField 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;
        myPane = new Pane();
        HBox hboxContainer = new HBox();
        hboxContainer.getChildren().add(new Label("Favorite Colour: "));
        color = new TextField();
        hboxContainer.getChildren().add(color);
        myPane.getChildren().add(hboxContainer);
        // Load the default value
        loadValues();
    }

    public Pane getWindow()
    {
        return myPane;
    }

    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 properties 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 method getJavaEditor() of the class BClass exposed by the Extensions API. Note that only Java classes type can be modified. The Extensions API does not yet support changes in Stride classes.

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 JavaEditor 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.

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

private void addComment(BClass curClass)
{
    JavaEditor classEditor = null;
    try
    {
        classEditor = curClass.getJavaEditor();
    }
    catch (Exception e) { }
    if(classEditor == null)
    {
        System.out.println("Can't create Java 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");
}
Allow external files be shown in BlueJ and opened outside BlueJ

Extensions can specify a launcher for external file types that BlueJ does not support otherwise. BlueJ will search for all external files types exposed by BlueJ extensions and allow files of these types to be shown in the BlueJ class diagramme as an external file. BlueJ then call the launcher defined by the extension to open the file outside BlueJ. An extension must call the method addExternalFileLaunchers() of the BlueJ object argument of startup(). The extension can provide a list of ExternalFileLauncher objects, which contains the file extension to open and the associated launcher (a ExternalFileLauncher.OpenExternalFileHandler object).
For example, this code can be added to startup() in the SimpleExtension class described above to allow BlueJ showing HTML and PDF as external files in the main interface, and provide a launcher to open them:

// This is the launcher object that will be used for both HTML and PDF extensions
ExternalFileLauncher.OpenExternalFileHandler browserOpener = new ExternalFileLauncher.OpenExternalFileHandler()
{
	@Override
	public void openFile(String fileName) throws Exception
	{
		// This method will be called by BlueJ when an attempt to open the file is made.
		// (provided no other extension has overwritten the launcher for the specified file type.)
		AtomicBoolean hasErrorOccurred = new AtomicBoolean(false);
		Task task = new Task<>()
		{
			@Override
			public Void call()
			{
				File externalFile = new File(fileName);
				try
				{
					Desktop.getDesktop().browse(externalFile.toURI());
				}
				catch (IOException e)
				{
					hasErrorOccurred.set(true);
					this.cancel();
				}
				return null;
			}
		};

		// Launch the browser outside the main BlueJ java FX thread.
		Thread thread = new Thread(task);
		thread.start();

		// Wait sufficiently long to get the application opening and if not returning just continue
		thread.join(10000);
		if(hasErrorOccurred.get()){
			throw new Exception("A problem occurred opening "+ fileName);
		}
	}
};

// Prepare a list of launchers for HTML and PDF files.
List list = new ArrayList<>();
ExternalFileLauncher htmlLauncher = new ExternalFileLauncher("*.html", browserOpener);
ExternalFileLauncher pdfLauncher = new ExternalFileLauncher(".pdf", browserOpener);
list.add(htmlLauncher);
list.add(pdfLauncher);

// Set the launchers for registration to BlueJ
bluej.addExternalFileLaunchers(list);
Extensions upgrade to BlueJ 5.0.0 changes guidelines

As for BlueJ 5.0.0, the Extensions API breaks backward compatibility with extensions designed for previous versions of BlueJ. Therefore, extensions need to be updated to work with the new Extensions API as BlueJ will not be able to launch extensions referencing the old version of the API.
The main updates brought to the Extensions API were made to:

Authors of BlueJ extensions can follow these guidelines that covers the necessary steps to update their extensions.

  1. General changes,
  2. Changes for extensions adding menu entries in BlueJ
  3. Changes for extensions adding UI to the Tools/Preferences/Extensions panel in BlueJ
  4. Changes for extensions interacting with text in the BlueJ editor
  5. Changes for extensions changing the classes' representation in BlueJ
  1. General changes
  2. To be launched by BlueJ, extensions now need to use the new packages exposed by the Extensions API. The new root package is bluej.extensions2 instead of bluej.extensions.
    The events mechanism has slightly changed with the new Extensions API so older extensions may need to be updated accordingly.

    • The event types are no longer referred as int fields in the *Event classes. They have been replaced by enum constants, the enumeration are called EventType and the underlying members use the same names as the previous int field names .
    • The ClassListener2 (which did not appear in previous Extensions API documentation) has been removed and merged with ClassListener: the latter now contains EventType.REMOVED and this event type is now properly sent by BlueJ to extensions.
    • The InvocationEvent class and the InvocationListener interface have been renamed to InvocationFinishedEvent and InvocationFinishedListener.
    • The method CompileEvent.keepClasses() (which did not appear in the previous Extensions API documentation) has been renamed to CompileEvent.isUserGeneratedCompilation().

    Since BlueJ 5.0.0 no longer supports Swing for the extensions, retrieving BlueJ GUI elements has changed too.

    • The method BlueJ.getCurrentFrame() has been renamed to BlueJ.getCurrentWindow() and returns BlueJ's JavaFX Stage instead of a Swing Frame.
    • Similarly, BPackage.getFrame() has been renamed to BPackage.getWindow() and returns the underlying BlueJ package's JavaFX Stage instead of a Swing Frame.
    • BlueJ's bluej.pkgmgr.PkgMgrFrame exposed a method getWindow() which returned a Swing Frame object. This method returns now a JavaFX Stage object instead. If an extension accessed this methods via the Extensions API, it must adapt to its new returned type.
    • As it was already the case with extensions written for BlueJ's prior versions, updated extensions need to be aware of GUI threading practices. In order to avoid blocking the BlueJ’s GUI, it is advised that heavy computing operations be performed on a separate thread rather than on the JavaFX (GUI) thread. For more information about threading in JavaFX check this JavaFX documentation.

    The following deprecated methods or fields are now completely removed from the Extensions API.

    • InvocationEvent.FORCED_EXIT (use other fields instead)
    • BClass.beginChangeSource() (replaced by JavaEditor.setReadOnly(boolean readOnly))
    • BClass.endChangeSource() (replaced by JavaEditor.setReadOnly(boolean readOnly))
    • MenuGenerator.getMenuItem() (replaced by MenuGenerator.get*MenuItem())
    • BField.matches(String) (use BField.getName().equals(fieldName) instead)

    Other changes:

    • The method BlueJ.newProject(File directory) no longer opens the created project in BlueJ. Use BlueJ.openProject​(java.io.File directory) in a subsequent call instead to open the project.
    • The class Extension does no longer expose the int fields VERSION_MAJOR and VERSION_MINOR to retrieve the Extensions API's major and minor versions. Instead, extensions are invited to call the new methods getExtensionsAPIVersionMajor() and getExtensionsAPIVersionMinor() exposed by Extension. This is notably encouraged to evaluate if an extension is compatible with the Extensions API in the method isCompatible().

  3. Changes for extensions adding menu entries in BlueJ
  4. The mechanism for extensions to add menu entries in BlueJ functionally stays the same. However, extensions can no longer add an entry into the BlueJ's View menu (part which did not appear in the previous Extensions API documentation), and because of the extensions now working with JavaFX rather than Swing, the following methods return types have changed in the MenuGenerator class:

    • getClassMenuItem(BClass bClass), getObjectMenuItem(BObject bObject), getPackageMenuItem(BPackage bPackage) and getToolsMenuItem(BPackage bPackage) should now return a javafx.scence.control.MenuItem object,
    • notifyPostClassMenu(BClass bClass, javafx.scene.control.MenuItem menuItem), notifyPostObjectMenu(BObject bObject, javafx.scene.control.MenuItem menuItem), notifyPostPackageMenu(BPackage bPackage, javafx.scene.control.MenuItem menuItem) and notifyPostToolsMenu(BPackage bPackage, javafx.scene.control.MenuItem menuItem) take a javafx.scene.control.MenuItem object as an argument and therefore these methods' content need to be adapted for this type of object.

  5. Changes for extensions adding UI to the Tools/Preferences/Extensions panel in BlueJ
  6. Extensions can implement a UI using the Swing framework. That is, for the class PreferenceGenerator, the method getPanel() does no longer exist and is replaced getWindow() that must return an object of type javafx.scene.layout.Pane containing the extension’s own interface (now to be written with JavaFX). This Pane object is added by BlueJ in the preferences tab. Potentially, the content of methods saveValues() and loadValues() may need to be adapted as well to use the new JavaFX UI components if it applies.

  7. Changes for extensions interacting with text in the BlueJ editor
  8. Extensions need to be updated to amend the following changes:

    • the class Editor exposed by the Extensions API has been renamed JavaEditor,
    • the method BClass.getEditor() has been renamed BClass.getJavaEditor(),
    • extensions can only interact with editors associated with a class of type Java, if an attempt is made to interact with an editor targeting a Stride class, BClass.getJavaEditor() now returns null.

  9. Changes for extensions changing the classes' representation in BlueJ
  10. Although not documented in the previous versions of the Extensions API, extensions can no longer modify the classes' representation in BlueJ. As a consequence the following changes have been made in the Extensions API:

    • BClassTarget and BDependency have been removed from the API, as well as ClassTargetListener, ClassTargetEvent, DependencyListener and DependencyEvent (BlueJ does no longer trigger the associated events),
    • what was in relation with the submentioned classes in BClass, BConstructor and BlueJ has been removed,
    • ExtensionClassTargetPainter and the subpackage bluej.extensions.painter have been removed from the API.