Copyright © 2003 Berthold Daum.
 Eclipse Corner Article

 

Mutatis mutandis - Using Preference Pages as Property Pages

Summary
A common problem in the implementation of applications is the implementation of project-specific properties that override workbench-wide preferences on project or file level. The naive approach is to implement these pages from scratch. However, writing the same code twice is a boring task and leads to increased maintenance efforts. In this article we show how existing preferences pages (with or without field editors) can be easily converted into pages that can act as both preference and property pages. We demonstrate this by implementing the abstract class FieldEditorOverlayPage providing the necessary functionality.

Berthold Daum, bdaum industrial communications
October 24, 2003


The problem

Implementing Preference Pages for Eclipse plug-ins is not a difficult task, especially if you use field editors. Such preference pages are usually implemented as subclasses of org.eclipse.jface.preference.PreferencePage and org.eclipse.jface.preference.FieldEditorPreferencePage. There are already two excellent articles on Eclipse Corner dealing with this subject: Preferences in the Eclipse Workbench UI by Tod Creasey and Simplifying Preference Pages with Field Editors by Ryan Cooper. In this article I will not go into details with the implementation of preference pages but assume that you are already fairly familiar with standard implementation techniques of such pages.

The problem that I want to discuss here begins after you already have implemented preference pages for your plug-in. At some stage you notice that some or all of your plug-in preferences should rather be project specific or even file specific. The usual way to implement such project or file specific settings are Property Pages, which usually are subclasses of org.eclipse.ui.dialogs.PropertyPage. The Eclipse workbench already contains some examples of how property pages are used to override workbench settings on project level. One example are the Java Compiler settings. These settings can be controlled in the workbench preferences but can be overridden for each Java project separately by project specific property pages. 

The Java compiler settings in the workbench preferences

 

The Java compiler settings as a property page of a Java project

As we see, both preference page and property page look, in fact, very similar. There are some differences, however, at the top of the page. Instead of the page description, the property page contains two radio buttons for toggling between workbench settings and project settings. It also features a button for direct access to the corresponding workbench preference page. But its main body (in this case the tabbed notebook) is identical with the main body of the corresponding preference page.

Possible solutions

An obvious solution to this problem is to implement both pages from scratch. However, since the main body of the both pages is identical, this would result in a large portion of duplicated code, code that is subject to later maintenance and thus increases maintenance costs. After all, object-oriented programming is about reuse, isn't it?

Well, why don't we factor out the main body of both pages into a separate class? This is actually the way in which both pages shown above are implemented. (The tabbed notebook is implemented in JDT class CompilerConfigurationBlock.) But this technique is not trivial. What I did not tell you is that preference pages and property pages are based on different data models. Workbench preferences are usually stored in plug-in specific but workbench-wide preference stores. Properties, in contrast, are stored as resource related properties. The interface org.eclipse.core.resources.IResource provides the necessary access method. When a resource is deleted, also its properties cease to exist. This different way of data management would require us to implement two variants of access routines, if we want to use preference pages in the role of property pages. For property pages we would need to direct all field accesses to the respective resource (project or file), for preference pages we would need to direct all field accesses to the plug-in specific preference store. Unfortunately, this strategy is not possible for preference pages utilizing field editors. Field editors internally always access a preference store - we cannot tell them to access resource properties instead. So we would need to re-implement the whole property page - replacing the field editors with standard SWT widgets! Not very nice.

Fortunately, there is another solution and it works for field editors, too. To get to this solution we have to look at the inheritance trees of both field editor preference pages and property pages. (We will deal with the problem of field editor preferences pages first and will discuss "normal" preference pages later.)

Both types are extensions of the abstract class PreferencePage. The class FieldEditorPreferencePage implements, in addition, the interface IPropertyChangeListener, while the class PropertyPage additionally implements the interface IWorkbenchPropertyPage. Since the extension from PreferencePage to PropertyPage is minimal (the implementation of IWorkbenchPropertyPage is trivial), it makes sense to base our solution on class FieldEditorPreferencePage and simply add the implementation of IWorkbenchPropertyPage.

