Skip to main content

Working XML: Layout, properties, and preferences in Eclipse

User interface gimmicks

Benoit Marchal (bmarchal@pineapplesoft.com), Consultant, Pineapplesoft
Benoît Marchal is a Belgian consultant. He is the author of XML by Example and other XML books. Benoît is available to help you with XML projects -- you can contact him at bmarchal@pineapplesoft.com.

Summary:  Originally developed here in the Working XML column on developerWorks, XM is a simple publishing framework that uses XML and XSL. In this installment, Benoît Marchal looks more directly at user interface considerations, including managing properties and preferences in Eclipse for his XSL publishing plug-in. Share your thoughts on this article with the author and other readers in the accompanying discussion forum.

View more content in this series

Date:  16 Apr 2003
Level:  Intermediate
Activity:  1580 views
Comments:  

A few columns ago, I started building a user interface for XM. I chose to build the user interface over Eclipse, an integrated development environment (IDE) donated to the open source community by IBM. It's designed as a basic framework that tool developers can extend through plug-ins. My aim in this series is to write such a plug-in for XSL.

One of the most interesting aspects of Eclipse is that it is possible (and easy) for you to customize your working environment by combining the best plug-ins in their categories. For example, a clear synergy exists between XM and XML editors (see the "Black Sun" site in Resources). Furthermore, as this column demonstrates, if your tool of choice is not currently supported, it is not too difficult to add a plug-in.

User interface considerations

User interface is an ongoing concern with the plug-in. After all, the plug-in aims to offer a more graphical interface than the original command-line interface that XM has had for a long time.

I chose to work with Eclipse based on the strength of its user interface. Eclipse has its own GUI library -- the standard widget library (SWT) -- which provides a native alternative to Swing. I have used Swing-based products extensively and always found them to be inferior to native applications. Somehow, Swing emulation of native controls just does not feel right. SWT uses the platform's controls, and the result is much more pleasant.

Nevertheless, there is surprisingly little code in the plug-in to manage user interface. The plug-in mostly registers invisible services, such as the builder discussed in my previous column, "Building a project with Eclipse and XM" (see Resources for a link to the article). This column looks at preferences and properties, which gives me a chance to explore SWT.

I encourage you to download the code for this project (see Resources) and review it as you read this article.


SWT layouts

SWT has all the controls you would expect from a modern library, such as entry fields, lists, and buttons. But it also has trees, toolbars, tables, and more. Whenever possible, SWT uses native controls, so -- at long last -- virtually no differences exist between a Java application and a native application.

AWT, Swing, and SWT share many similarities. The libraries differ mostly in their implementations (the Java platform as opposed to native platform) and their naming conventions. For SWT, a control is a widget. Widgets are similar to components in the JDK. On screen, widgets are arranged on composites (panels, windows, and so on). SWT composites are similar to containers in the JDK.

I won't spend too much time in this column on the specifics of SWT. Suffice it to say that the widgets are in the org.eclipse.swt.widgets package. The class names are self-explanatory, so you should have no problem navigating through the documentation.

Instead, I will concentrate on the layouts, which differ from their AWT and Swing counterparts. Listing 1 is an excerpt from the XMProjectPropertiesPage class. I'll cover this class in detail in Preferences and properties, but for now let's concentrate on the buildGUI() method.


