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.
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
|
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:
Properties class. This constitutes the "document
class" of our editor.Properties 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);
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:

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:
|
These methods should be overridden to insert initialization and destruction code into our wizard.
init(IWorkbench workbench, IStructuredSelection
editorSelection): Called by Eclipse to provide the wizard
with information about the workbench. Override to keep a handle to
IWorkbench and object for later. If this were an
editor wizard instead of a new wizard, we'd receive as the second
parameter the current editor selection, as well.dispose(): Called by Eclipse to clean up. Override
to clean up resources used by the wizard.finalize(): For cleanup code, prefer using
dispose() instead.These methods are used to decorate the wizard window.
setWindowTitle(String title): Call to provide the
string that appears in title bar.setDefaultPageImageDescriptor(ImageDescriptor
image): Call to provide the graphic that appears in the
top right of the wizard on all pages.setTitleBarColor(RGB color): Call to specify what
color to use in the title bar.These methods control the availability and behavior of the wizard's buttons.
boolean canFinish(): Override to indicate if the
Finish button should be enabled or not, according to the wizard's
state.boolean performFinish(): Override to implement the
wizard's ultimate business logic. Return false if the wizard can't
finish (error condition).boolean performCancel(): Override to clean up
after a user clicks on the Cancel button. Return false if the
wizard can't cancel.boolean isHelpAvailable(): Override to indicate if
the Help button should be visible or not.boolean needsPreviousAndNextButtons(): Override to
specify if the Previous and Next buttons should be visible or
not.boolean needsProgressMonitor(): Override to
indicate if a progress monitor widget should be visible. This
appears when clicking the Finish button while the
performFinish() method is called.These methods control the appearance of pages.
addPages(): Called when the wizard appears.
Override to insert new pages into the wizard.createPageControls(Composite pageContainer):
Called by Eclipse to instantiate all of the wizard's pages (already
added by addPages() above). Override to add
always-visible widgets (other than the pages) to the wizard.IWizardPage getStartingPage(): Override to
determine what should be the first page of the wizard.IWizardPage getNextPage(IWizardPage nextPage): By
default, clicking Next goes to the next page in the array provided
in addPages(). You may want to go to a different page
based on the user's selection. Override to calculate the next
page.IWizardPage getPreviousPage(IWizardPage
previousPage): Similar to getNextPage(), used
to calculate the previous page.int getPageCount(): Retrieves the number of pages
that were added in addPages(). You typically don't
need to override this, except when you want to display the page
count, and it varies.These are useful helper methods:
setDialogSettings(IDialogSettings settings): You
can load the state of the dialog and publish this data by calling
this method in init(). Typically, the settings in
question are the default values for the wizard's fields. See the
class
DialogSettings for more information.IDialogSettings getDialogSettings(): Once the data
is needed, call this method to retrieve it. At the end of the
dialog in performFinish(), you can save the data to
file again.IWizardContainer getContainer(): Useful to
retrieve the Shell, running background threads,
refreshing the window, etc.
|
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:
dispose(): Override to implement cleanup
code.createControl(Composite parent): Override to add
controls to the page.IWizard getWizard(): Used to get the parent wizard
object. Useful to call getDialogSettings().setTitle(String title): Call to provide the string
that appears in the title area of the wizard.setDescription(String description): Call to
provide the text that appears just below the title.setImageDescriptor(ImageDescriptor image): Call to
provide the graphic that appears in the top right of the page
instead of the default image.setMessage(String message): Call to display a text
message below the description string. This is used for warning or
informing the user.setErrorMessage(String error): Call to display a
highlighted text message below the description string. This usually
means the wizard can't continue until an error is fixed.setPageComplete(boolean complete): If this is
true, the Next button will be available.performHelp(): Override to provide
context-sensitive help. This is called by the wizard when the Help
button is pressed.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:
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:
IFile
class) of the file we want to save.File equivalent of this.IProgressMonitor object
passed as a parameter.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.
|
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 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:
validateJavaTypeName() to check class and
interface names.validatePackageName() to check package names.validateFieldName() to check data member
names.validateMethodName() to check method names.validateIdentifierName() to check variable
names.The interface ICellEditorValidator is not needed
right now, but we will need it in the next article.
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:

Figure 2. Customized wizard
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.
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 © 2007 O'Reilly Media, Inc.