But how do we deal with the problem of different data models for property pages and preference pages? Since field editors can only access preference stores, we simply create a temporary preference store (which we call overlay store within this article) when a property page is initialized. We implement this store by extending class PreferenceStore but provide different semantics to its access methods. In particular, when fetching a value from the store, we look into the resource properties first if we can find an identically named value there. Only if such a value is not found, we refer to the underlying workbench preference store. The save() methods of this store have different semantics, too: they must store the values contained in the overlay store into the resource properties instead of saving them as preferences. So, our field editors have only to deal with a preference store and are perfectly happy.

Implementing class PropertyStore

The class PropertyStore implements the overlay store and extends the class PreferenceStore. Each instance of PropertyStore represents the properties of a specific resource in context of a specific property page but as an incarnation of a property store. Because the store is resource and page specific, we pass the resource and a page identification in the constructor:

	public class PropertyStore extends PreferenceStore {
	  private IResource resource;
	  private IPreferenceStore workbenchStore;
	  private String pageId;
	  public PropertyStore(IResource resource, 
                               IPreferenceStore workbenchStore, 
                               String pageId) {
	    this.resource = resource;
	    this.workbenchStore = workbenchStore;
	    this.pageId = pageId;
	}

We also pass the underlying workbench preference store. When a preference value is not available as a resource property value, we will route accesses through to this store.

Getters and Setters

Now, let's first discuss the get...() accessors. The class PreferenceStore has an awful lot of them, one for each primitive Java type and one for java.lang.String. Here, we show only the accessor for type String but leave the others to your imagination. Let's start with the accessor for the default values:

	public String getDefaultString(String name) {
	  return workbenchStore.getDefaultString(name);
	}

Well, that was easy enough. Because we don't store any default values in PropertyStore we just fetch the default values from the workbench preference store. Next are the accessors for non-default values:

	public String getString(String name) {
	  insertValue(name);
	  return super.getString(name);
	}

That seems easy enough, too, but what's in insertValue() ?

	private boolean inserting = false;
	private synchronized void insertValue(String name) {
	  if (inserting)
	    return;
	  if (super.contains(name))
	    return;
	  inserting = true;
	  String prop = null;
	  try {
	    prop = getProperty(name);
	  } catch (CoreException e) {
	  }
	  if (prop == null)
	    prop = workbenchStore.getString(name);
	  if (prop != null)
	    setValue(name, prop);
	  inserting = false;
	}
	private String getProperty(String name) 
                              throws CoreException {
	  return resource.getPersistentProperty(
                          new QualifiedName(pageId, name));
	}

Not easy at all! It starts with a synchronized method and a semaphore (inserting). This is necessary to avoid recursions. Method calls inside the insertValue() body internally call getString() which would lead to unlimited recursion and a stack overflow. The semaphore inhibits just this. To make this logic thread safe we have declared this method as synchronized.

After we have made sure that this is not a recursive call we first look if the value is already stored in the local store instance. If yes, we do nothing. If not, we try to read a property with the same name. We always qualify the names of properties with the page identification to avoid name clashes between properties from different property pages. If such a property value does not exist, we get the value from the workbench preference store. Then we cache this value in the local store. Sooner or later, the value of all fields in a given page will end up in this local core: all field editors will call a get...() method in order to display a value on the page. 

What about the corresponding set...() methods? Fortunately, we don't have to override these methods because all changes are applied directly to the local store. However, a few other PreferenceStore methods need consideration.

More methods

In particular, we have to modify the methods contains(), isDefault() and setToDefault(). Let's start with method contains():

	public boolean contains(String name) {
	  return workbenchStore.contains(name);
	}

Here, we just delegate to the workbench preference store assuming that the property page is an exact replica of the preference page.

Default values need a bit more attention. Above, we had simply delegated getDefault...() calls to the workbench preference store. But what if we want to reset a property to its default value? Standard preference stores internally contain two sets of properties: one for the default values and one for the non-default values. The second set only contains a value if it is unequal to the default value. So, when a value is reset to its default value, it is sufficient to remove it from the set of non-default values. In our case, however, we need a different implementation. We must explicitly store property values that equal the default value. So we are able to set resource level properties to their default while on workbench level they still have a non-default value:

	public void setToDefault(String name) {
	    setValue(name, getDefaultString(name));
	}

This has implications for the method isDefault(). The standard implementation just tests if the specified property name is contained in the set of non-default values. In our case, however, this is not sufficient because this value set may contain default values, too. We must therefore explicitly compare non-default values with default values:

	public boolean isDefault(String name) {
	  String defaultValue = getDefaultString(name);
	  if (defaultValue == null) return false;
	  return defaultValue.equals(getString(name));
	}
Updating Resource Properties

What remains to do is overriding both save() methods of class PreferenceStore. In contrast to the standard save operation we write all cached values to the resource properties:

	public void save() throws IOException {
	  writeProperties();
	}
	public void save(OutputStream out, String header) 
                                         throws IOException {
	  writeProperties();
	}
	private void writeProperties() throws IOException {
	  String[] preferences = super.preferenceNames();
	  for (int i = 0; i < preferences.length; i++) {
	    String name = preferences[i];
	    try {
	      setProperty(name, getString(name));
	    } catch (CoreException e) {
	      throw new IOException(
                    "Cannot write resource property " + name);
	    }
	  }
	}
	private void setProperty(String name, String value) 
                                         throws CoreException {
	  resource.setPersistentProperty(
                       new QualifiedName(pageId, name), value);
	}

Here, we first get the names of all values in the local preference store. Then we write each of them into the resource properties as shown above. Again, we need to qualify the property name with the page identification.

We are now done with the implementation of class PropertyStore and turn our attention to GUI issues.

Implementing class FieldEditorOverlayPage

The class FieldEditorOverlayPage can act as a common superclass for all field editor preference pages that also want to behave as property pages.

A place in the type hierarchy

As we have already discussed above this class extends class FieldEditorPreferencePage and additionally implements IWorkbenchPropertyPage. We replicate all constructors of class FieldEditorPreferencePage but save the image passed in the third constructor for later use:

	public abstract class FieldEditorOverlayPage
	                extends FieldEditorPreferencePage
                     implements IWorkbenchPropertyPage
	public FieldEditorOverlayPage(int style) {
	  super(style);
	}
	public FieldEditorOverlayPage(String title, int style) {
	  super(title, style);
	}
	private ImageDescriptor image;
	public FieldEditorOverlayPage(String title, 
                                      ImageDescriptor image, 
                                      int style) {
	  super(title, image, style);
	  this.image = image;
	}

The implementation of interface IWorkbenchPropertyPage is trivial, indeed:

	private IAdaptable element;
	public void setElement(IAdaptable element) {
	  this.element = element;
      	}
   	public IAdaptable getElement() {
	  return element;
      	}

The method setElement() is called when a property page is opened. In our case, when the property page is called on a project, folder, or file, the IAdaptable that is passed as an argument is actually of type IResource. We simply store this element in an instance variable. Obviously, when this variable is not null, the current instance of FieldEditorOverlayPage represents a property page, otherwise a preference page. We express this fact in the following method:

	public boolean isPropertyPage() {
	  return element != null;
	}

We can use this method, for example, in subclasses that want to vary the page content depending on if it is a property page or a preference page.

Additional GUI elements

Also the GUI elements of class FieldEditorOverlayPage are variable. If the page instance represents a property page, we want the button group for project/workbench selection at the top. If it is a preference page, we don't. Depending on the state of these buttons we must enable or disable the field editors, too.

Let's start with this button group. To add these buttons to a property page we extend the method createContents():

	public static final String USEPROJECTSETTINGS = 
                                     "useProjectSettings";
	private Button useWorkspaceSettingsButton, 
	               useProjectSettingsButton,
	               configureButton;
	protected Control createContents(Composite parent) {
	  if (isPropertyPage())
	    createSelectionGroup(parent);
	  return super.createContents(parent);
	}
	private void createSelectionGroup(Composite parent) {
	  Composite comp = new Composite(parent, SWT.NONE);
	  GridLayout layout = new GridLayout(2, false);
	  layout.marginHeight = 0;
	  layout.marginWidth = 0;
	  comp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
	  Composite radioGroup = new Composite(comp, SWT.NONE);
	  radioGroup.setLayout(new GridLayout());
	  radioGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
	  useWorkspaceSettingsButton =
	           createRadioButton(radioGroup, "Use workspace settings");
	  useProjectSettingsButton =
	           createRadioButton(radioGroup, "Use project settings");
	  configureButton = new Button(comp, SWT.PUSH);
	  configureButton.setText("Configure Workspace Settings ...");
	  configureButton.addSelectionListener(new SelectionAdapter() {
	    public void widgetSelected(SelectionEvent e) {
	      configureWorkspaceSettings();
	    }
	  });
	  try {
	    String use = ((IResource) element).getPersistentProperty(
	                   new QualifiedName(pageId, USEPROJECTSETTINGS));
	    if ("true".equals(use)) {
	      useProjectSettingsButton.setSelection(true);
	      configureButton.setEnabled(false);
	    } else
	      useWorkspaceSettingsButton.setSelection(true);
	  } catch (CoreException e) {
	    useWorkspaceSettingsButton.setSelection(true);
	  }
	}

This code is straightforward. If the current instance is a property page, we create a group of two radio buttons and a push button. When this button is pressed, the method configureWorkspaceSettings() is called for configuring the corresponding workspace settings. (We will discuss this method later.) 

After the controls have been created they are initialized. To do so we fetch the resource property USEPROJECTSETTINGS. Because this setting may be different for each single property page, we qualify the name of this property with the page identification.

Both radio buttons are created via the convenience method createRadioButton():

	private Button createRadioButton(Composite parent, String label) {
	  final Button button = new Button(parent, SWT.RADIO);
	  button.setText(label);
	  button.addSelectionListener(new SelectionAdapter() {
	    public void widgetSelected(SelectionEvent e) {
	      configureButton.setEnabled(button == useWorkspaceSettingsButton);
	      updateFieldEditors();
	    }
	  });
	  return button;
	}

When a radio button is pressed, we enable or disable the button for the workbench configuration (enabled when we use the workbench settings), and we enable or disable the field editors (disabled when the workbench settings are activated). 

However, this confronts us with a problem. We have to know the page's field editors. Our parent class FieldEditorPreferencePage has this knowledge, but unfortunately it keeps this knowledge close to itself. We must therefore keep track of the field editor ourselves. To do so, we override the method addField() and add each field editor added to the page to a list:

	private List editors = new ArrayList();
	protected void addField(FieldEditor editor) {
	  editors.add(editor);
	  super.addField(editor);
	}

Now, we can implement the method updateFieldEditors():

	private void updateFieldEditors() {
	  boolean enabled = useProjectSettingsButton.getSelection();
	  updateFieldEditors(enabled);
	}
	protected void updateFieldEditors(boolean enabled) {
	  Composite parent = getFieldEditorParent();
	  Iterator it = editors.iterator();
	  while (it.hasNext()) {
	    FieldEditor editor = (FieldEditor) it.next();
	    editor.setEnabled(enabled, parent);
	  }
	}

We simply iterate through the list of field editors and tell each field editor if it is enabled or not. Subclasses may override this method if special treatment is required. This may be the case when the enablement of field editors depends on the state of other field editors or on the current state of the application.

Modifying the access layer

We have nearly completed the GUI part of class FieldEditorOverlayPage. We only must enable or disable the field editors appropriately after they have been created. The best place to do this is the method createControl() where we call updateFieldEditors() after the complete page content has been created:

	private IPreferenceStore overlayStore;
	private String pageId;
	public void createControl(Composite parent) {
	  if (isPropertyPage()) {
	    pageId = getPageId();
	    overlayStore = new PropertyStore((IResource) getElement(), 
                                             super.getPreferenceStore(), 
                                             pageId);
	  }
	  super.createControl(parent);
	  if (isPropertyPage())
	    updateFieldEditors();
	}
	protected abstract String getPageId();

The method createControl() is also the best place to create an instance of class PropertyStore (see above). This instance will act as our local overlay store. This PropertyStore instance is supplied with a pageId used for qualifying property names. We obtain this value via the method getPageId(). As this method is abstract, subclasses of FieldEditorOverlayPage are required to implement it.

What remains to do is to inform clients about our overlay store. We do this by overriding the method getPreferenceStore():

	public IPreferenceStore getPreferenceStore() {
	  if (isPropertyPage())
	    return overlayStore;
	  return super.getPreferenceStore();
	}

If the page is a property page we return the overlay store, otherwise we just return the standard preference store. All clients that can work with preference stores (such as field editors) will be happy.

Handling button events

The button group from above, needs some special treatment when the OK button or the Restore Defaults button is pressed. When the OK button is pressed, we need to save the selection state of the radio buttons, and when the Restore Defaults button is pressed, we need to set the selection state of these buttons:

To implement this OK button behavior, we override the method performOk(). Pay close attention here because things are getting a bit tricky:

	public boolean performOk() {
	  boolean result = super.performOk();
	  if (result && isPropertyPage()) {
	    IResource resource = (IResource) element;
	    try {
	      String value = (useProjectSettingsButton.getSelection()) ? 
                             TRUE : FALSE;
	      resource.setPersistentProperty(
                    new QualifiedName(pageId, USEPROJECTSETTINGS), value);
            } catch (CoreException e) {
            }
	  }
	  return result;
	}

In method performOk() we first execute the performOk() method of the super class, then save the state of the radio buttons into a resource property.

	protected void performDefaults() {
	  if (isPropertyPage()) {
	    useWorkspaceSettingsButton.setSelection(true);
	    useProjectSettingsButton.setSelection(false);
	    configureButton.setEnabled(true);
	    updateFieldEditors();
	  }
	  super.performDefaults();
	}

Here, we reset all buttons to Use workspace settings and invoke method updateFieldEditors() to disable the field editors, too.

To conclude the definition of this class, we implement the invocation of the corresponding workbench preference page when the Configure Workbench Settings... button is pressed. This is done in method configureWorkspaceSettings():

	protected void configureWorkspaceSettings() {
	  try {
	    IPreferencePage page = 
                   (IPreferencePage) this.getClass().newInstance();
	    page.setTitle(getTitle());
	    page.setImageDescriptor(image);
	    showPreferencePage(pageId, page);
	  } catch (InstantiationException e) {
	    e.printStackTrace();
	  } catch (IllegalAccessException e) {
	    e.printStackTrace();
	  }
	}

This method first creates a sibling of the current instance, then completes its definition with title and image (remember, we had saved the required ImageDescriptor instance in one of the constructors), and calls method showPreferencePage() on this new page:

	protected void showPreferencePage(String id, IPreferencePage page) {
	  final IPreferenceNode targetNode = new PreferenceNode(id, page);
	  PreferenceManager manager = new PreferenceManager();
	  manager.addToRoot(targetNode);
	  final PreferenceDialog dialog = 
                new PreferenceDialog(getControl().getShell(), manager);
	  BusyIndicator.showWhile(getControl().getDisplay(), new Runnable() {
	    public void run() {
	      dialog.create();
	      dialog.setMessage(targetNode.getLabelText());
	      dialog.open();
	    }
	  });
	}

The code for method showPreferencePage() is actually pinched from an existing workbench component (to be precise: from the property page implementation for the Java compiler settings). It creates a solitary preference node, adds it as the root object to a new preference manager, then constructs a new preference dialog with this manager. This dialog is then created and opened.

This concludes the definition of class FieldEditorOverlayPage.

Using class FieldEditorOverlayPage

Using (i.e. subclassing) this class is very simple. Let us assume that we already have implemented a subclass of class FieldEditorPreferencePage. All what we have to do is to:

  1. modify the extends declaration of this subclass (exchange FieldEditorPreferencePage against FieldEditorOverlayPage).
  2. provide an implementation of method getPageId().
  3. optionally we may implement layout variations depending on the page type (preference page or property page).

Afterwards we can use this subclass in both roles: as a property page and as a preference page.

Let us look at an example. Here is the original field editor preference page:

	public class DefaultSpellCheckerPreferencePage 
	       extends FieldEditorPreferencePage 
               implements IWorkbenchPreferencePage {
	  public DefaultSpellCheckerPreferencePage() {
	    super(GRID);
	  }
	  public IPreferenceStore doGetPreferenceStore() {
	    return SpellCheckerPlugin.getDefault().getPreferenceStore();
	  }
	  public void init(IWorkbench workbench) {
	    setDescription("All changes will take effect ...");
	  }
	  public void createFieldEditors() {
	    Composite composite = getFieldEditorParent();
	    addField(new IntegerFieldEditor(
	       Configuration.SPELL_THRESHOLD,
	       "Spell &Threshold",
	       composite));
	    addField(new BooleanFieldEditor(
	       Configuration.SPELL_IGNOREDIGITWORDS,
	       "&Ignore Numbers",
	       composite));
	    ...
	    addField(new BooleanFieldEditor(
	       SpellCheckerPreferences.CHECKWHILETYPING,
	       "Check &while Typing",
	       composite));
	    ...
	  }
	}

And this is how we have to modify this class to be able using it as both a preference page and a property page. We have printed these changes in bold type:

	public class DefaultSpellCheckerPreferencePage 
	       extends FieldEditorOverlayPage 
               implements IWorkbenchPreferencePage {
	  public DefaultSpellCheckerPreferencePage() {
	    super(GRID);
	  }
	  public IPreferenceStore doGetPreferenceStore() {
	    return SpellCheckerPlugin.getDefault().getPreferenceStore();
	  }
	  public void init(IWorkbench workbench) {
	    setDescription("All changes will take effect ...");
	  }
	  protected String getPageId() {
	    return "com.bdaum.SpellChecker.preferences.defaultPreferences";
	  }
	  public void createFieldEditors() {
	    Composite composite = getFieldEditorParent();
	    addField(new IntegerFieldEditor(
	       Configuration.SPELL_THRESHOLD,
	       "Spell &Threshold",
	       composite));
	    addField(new BooleanFieldEditor(
	       Configuration.SPELL_IGNOREDIGITWORDS,
	       "&Ignore Numbers",
	       composite));
	    ...
	    if (!isPropertyPage()) {
	      addField(new BooleanFieldEditor(
	         SpellCheckerPreferences.CHECKWHILETYPING,
	         "Check &while Typing",
	         composite));
	      ...
	    }

Note, that we display the last field editor only in preference pages, not in property pages.

In the plug-in manifest file plugin.xml the necessary declarations could look like this:

	<extension id="com.bdaum.aoModeling.preferences"
		   name="SpellChecker Preferences"
	           point="org.eclipse.ui.preferencePages">
	  <page name="Spelling" class=
   "com.bdaum.SpellChecker.preferences.DefaultSpellCheckerPreferencePage"
	        id= "com.bdaum.SpellChecker.preferences.defaultPreferences">
	  </page>
	</extension>
	<extension id="com.bdaum.aoModeling.properties"
		   name="SpellChecker Properties"
	           point="org.eclipse.ui.propertyPages">
	  <page objectClass="org.eclipse.core.resources.IProject"
	        adaptable="true"
	        name="Spell Default" class=
   "com.bdaum.SpellChecker.preferences.DefaultSpellCheckerPreferencePage"
	        id="com.bdaum.SpellChecker.propertyPage.project">
	  </page>
	</extension>

Note, that the page identification returned by method getPageId() matches the page identification specified in the preference page extension point. Note also, that we used the same page implementation in both the preference page extension point and the property page extension point. (The identification of the property page actually may differ from the preference page id.)

Preferences page for a spell checker plug-in

 

Corresponding properties page for the same plug-in. The last group of widgets 
is deliberately suppressed for property pages by this page implementation.

Retrieving overlayed preference values

Reading such overlayed preference values is a multi-stage process. First we must interrogate the resource, if this property page uses the project settings or the workbench settings. If it uses the project settings, we must read the value from the resource properties, if not we fall back to the values defined in the workbench preference store. Here again, we have to qualify the field name with the page identification when we access the resource properties.

	public static String getOverlayedPreferenceValue(
                                             IPreferenceStore store, 
                                             IResource resource, 
                                             String pageId, 
                                             String name) {
	  IProject project = resource.getProject();
	  String value = null;
	  if (useProjectSettings(project, pageId)) {
	    value = getProperty(resource, pageId, key);
	  }
	  if (value != null)
	    return value;
	  return store.getString(key);
	}
	private static boolean useProjectSettings(IResource resource, 
                                                  String pageId) {
	  String use = getProperty(
                             resource, 
                             pageId, 
                             FieldEditorOverlayPage.USEPROJECTSETTINGS);
	  return "true".equals(use);
	}
	private static String getProperty(IResource resource, 
                                          String pageId, 
                                          String key) {
	  try {
	    return resource.getPersistentProperty(
                             new QualifiedName(pageId, key));
	  } catch (CoreException e) {
	  }
	  return null;
	}

Technology transfer

What works well for field editor preferences should also work for "normal" preference pages. However, some things are different when creating the class OverlayPage, since we now extend a PreferencePage and not a FieldEditorPreferencePage:

  1. We can directly extend the class PropertyPage because this class is a subclass of PreferencePage. This saves us from implementing the interface IWorkbenchPropertyPage with its getElement() and setElement() methods.
  2. We need to modify the constructors (no style parameter required).
  3. We need a different implementation for method createContents(). Since PreferencePage only defines this method as an abstract method, we need to implement it from scratch. We do so by creating two containers in the parent composite: One for our own button group and one for the contents created by subclasses. We return this second container as the method's result.
  4. There is no addField() method that we can override to keep track of field editors. In fact, there are no field editors at all. Consequently, we cannot rely on the knowledge encapsulated in the field editors. Therefore, we drop the methods addField() and updateFieldEditors(). Instead, we implement a method setControlsEnabled(). This method walks through the whole tree of child controls created by subclasses enabling or disabling them. (We spare tabbed notebooks and the like to allow for user navigation.) Again, subclasses may want to override this method when the enablement of controls depends on the state of other controls or on the state of the application.
  5. Subclasses must play it safe: If they override method performOk() they must invoke super.performOk(); and the implementation of createContents() must invoke super.createContents() and use the composite returned by this method as container for all controls created in the subclass.

At this point we leave it with these hints. For implementation details please see class OverlayPage in the source code zip.

Summary

We have implemented two abstract classes FieldEditorOverlayPage and OverlayPage. Subclasses extending these classes can act as both preference pages and property pages leading to an improved application consistency and lower maintenance cost. With only minimal modifications, already existing preference pages can be made by extending  FieldEditorOverlayPage or OverlayPage and can then be reused in the role of property pages. We achieved this by implementing an abstract access layer in form of class PropertyStore that encapsulates the properties of a resource but behaves as a preference store. In addition, both classes FieldEditorOverlayPage and OverlayPage provide the additional GUI elements required by property pages.

Source Code

To use these classes within your own plug-in, download the source code zip and import its contents of into the source folder of your Eclipse project.

About the author

Berthold Daum is an independent consultant and writer based in Germany. His best-selling book "Java Entwicklung mit Eclipse 2" (dpunkt verlag) will appear in an English version as "Eclipse 2 for Java Developers" in November 2003 (John Wiley & Sons).

Java and all Java-based trademarks and logos are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States, other countries, or both.