Listing 1. The buildGUI() method
private Control buildUI(Composite parent)
{
   Composite composite = new Composite(parent,SWT.NULL);
   RowLayout rowLayout = new RowLayout();
   rowLayout.type = SWT.VERTICAL;
   rowLayout.wrap = false;
   composite.setLayout(rowLayout);
   hasNature = new Button(composite,SWT.CHECK);
   hasNature.setText(Resources.getString("eclipse.hasnature"));
   hasNature.addSelectionListener(new SelectionListener()
   {
      public void widgetDefaultSelected(SelectionEvent e)
      {
         group.setEnabled(hasNature.getSelection());
      }
         
      public void widgetSelected(SelectionEvent e)
      {
         group.setEnabled(hasNature.getSelection());
      }
   });
   group = new Group(composite,SWT.NONE);
   group.setText(Resources.getString("eclipse.properties"));
   GridLayout gridLayout = new GridLayout();
   gridLayout.numColumns = 2;
   group.setLayout(gridLayout);
   Label label = new Label(group,SWT.RIGHT);
   label.setText(Resources.getString("eclipse.src"));
   srcText = new Text(group,SWT.SINGLE);
   GridData gridData = new GridData();
   gridData.widthHint = 150;
   srcText.setLayoutData(gridData);
   label = new Label(group,SWT.RIGHT);
   label.setText(Resources.getString("eclipse.rules"));
   rulesText = new Text(group,SWT.LEFT);
   gridData = new GridData();
   gridData.horizontalAlignment = GridData.FILL;
   gridData.grabExcessHorizontalSpace = true;
   rulesText.setLayoutData(gridData);
   label = new Label(group,SWT.RIGHT);
   label.setText(Resources.getString("eclipse.publish"));
   publishText = new Text(group,SWT.LEFT);
   gridData = new GridData();
   publishText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL
                                          | GridData.GRAB_HORIZONTAL));
   isBuild = new Button(group,SWT.CHECK);
   isBuild.setText(Resources.getString("eclipse.build"));
   gridData = new GridData();
   gridData.horizontalSpan = 2;
   isBuild.setLayoutData(gridData);
   return composite;
}

Like AWT and Swing, SWT uses layout to control the size and position of widgets on a composite. SWT layouts are almost identical to JDK layouts, but they use different classes -- again, similar concepts with different implementations.

