ONJava.com    
 Published on ONJava.com (http://www.onjava.com/)
 See this if you're having trouble printing code examples


Eclipse Plugins Exposed, Part 3: Customizing a Wizard

by Emmanuel Proulx
07/27/2005

A while ago, I had a tiny portable electronic address book. I took it for granted until the day it stopped working. The salesperson who sold it to me couldn't retrieve my contacts, but offered to replace it. That day I learned that data is important. This shiny gizmo was worth nothing compared to the bits stored on it.

In part one of this series, we introduced Eclipse's plugin development environment and developed a simple plugin. In part two, we added a toolbar button, a menu item, and dialogs. The result was a shiny gizmo that didn't do much for us. It simply displayed sample text using a font. Now we need to make it manage actual data. We will massage the plugin so that it does what we need it to do. This article discusses editor documents and shows how to customize a wizard.

Invokatron Story

But first, let's elaborate on Invokatron itself. As discussed in the previous articles, Invokatron is an graphical tool that generates Java code. You can code up a class' methods simply by dragging and dropping. The dragged-in method is "invoked" by the edited method; thus, the name of the plugin. We will let the data drive the design of our application. In a later article, we're going to develop this GUI. For now, all we need to do is to figure out the important data our plugin will input and store. This is often referred to as the model of the application. Here are the things we'll need to worry about when designing our system:

Related Reading

Eclipse Cookbook
By Steve Holzner

These decisions have to be resolved before we go on. There is no right answer that is good for all projects; it all depends on your needs. In our case, I made arbitrary, questionable decisions as follows:

Document Class

The next step is to write the document class. Create a new package named invokatron.model, and a new class named InvokatronDocument. Here's the first shot at our document class:

public class InvokatronDocument
        extends Properties
{
    public static final String PACKAGE = "package";
    public static final String SUPERCLASS = "superclass";
    public static final String INTERFACES = "interfaces";
}

Using the Properties class allows for simple parsing and saving of our data. The getter and setter methods are not necessary, but you could add them if you want to. This class is not finished; we will add the interfaces that Eclipse requires later on.

With this class, getting a property is as simple as:

String package =
    document.getProperty(InvokatronDocument.PACKAGE);

Customizing a Wizard

Have a look at our wizard as it was in the previous article (get the source code if you don't have it already). Remember, you can access it by clicking on the toolbar button or menu item we added. Here it is in figure 1:

Old Wizard

Figure 1. Old wizard

It has only one page, with no graphic in the upper-right corner. We'd like to enter more information and have a nice graphic. In other words, we'd like to customize this wizard.

Let's dissect our wizard. Open the file InvokatronWizard.java. Notice how this class extends Wizard and implements INewWizard. These have many methods you should know about. To customize the wizard, we simply invoke or override some of these methods. Here are a few important ones:

Lifecycle Methods

These methods should be overridden to insert initialization and destruction code into our wizard.

Beautification Methods

These methods are used to decorate the wizard window.

Button Methods

These methods control the availability and behavior of the wizard's buttons.

Page Methods

These methods control the appearance of pages.

Other Useful Methods

These are useful helper methods:

Wizard Page Methods

As we've seen, a wizard is composed of one or more pages. These pages extend the WizardPage class and implement the IWizardPage interface. These have many methods you should know about in order to customize individual pages. Here are a few important ones:

Coding the Wizard

Armed with these many methods, it is now possible to develop wizards with infinite flexibility. We will now modify the Invokatron wizard that we created in the previous articles, and give it a page to request the initial document data. We will also add a graphic to the wizard. The new code is in bold:

public class InvokatronWizard extends Wizard
        implements INewWizard {
    private InvokatronWizardPage page;
    private InvokatronWizardPage2 page2;
    private ISelection selection;

    public InvokatronWizard() {
        super();
        setNeedsProgressMonitor(true);
        ImageDescriptor image =
            AbstractUIPlugin.
                imageDescriptorFromPlugin("Invokatron",
                   "icons/InvokatronIcon32.GIF");
        setDefaultPageImageDescriptor(image);
    }

    public void init(IWorkbench workbench,
            IStructuredSelection selection) {
        this.selection = selection;
    }

In the constructor, we're turning on the progress monitor and setting the image for the wizard. You can download the following new icon by right-clicking:

Invokatron Icon

to save. You have to save this icon in the Invokatron/icons folder. To facilitate the loading of this image, we use the handy-dandy AbstractUIPlugin.imageDescriptorFromPlugin() method.

Note: You should know that, although this wizard is of the type INewWizard, not all wizards are used to create new documents. For information on how to display a "standalone" wizard, refer to the resources section at the bottom of this article.

Next is the addPages() method:

    public void addPages() {
        page=new InvokatronWizardPage(selection);
        addPage(page);
        page2 = new InvokatronWizardPage2(
            selection);
        addPage(page2);
    }

In this method, we're adding one new page, named InvokatronWizardPage2, which we'll write soon. Next are methods that will execute when the user presses the Finish button on the wizard:

    public boolean performFinish() {
        //First save all the page data as variables.
        final String containerName =
            page.getContainerName();
        final String fileName =
            page.getFileName();
        final InvokatronDocument properties =
            new InvokatronDocument();
        properties.setProperty(
            InvokatronDocument.PACKAGE,
            page2.getPackage());
        properties.setProperty(
            InvokatronDocument.SUPERCLASS,
            page2.getSuperclass());
        properties.setProperty(
            InvokatronDocument.INTERFACES,
            page2.getInterfaces());

        //Now invoke the finish method.
        IRunnableWithProgress op =
            new IRunnableWithProgress() {
            public void run(
                    IProgressMonitor monitor)
                    throws InvocationTargetException {
                try {
                    doFinish(
                        containerName,
                        fileName,
                        properties,
                        monitor);
                } catch (CoreException e) {
                    throw new InvocationTargetException(e);
                } finally {
                    monitor.done();
                }
            }
        };
        try {
            getContainer().run(true, false, op);
        } catch (InterruptedException e) {
            return false;
        } catch (InvocationTargetException e) {
            Throwable realException =
                e.getTargetException();
            MessageDialog.openError(
                getShell(),
                "Error",
                realException.getMessage());
            return false;
        }
        return true;
    }

Here we have a background task to do the saving of the data. The background task is executed by the wizard's container (the Eclipse workbench) and must implement the IRunnableWithProgress interface, containing a sole run() method. The IProgressMonitor that is passed in allows reporting the progress of the task, as we'll see next. The real data-saving work is in a helper method, doFinish():

    private void doFinish(
        String containerName,
        String fileName,
        Properties properties,
        IProgressMonitor monitor)
        throws CoreException {
        // create a sample file
        monitor.beginTask("Creating " + fileName, 2);
        IWorkspaceRoot root = ResourcesPlugin.
            getWorkspace().getRoot();
        IResource resource = root.findMember(
            new Path(containerName));
        if (!resource.exists() ||
            !(resource instanceof IContainer)) {
            throwCoreException("Container \"" +
                containerName +
                "\" does not exist.");
        }
        IContainer container =
            (IContainer)resource;
        final IFile iFile = container.getFile(
            new Path(fileName));
        final File file =
            iFile.getLocation().toFile();
        try {
            OutputStream os =
                new FileOutputStream(file, false);
            properties.store(os, null);
            os.close();
        } catch (IOException e) {
            e.printStackTrace();
            throwCoreException(
                "Error writing to file " +
                file.toString());
        }

        //Make sure the project is refreshed
        //as the file was created outside the
        //Eclipse API.
        container.refreshLocal(
            IResource.DEPTH_INFINITE, monitor);

        monitor.worked(1);

        monitor.setTaskName(
            "Opening file for editing...");
        getShell().getDisplay().asyncExec(
            new Runnable() {
            public void run() {
                IWorkbenchPage page =
                    PlatformUI.getWorkbench().
                        getActiveWorkbenchWindow().
                        getActivePage();
                try {
                    IDE.openEditor(
                        page,
                        iFile,
                        true);
                } catch (PartInitException e) {
                }
            }
        });
        monitor.worked(1);
    }

Here we execute a lot of work:

The last method is a helper method to display error messages in the wizard if the file saving failed:

    private void throwCoreException(
            String message) throws CoreException {
        IStatus status =
            new Status(
                IStatus.ERROR,
                "Invokatron",
                IStatus.OK,
                message,
                null);
        throw new CoreException(status);
    }
}

A CoreException is caught by the wizard, and the Status object it contains is then displayed for the user to see. The wizard won't be closed.

Coding the New Wizard Page

Next, we need to write the InvokatronWizardPage2. This whole class is new:

public class InvokatronWizardPage2 extends WizardPage {
    private Text packageText;
    private Text superclassText;
    private Text interfacesText;

    private ISelection selection;

    public InvokatronWizardPage2(ISelection selection) {
        super("wizardPage2");
        setTitle("Invokatron Wizard");
        setDescription("This wizard creates a new"+
            " file with *.invokatron extension.");
        this.selection = selection;
    }

    private void updateStatus(String message) {
        setErrorMessage(message);
        setPageComplete(message == null);
    }

    public String getPackage() {
        return packageText.getText();
    }
    public String getSuperclass() {
        return superclassText.getText();
    }
    public String getInterfaces() {
        return interfacesText.getText();
    }

The constructor above sets the title of the page (which appears highlighted below the title bar) and the description (which appears below the page title). We also have a few helper methods. updateStatus takes care of displaying page-specific error messages. If there are no error messages, it means the page is completed; therefore, the Next button will become available. There are also getter methods for the data fields' contents. Next is the createControl() method, which builds all of the visual components of the page:

    public void createControl(Composite parent) {
        Composite controls =
            new Composite(parent, SWT.NULL);
        GridLayout layout = new GridLayout();
        controls.setLayout(layout);
        layout.numColumns = 3;
        layout.verticalSpacing = 9;

        Label label =
            new Label(controls, SWT.NULL);
        label.setText("&Package:");

        packageText = new Text(
            controls,
            SWT.BORDER | SWT.SINGLE);
        GridData gd = new GridData(
            GridData.FILL_HORIZONTAL);
        packageText.setLayoutData(gd);
        packageText.addModifyListener(
            new ModifyListener() {
                public void modifyText(
                        ModifyEvent e) {
                    dialogChanged();
                }
        });

        label = new Label(controls, SWT.NULL);
        label.setText("Blank = default package");

        label = new Label(controls, SWT.NULL);
        label.setText("&Superclass:");

        superclassText = new Text(
            controls,
            SWT.BORDER | SWT.SINGLE);
        gd = new GridData(
            GridData.FILL_HORIZONTAL);
        superclassText.setLayoutData(gd);
        superclassText.addModifyListener(
            new ModifyListener() {
                public void modifyText(
                        ModifyEvent e) {
                    dialogChanged();
            }
        });

        label = new Label(controls, SWT.NULL);
        label.setText("Blank = Object");

        label = new Label(controls, SWT.NULL);
        label.setText("&Interfaces:");

        interfacesText = new Text(
            controls,
            SWT.BORDER | SWT.SINGLE);
        gd = new GridData(
            GridData.FILL_HORIZONTAL);
        interfacesText.setLayoutData(gd);
        interfacesText.addModifyListener(
            new ModifyListener() {
                public void modifyText(
                        ModifyEvent e) {
                    dialogChanged();
            }
        });

        label = new Label(controls, SWT.NULL);
        label.setText("Separated by ','");

        dialogChanged();
        setControl(controls);
    }

You need to know SWT in order to write this code. If you don't, at the bottom of this article there are links to places where you can learn it. Basically this method creates labels and fields and places them in a grid layout. Every time a field is changed, its data is validated by a call to dialogChanged():

    private void dialogChanged() {
        String aPackage = getPackage();
        String aSuperclass = getSuperclass();
        String interfaces = getInterfaces();

        String status = new PackageValidator().isValid(aPackage);
        if(status != null) {
            updateStatus(status);
            return;
        }

        status = new SuperclassValidator().isValid(aSuperclass);
        if(status != null) {
            updateStatus(status);
            return;
        }

        status = new InterfacesValidator().isValid(interfaces);
        if(status != null) {
            updateStatus(status);
            return;
        }

        updateStatus(null);
    }

}

This work is done with three utility classes: PackageValidator, SuperclassValidator, and InterfacesValidator. We will write those classes next.

Validation Classes

Validation can be done in any part of the plugin with which the user enters data. Thus, it makes sense to put the validation code in reusable classes, rather than having multiple copies all over the place. The following is an example of a validation class.

public class InterfacesValidator implements ICellEditorValidator
{
    public String isValid(Object value)
    {
        if( !( value instanceof String) )
            return null;

        String interfaces = ((String)value).trim();
        if( interfaces.equals(""))
            return null;

        String[] interfaceArray = interfaces.split(",");
        for (int i = 0; i < interfaceArray.length; i++)
        {
            IStatus status = JavaConventions
                    .validateJavaTypeName(interfaceArray[i]);
            if (status.getCode() != IStatus.OK)
                return "Validation of interface " + interfaceArray[i]
                        + ": " + status.getMessage();
        }
        return null;
    }
}

The other validation classes are very similar to this one--refer to the source code at the end of the article.

Another nifty class in the Eclipse arsenal is JavaConventions, which validates this data for us! It contains many validation methods, such as:

The interface ICellEditorValidator is not needed right now, but we will need it in the next article.

The Result

At this point, we have a working wizard with a graphic and a second page, which creates our initial Invokatron document. Figure 2 shows the result:

Customized Wizard

Figure 2. Customized wizard

Shiny Gizmo

As we've seen, it is data that often drives applications. Presentation is important, too. An ugly gizmo won't sell; a shiny one may. But data is the very nature of what we, programmers, do.

In this article, we first decided on the data we would process. Secondly, we captured visually this data in the form of a customized wizard. The next article will continue on the presentation side, with a customized editor and property page.

Resources

Emmanuel Proulx is an expert in J2EE and Enterprise JavaBeans, and is a certified WebLogic Server 7.0 engineer. He works in the fields of telecommunications and web development.


Return to ONJava.com.

Copyright © 2009 O'Reilly Media, Inc.