Writing a Plugin Core Class

The major issues encountered when writing a plugin core class arise from the developer's decisions on what features the plugin will make available. These issues have implications for other plugin elements as well.

Choosing a Base Class

If the plugin will respond to EditBus messages, it must be derived from EBPlugin. Otherwise, EditPlugin will suffice as a base class.

Knowing what types of messages are made available by the plugin API is obviously helpful is determining both the base plugin class and the contents of a handleMessage() method. The message classes derived from EBMessage cover the opening and closing of the application, changes in the status of text buffers and their container and changes in user settings, as well as changes in the state of other program features. Specific message classes of potential interest to a plugin include the following:

  • EditorStarted, sent during the application's startup routine, just prior to the creation of the initial View;

  • EditorExitRequested, sent when a request to exit has been made, but before saving open buffers and writing user settings to storage;

  • EditorExiting, sent just before jEdit actually exits;

  • EditPaneUpdate, sent when an edit pane containing a text area (including a pane created by splitting an existing window) has been created or destroyed, or when a buffer displayed in an edit pane has been changed;

  • BufferUpdate, sent when a text buffer is created, loaded, or being saved, or when its editing mode or markers have changed;

  • ViewUpdate, sent when a View is created or closed; and

  • PropertiesChanged, sent when the properties of the application or a plugin has been changed through the General Options dialog;

Detailed documentation for each message class can be found in Chapter 21.

Implementing Base Class Methods

General Considerations

Whether EditPlugin or EBPlugin is selected as the base of the plugin core class, the implementations of start() and stop() in the plugin's derived class are likely to be trivial, or not present at all (in which case they will be "no-ops" inherited from EditPlugin).

The plugin core class can include static final String data members containing information to be registered with the EditBus or key names for certain types of plugin properties. This makes it easier to refer to the information when a method such as handleMessage() examines the contents of a message. The kind of data that can be handled in this fashion include the following:

  • the name of the plugin;

  • a label for identifying the plugin's menu;

  • a prefix for labeling properties required by the plugin API; and

  • a prefix to be used in labeling items used in the plugin's option pane

Example Plugin Core Class

We will derive the plugin core class for QuickNotepad from EditPlugin, since there are no EditBus messages to which the plugin core class need respond. There are no special initialization or shut down chores to perform, so we will not need a start() or stop() method. We will define a few static String data members to enforce consistent syntax for the name of properties we will use throughout the plugin. Finally, we will use a standalone plugin window class to separate the functions of that class from the visible component class we will create.

The resulting plugin core class is lightweight and straightforward to implement:

public class QuickNotepadPlugin extends EBPlugin {
    public static final String NAME = "quicknotepad";
    public static final String MENU = "quicknotepad.menu";
    public static final String PROPERTY_PREFIX
        = "plugin.QuickNotepadPlugin.";
    public static final String OPTION_PREFIX
        = "options.quicknotepad.";

    public void createMenuItems(Vector menuItems) {
        menuItems.addElement(GUIUtilities.loadMenu(MENU));
    }

    public void createOptionPanes(OptionsDialog od) {
        od.addOptionPane(new QuickNotepadOptionPane());
    }


}

The implementations of createMenuItems() and createOptionPane() are typically trivial, because the real work will be done using other plugin elements. Menu creation is performed by a utility function in jEdit's API, using properties defined in the plugin's properties file. The option pane is constructed in its own class.

If the plugin only had a single menu item (for example, a checkbox item that toggled activation of a dockable window), we would call GUIUtilities.loadMenuItem() instead of loadMenu(). We will explain the use of both methods in the next section.

Resources for the Plugin Core Class

Actions

The plugin's user action catalog, actions.xml, is the resource used by the plugin API to get the names and definitions of user actions. The following actions.xml file from the QuickNotepad plugin can provide a model:

<!DOCTYPE ACTIONS SYSTEM "actions.dtd">

<ACTIONS>
    <ACTION NAME="quicknotepad.choose-file">
        <CODE>
            view.getDockableWindowManager()
              .getDockable(QuickNotepadPlugin.NAME).chooseFile();
        </CODE>
    </ACTION>

    <ACTION NAME="quicknotepad.save-file">
        <CODE>
            view.getDockableWindowManager()
              .getDockable(QuickNotepadPlugin.NAME).saveFile();
        </CODE>
    </ACTION>

    <ACTION NAME="quicknotepad.copy-to-buffer">
        <CODE>
            view.getDockableWindowManager()
              .getDockable(QuickNotepadPlugin.NAME).copyToBuffer();
        </CODE>
    </ACTION>
</ACTIONS>

This file defines three actions. They use the current view's DockableWindowManager object and the method getDockable() to find the QuickNotepad plugin window and call the desired method.

When an action is invoked, program control must pass to the component responsible for executing the action. The use of an internal table of BeanShell scripts that implement actions avoids the need for plugins to implement ActionListener or similar objects to respond to actions. Instead, the BeanShell scripts address the plugin through static methods, or if instance data is needed, the current View, its DockableWindowManager, and the plugin object return by the getDockable() method.

If you are unfamiliar with BeanShell code, you may nevertheless notice that the code statements bear a strong resemblance to Java code, with one exception: the variable view is never assigned any value.

For complete answers to this and other BeanShell mysteries, see Part III; two observations will suffice here. First, the variable view is predefined by jEdit's implementation of BeanShell to refer to the current View object. Second, the BeanShell scripting language is based upon Java syntax, but allows variables to be typed at run time, so explicit types for variables need not be declared.

A formal description of each element of the actions.xml file can be found in the section called "The Action Catalog".

Action Labels and Menu Items

Now that we have named and defined actions for the plugin, we have to put them to work. To do so, we must first give them labels that can be used in menu items and in the sections of jEdit's options dialog that deal with toolbar buttons, keyboard shortcuts and context menu items. We supply this information to jEdit through entries in the plugin's properties file. A call to GUIUtilities.loadMenu() or GUIUtilities.loadMenuItem() will read and extract the necessary labels from the contents of a properties file.

The following excerpt from QuickNotepad.props illustrates the format required for action labels and menu items:

# action labels
quicknotepad.toggle.label=QuickNotepad
quicknotepad-to-front.label=Bring QuickNotepad to front
quicknotepad.choose-file.label=Choose notepad file
quicknotepad.save-file.label=Save notepad file
quicknotepad.copy-to-buffer.label=Copy notepad to buffer

# application menu items
quicknotepad.menu.label=QuickNotepad
quicknotepad.menu=quicknotepad.toggle - quicknotepad.choose-file \
    quicknotepad.save-file quicknotepad.copy-to-buffer

GUIUtilities.loadMenuItem() and GUIUtilites.loadMenu() use special conventions for the value of a menu property to specify menu layout. In loadMenu(), the use of the dash, as in the second item in the example menu list, signifies the placement of a separator. In addition, the character '%' used as a prefix on a label causes loadMenu() to call itself recursively with the prefixed label as the source of submenu data. Most plugins will not need to define menus that contain other submenus.

Note also that quicknotepad-to-front is not included in the menu listing. It will appear, however, on the Shortcuts pane of the Global Options dialog, so that the action can be associated with a keyboard shortcut.