SWT proposes four layouts in the org.eclipse.swt.layout package:

  • FillLayout: Renders the widgets in a vertical or horizontal row. It is similar to BoxLayout in the JDK.

  • RowLayout: Renders the widgets over one or more rows. More specifically, it wraps widgets at the end of the row. RowLayout is similar to FlowLayout from the JDK.

  • GridLayout: A more powerful layout. It is close to the GridBagLayout from the JDK. GridLayout renders widgets in a grid and offers many options to control spacing, margin, alignment, and more. A widget can span several cells in the grid.

  • FormLayout: Unique to SWT (at least I don't know of a JDK equivalent). FormLayout is a special grid that groups data entry fields and their labels.

Listing 1 uses both RowLayout and GridLayout. Figure 1 shows the form that Listing 1 creates. It contains a checkbox and a group of four other widgets. RowLayout aligns the checkbox and the group. FillLayout is not an option because the checkbox and the group have different sizes (FillLayout insists that all its widgets have the same size).


Figure 1. The form created by Listing 1
Eclipse navigator

Initializing the RowLayout is as easy as setting a few properties: type toggles between vertical and horizontal rows, while wrap controls whether the layout can create more than one row. You then assign the layout to a composite with the setLayout() method.

The group is a composite that draws a border around its widgets. In Listing 1, the group uses a GridLayout class. To initialize GridLayout, you simply set the number of columns. Note that GridBagLayout specifies the number of rows indirectly through constants such as REMAINDER and RELATIVE.

To specify the properties of each cell individually, GridLayout uses a GridData class. GridData is similar to GridBagConstraints from the JDK, but the property names are different. Note a major difference between SWT and the JDK: With SWT you set GridData on the widget (with the setLayoutData() method), not on the layout.

Listing 1 illustrates another difference between SWT and the JDK: Composites lack the add() method from containers. Instead, widgets take a reference to their parent in their constructors.

Although there are differences between the two APIs, it will be easy for you to learn SWT if you already have experience with AWT or Swing.


Preferences and properties

Preferences and properties are closely related. Both control the configuration and options in the environment. Preferences are intended for configuration options that apply to the environment in every project. A typical example would be colors used for syntax highlighting with an editor.

Properties are attached to resources, in the Eclipse sense of the term (see the Resources for a link to "Creating a project" which includes a discussion of Eclipse resources). For example, properties may control how a given project is built.

The XM plug-in will need both preferences and properties. Our project already uses properties to set the source, publish, and rules directories, as well as to control whether to build or make the project. Those properties were derived from the old command-line interface. In "Creating a project," I introduced a wizard that initializes the properties, but so far there hasn't been any interface gimmick to modify them.

Since I started working with project natures (see Resources for links to the previous articles), I have been searching for an option to assign the XM nature to any project. Eventually, I came to the conclusion that the nature could be treated as a special property.

As for the preferences, the console needs a mechanism to filter messages. When working on large projects, a rebuild generates dozens of messages. It is difficult to separate errors from progress messages, so I wanted to give the user an option to control which messages are displayed.

Project properties

Let's start with the project properties. The implementation is spread over two classes: ProjectProperties and XMProjectPropertiesPage. ProjectProperties is a utility class. It is not required by Eclipse, but I find it convenient. ProjectProperties takes care of saving and reading the properties from the Eclipse-supplied IProject. By isolating this code in one class, it is easier to make sure that properties (and in particular default values) are treated coherently throughout the plug-in. (See Resources for information on downloading the code for this class.)

XMProjectPropertiesPage is specific to Eclipse. It adds a page to the navigator's properties dialog, which you can access by right-clicking on a resource in the navigator. XMProjectPropertiesPage attaches to the org.eclipse.ui.propertyPages extension point.

Listing 2 is an excerpt from plugin.xml, the plug-in manifest that declares the extension. Pay special attention to the objectClass attribute, which specifies which object the property applies to. Any object that the navigator recognizes is a potential candidate for this field, from the generic IResource to more specific elements like IProject or IFolder. To be even more specific, use the <filter> tag that was introduced with the pop-up menu.


Listing 2. An excerpt from plugin.xml
<extension point="org.eclipse.ui.propertyPages">
   <page id="XMProjectProperties"
         name="XM Properties"
         objectClass="org.eclipse.core.resources.IProject"
         class="org.ananas.xm.eclipse.XMProjectPropertiesPage">
   </page>
</extension>

In Listing 2, objectClass is set to an IProject object. This means that the property page is visible if the user clicks on a project. Files and directories, however, won't show this property page.

XMProjectPropertiesPage inherits from PropertyPage and overrides createContents(), shown in Listing 3. The createContents() method initializes the property page by calling buildUI() (which I introduced earlier in SWT layouts) and reading the properties with the help of a ProjectProperties object.


Listing 3. The createContents() method
public Control createContents(Composite parent)
{
   Control control = buildUI(parent);
   try
   {
      IAdaptable adaptable = getElement();
      if(adaptable instanceof IProject)
      {
         properties = new ProjectProperties((IProject)adaptable);
         readProperties();
      }
   }
   catch(CoreException x)
   {
      ErrorDialog.openError(
         PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(),
         Resources.getString("eclipse.dialogtitle"),
         Resources.getString("eclipse.propertyerror"),
         PluginTools.makeStatus(x));
   }
   return control;
}

PropertyPage also defines callback methods when the user presses the OK, Cancel, Apply, or Default buttons. XMProjectPropertiesPage overrides some of these methods to save the new property values.

Project nature as a property

Strictly speaking, the project nature is not a property. As I've discussed before, Eclipse uses the project nature to decide which plug-ins are in charge of a project. Among other things, the project nature controls entries in the menus, builder, and more. Yet the property page includes a checkbox for the nature.

I treat the project as a property because I want to give users a chance to add XM to any existing projects. The rationale is that Eclipse lets you work on radically different projects and languages from a familiar environment. You can write Java code in the morning, PHP code in the early afternoon, and updates to a Web site with XM in late afternoon.

In real life, a project often combines two or more such tools. For example, at Pineapplesoft, most of our Web sites combine XSL (through XM) and PHP. It seems logical to have one project with a dual nature. From a user standpoint, enabling the XM nature on a project and setting the project properties happen at the same time, so it seems more logical to offer all these options on the same screen.

When testing this feature, I realized that I had not implemented the project nature properly. It turns out you have to declare the project nature as an extension point. The extension point must implement the IProjectNature interface and provide methods to configure and deconfigure the project.

The extension point is in the XMProjectNature class, shown in Listing 4. The framework calls the configure() method when the nature is added to a project. It calls the deconfigure() method when the nature is removed. The configure() method should make sure that the nature works properly, which involves registering the appropriate builders. The deconfigure() method does the opposite. The code in configure() originally appeared in the project wizard; I only had to move it to its new home.


Listing 4. The XMProjectNature class
package org.ananas.xm.eclipse;

import org.eclipse.core.runtime.*;
import org.eclipse.core.resources.*;

public class XMProjectNature
   implements IProjectNature, PluginConstants
{
   private IProject project;
   
   public void configure()
      throws CoreException
   {
      IProjectDescription description = project.getDescription();
      if(!hasBuildSpec(description.getBuildSpec()))
      {
         ICommand[] old = description.getBuildSpec(),
                    specs = new ICommand[old.length + 1];
         System.arraycopy(old,0,specs,0,old.length);
         ICommand command = description.newCommand();
         command.setBuilderName(BUILDER_ID);
         specs[old.length] = command;
         description.setBuildSpec(specs);
         project.setDescription(description,new NullProgressMonitor());
      }
   }

   public void deconfigure()
      throws CoreException
   {
      IProjectDescription description = project.getDescription();
      int count = getBuildSpecCount(description.getBuildSpec());
      if(count != 0)
      {
         ICommand[] old = description.getBuildSpec(),
                    specs = new ICommand[old.length - count];
         int i = 0,
             j = 0;
         while(i < old.length)
         {
            if(!old[i].getBuilderName().equals(BUILDER_ID))
               specs[i] = old[j++];
            i++;
         }
         description.setBuildSpec(specs);
         project.setDescription(description,new NullProgressMonitor());
      }
   }
   
   public IProject getProject()
   {
      return project;
   }
   
   public void setProject(IProject project)
   {
      this.project = project;
   }

   private boolean hasBuildSpec(ICommand[] commands)
   {
      return getBuildSpecCount(commands) != 0;
   }
   
   private int getBuildSpecCount(ICommand[] commands)
   {
      int count = 0;
      for(int i = 0;i < commands.length;i++)
         if(commands[i].getBuilderName().equals(BUILDER_ID))
            count++;
      return count;
   }
}

Listing 5 is the declaration in the plug-in manifest.


Listing 5. Another excerpt from plugin.xml
<extension point="org.eclipse.core.resources.natures"
           id="xmnature"
           name="XM Nature">
   <runtime>
      <run class="org.ananas.xm.eclipse.XMProjectNature"/>
   </runtime>
</extension>

XM preferences

Preferences are similar to properties, but they are associated with a plug-in instead of a resource.

Unfortunately, Eclipse stores preferences differently than properties. For preferences, Eclipse uses a preference store. The store must be initialized with default values for every preference (and, indeed, the store only records a preference if its value is not the default).

The store is initialized in the plug-in class, a class that initializes objects global to the plug-in. Most plug-ins have a plug-in class (and if you use the Eclipse wizard to generate a new plug-in, it automatically has one), but somehow XM managed to get by without one.

You declare the plug-in class by adding a class attribute to the <plugin> tag in the manifest, as shown here:

<plugin class="org.ananas.xm.eclipse.XMPlugin">

For this project, the plug-in class is XMPlugin. XMPlugin extends AbstractUIPlugin. The framework creates an instance of the plug-in class when it loads the plug-in, giving it a chance to initialize global objects.

It is essential that the plug-in does not attempt to instantiate the plug-in class itself. Memory should never contain more than one instance of the plug-in class. To facilitate this, the plug-in class implements the singleton pattern: It stores a reference to itself in a private class variable. Access to this shared instance is provided through the getDefault() method.

The most useful method for us is initializeDefaultPreferences(), which initializes default values in the preference store, as shown here:

protected void initializeDefaultPreferences(IPreferenceStore store)
{
   store.setDefault(LEVEL_PREFERENCE_NAME,LEVEL_ALL);
}

The preference page, where the user can change preference values, is implemented in XMPreferencesPage, as shown in Listing 6. A preference page is similar to a property page, but Eclipse provides the FieldEditorPreferencePage class, a special class that simplifies preference pages even more.


Listing 6. The XMPreferencesPage class
package org.ananas.xm.eclipse;

import org.ananas.xm.*;
import org.eclipse.ui.*;
import org.eclipse.jface.preference.*;

public class XMPreferencesPage
   extends FieldEditorPreferencePage
   implements PluginConstants, IWorkbenchPreferencePage 
{
   public XMPreferencesPage()
   {
      super(GRID);          
      IPreferenceStore store =
         XMPlugin.getDefault().getPreferenceStore();
      setPreferenceStore(store);       
   }
   
   protected void createFieldEditors() 
   {
      RadioGroupFieldEditor levelEditor =
         new RadioGroupFieldEditor(
            LEVEL_PREFERENCE_NAME,
            Resources.getString("eclipse.level"),
            1,
            new String[][]
            {
               { Resources.getString("eclipse.level.all"), LEVEL_ALL },
               { Resources.getString("eclipse.level.info"), LEVEL_INFO },
               { Resources.getString("eclipse.level.warning"), LEVEL_WARNING },
               { Resources.getString("eclipse.level.error"), LEVEL_ERROR },
            },
            getFieldEditorParent());
      addField(levelEditor);                             
   }

   public void init(IWorkbench workbench)
   {
   }
}

FieldEditorPreferencePage works with field editors instead of regular widgets. A field editor is simply a widget that's associated with a preference. Simply instantiate the field editors in the createFieldEditors() method and the framework takes care of loading and saving the preferences.

Finally, I updated the console to read the preferences and display the messages accordingly. As Listing 7 illustrates, the console retrieves the preference store directly from the plug-in class. The console also registers a property change listener. The property store notifies listeners when a preference changes (for instance, through the preference page).


Listing 7. The initializeDefaultPreferences() method
IPreferenceStore store = XMPlugin.getDefault().getPreferenceStore();
store.addPropertyChangeListener(new IPropertyChangeListener()
{
   public void propertyChange(PropertyChangeEvent e)
   {
      setLevel((String)e.getNewValue());
   }
});
setLevel(store.getString(LEVEL_PREFERENCE_NAME));


Conclusion

I have already raved over Eclipse in my previous columns, but this environment continues to excite me. It is a powerful and flexible framework. Furthermore, it is very accessible for tool developers. As this column has demonstrated several times, it's easy to enhance Eclipse. At Pineapplesoft, we now use Eclipse and XM for all of our Web publishing projects.


Resources

About the author

Benoit Marchal

Benoît Marchal is a Belgian consultant. He is the author of XML by Example and other XML books. Benoît is available to help you with XML projects -- you can contact him at bmarchal@pineapplesoft.com.

Comments



Trademarks  |  My developerWorks terms and conditions

Help: Update or add to My dW interests

What's this?

This little timesaver lets you update your My developerWorks profile with just one click! The general subject of this content (AIX and UNIX, Information Management, Lotus, Rational, Tivoli, WebSphere, Java, Linux, Open source, SOA and Web services, Web development, or XML) will be added to the interests section of your profile, if it's not there already. You only need to be logged in to My developerWorks.

And what's the point of adding your interests to your profile? That's how you find other users with the same interests as yours, and see what they're reading and contributing to the community. Your interests also help us recommend relevant developerWorks content to you.

View your My developerWorks profile

Return from help

Help: Remove from My dW interests

What's this?

Removing this interest does not alter your profile, but rather removes this piece of content from a list of all content for which you've indicated interest. In a future enhancement to My developerWorks, you'll be able to see a record of that content.

View your My developerWorks profile

Return from help

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=XML
ArticleID=12255
ArticleTitle=Working XML: Layout, properties, and preferences in Eclipse
publish-date=04162003
author1-email=bmarchal@pineapplesoft.com
author1-email-cc=dwxed@us.ibm.com

My developerWorks community

Tags

Help
Use the search field to find all types of content in My developerWorks with that tag.

Use the slider bar to see more or fewer tags.

Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere).

My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Use the search field to find all types of content in My developerWorks with that tag. Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere). My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Rate a product. Write a review.

Special offers