When you are baking cookies with your kids, the most funny thing for the kids is to decorate the cookies. Some like chocolate flakes, some more the pink sugar pops, others want to use different colors on top.
The users of an Eclipse application are also not the same and do not all like the same settings. To provide a way to customize an Eclipse application, you can use Preferences that a user can configure.
This recipe will explain and show how to add a preference page to an Eclipse 4 application and how to handle preference changes.
This recipe is based on the Eclipse RCP Cookbook – The Topping Recipe. To get started fast with this recipe, the recipe is prepared for you on GitHub.
To use the prepared recipe, import the project by cloning the Git repository:
- File → Import → Git → Projects from Git
- Click Next
- Select Clone URI
- Enter URI https://github.com/fipro78/e4-cookbook-basic-recipe.git
- Click Next
- Select the p2_update branch
- Click Next
- Choose a directory where you want to store the checked out sources
- Select Import existing Eclipse projects
- Click Finish
In a plain Eclipse 4 application, you don't want to use the Compatibility Layer. As the existing preferences support has dependencies to org.eclipse.ui
(e.g. the ScopedPreferenceStore
is located in org.eclipse.ui.workbench
) or at least consumes classes from there, I implemented an alternative approach, that does not make use of org.eclipse.ui
and provides a way to contribute preference pages to a dialog, without the need to define Extension Points. The bundle that provides this feature is available as an update site on GitHub.
-
Open the target definition org.fipro.eclipse.tutorial.target.target in the project org.fipro.eclipse.tutorial.target
-
Update the Software Sites in the opened Target Definition Editor
- Alternative A
- Switch to the Source tab and add the following snippet to the editor
<target name="E4 Cookbook Target Platform" sequenceNumber="1568034040"> <locations> <location includeAllPlatforms="false" includeConfigurePhase="false" includeMode="planner" includeSource="true" type="InstallableUnit"> <unit id="org.eclipse.equinox.executable.feature.group" version="3.8.2400.v20240213-1244"/> <unit id="org.eclipse.sdk.feature.group" version="4.31.0.v20240229-1022"/> <unit id="org.eclipse.equinox.core.feature.feature.group" version="1.15.0.v20240214-0846"/> <unit id="org.eclipse.equinox.p2.core.feature.feature.group" version="1.7.100.v20240220-1431"/> <repository location="https://download.eclipse.org/releases/2024-03"/> </location> <location includeAllPlatforms="false" includeConfigurePhase="false" includeMode="planner" includeSource="true" type="InstallableUnit"> <unit id="org.fipro.e4.service.preferences.feature.feature.group" version="0.5.0.202406031042"/> <repository location="https://github.com/fipro78/e4-preferences/raw/master/releases/0.5.0"/> </location> </locations> </target>
- Alternative B
- Select Add...
- Select Software Site
- Click Next
- Enter https://github.com/fipro78/e4-preferences/raw/master/releases/0.5.0 in Work with:
- Select E4 Preferences Service
- Click Finish
- Alternative A
-
Switch to the Definition tab
- Wait until the Target Definition is completely resolved (check the progress at the bottom right)
- Reload and activate the target platform by clicking Reload Target Platform in the upper right corner of the Target Definition Editor
To provide the user an option to change preferences, a handler will be added to the application plug-in that can be triggered via a menu entry in the main menu and opens the preferences dialog .
- Update the bundle dependencies
- Open the file META-INF/MANIFEST.MF in the project org.fipro.eclipse.tutorial.app
- Switch to the Dependencies tab
- Add the following packages to the Imported Packages
org.eclipse.e4.core.di.extensions
org.fipro.e4.service.preferences
org.osgi.service.event
- Add the following packages to the Imported Packages
- Update the application model
- Open the file Application.e4xmi in the project org.fipro.eclipse.tutorial.app
- Add a command
- Application → Commands → Add
- Set Name to Preferences
- Set ID to org.fipro.eclipse.tutorial.app.command.preferences
(will be done automatically on setting the Name)
- Application → Commands → Add
- Add a handler
- Application → Handlers → Add
- Set ID to org.fipro.eclipse.tutorial.app.handler.preferences
- Set the Command reference to org.fipro.eclipse.tutorial.app.command.preferences via the Find... dialog
- Create a handler implementation by clicking on the Class URI link
- Set Package to org.fipro.eclipse.tutorial.app.handler
- Set Name to PreferencesHandler
- Click Finish
- Implement the
PreferencesHandler
similar to the following snippet- Use the
@PrefMgr
annotation to get the thePreferenceManager
injected in theexecute()
method - Open a
PreferenceDialog
- Use the
package org.fipro.eclipse.tutorial.app.handler; import org.eclipse.e4.core.di.annotations.Execute; import org.eclipse.jface.preference.PreferenceDialog; import org.eclipse.jface.preference.PreferenceManager; import org.eclipse.jface.viewers.TreeViewer; import org.eclipse.jface.viewers.ViewerComparator; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Shell; import org.fipro.e4.service.preferences.ContributedPreferenceNode; public class PreferencesHandler { @Execute public void execute(Shell shell, PreferenceManager manager) { PreferenceDialog dialog = new PreferenceDialog(shell, manager) { @Override protected TreeViewer createTreeViewer(Composite parent) { TreeViewer viewer = super.createTreeViewer(parent); viewer.setComparator(new ViewerComparator() { @Override public int category(Object element) { // this ensures that the General preferences page is always on top // while the other pages are ordered alphabetical if (element instanceof ContributedPreferenceNode && ("general".equals(((ContributedPreferenceNode) element).getId()))) { return -1; } return 0; } }); return viewer; } }; dialog.open(); } }
- Application → Handlers → Add
- Select Application → Windows → Trimmed Window → Main Menu → Menu File
- Add a Handled Menu Item
- Set the Label to Preferences
- Set the Command reference to the Preference command via Find... dialog
- Save the changes to the application model
- Open the file org.fipro.eclipse.tutorial.app.product in the project org.fipro.eclipse.tutorial.product
- Switch to the Contents tab
- Add org.fipro.e4.service.preferences.feature
Implement a JFace PreferencePage
for some basic settings, e.g. the application title shown in the main window:
- Right click on the project org.fipro.eclipse.tutorial.app
- New → Class
- Set Package to org.fipro.eclipse.tutorial.app.preferences
- Set Name to GeneralPreferencePage
- Set Superclass to org.eclipse.jface.preference.PreferencePage
- Click Finish
package org.fipro.eclipse.tutorial.app.preferences;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.preference.PreferencePage;
import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Text;
public class GeneralPreferencePage extends PreferencePage {
// Names for preferences
private static final String APP_TITLE = "app_title";
private static final String WELCOME_MSG = "welcome_message";
// Text fields for user to enter preferences
private Text fieldOne;
private Text fieldTwo;
public GeneralPreferencePage() {
super("General");
setDescription("The general preferences");
}
/**
* Creates the controls for this page
*/
@Override
protected Control createContents(Composite parent) {
Composite composite = new Composite(parent, SWT.NONE);
composite.setLayout(new GridLayout(2, false));
// Get the preference store
IPreferenceStore preferenceStore = getPreferenceStore();
// Create text fields.
// Set the text in each from the preference store
new Label(composite, SWT.LEFT).setText("Application Title:");
fieldOne = new Text(composite, SWT.BORDER);
fieldOne.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
fieldOne.setText(preferenceStore.getString(APP_TITLE));
new Label(composite, SWT.LEFT).setText("Welcome Message:");
fieldTwo = new Text(composite, SWT.BORDER);
fieldTwo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
fieldTwo.setText(preferenceStore.getString(WELCOME_MSG));
return composite;
}
/**
* Called when user clicks Restore Defaults
*/
@Override
protected void performDefaults() {
// Get the preference store
IPreferenceStore preferenceStore = getPreferenceStore();
// Reset the fields to the defaults
fieldOne.setText(preferenceStore.getDefaultString(APP_TITLE));
fieldTwo.setText(preferenceStore.getDefaultString(WELCOME_MSG));
}
/**
* Called when user clicks Apply or OK
*
* @return boolean
*/
@Override
public boolean performOk() {
// Get the preference store
IPreferenceStore preferenceStore = getPreferenceStore();
// Set the values from the fields
if (fieldOne != null) {
preferenceStore.setValue(APP_TITLE, fieldOne.getText());
}
if (fieldTwo != null) {
preferenceStore.setValue(WELCOME_MSG, fieldTwo.getText());
}
// Return true to allow dialog to close
return true;
}
}
The above implementation simply provides the option to set an application title and a welcome message via preferences.
Implement the PreferenceNodeContribution
service that contributes the PreferencePage
to the dialog:
- Right click on the project org.fipro.eclipse.tutorial.app
- New → Class
- Set Package to org.fipro.eclipse.tutorial.app.preferences
- Set Name to ApplicationPreferencesContribution
- Set Superclass to org.fipro.e4.service.preferences.PreferenceNodeContribution
- Click Finish
package org.fipro.eclipse.tutorial.app.preferences;
import org.fipro.e4.service.preferences.PreferenceNodeContribution;
import org.osgi.service.component.annotations.Component;
@Component(service = PreferenceNodeContribution.class)
public class ApplicationPreferencesContribution extends PreferenceNodeContribution {
public ApplicationPreferencesContribution() {
super("general", "General", GeneralPreferencePage.class);
}
}
Note:
If you want to provide multiple preference pages from one plugin, you can use one of the PreferenceNodeContribution#addPreferenceNode()
methods after the super()
call.
From the old Eclipse Wiki:
Addons are objects that are instantiated by Eclipse 4's dependency injection framework. Addons are global and are contained under the application.
These addon objects are created before the rendering engine actually renders the model. As such, addons can be used to alter the user interface that is produced by the rendering engine. For example, the min/max addon that comes with the Eclipse 4.x SDK tweaks the tab folders created for MPartStacks to have min/max buttons in the corner.
Add-ons can for example be used to perform actions once the app startup is completed, or even other events you fire. As our application does not contain any user interface classes so far, we will add an Add-on that reacts on the preference change to update the application title in the main window. Additionally it adds the ability to show a welcome message on startup.
- Update the application model
- Open the file Application.e4xmi in the project org.fipro.eclipse.tutorial.app
- Add an Add-on
- Application → Add-ons → Add
- Click on Class URI
- Set Package to org.fipro.eclipse.tutorial.app.addon
- Set Name to AppTitleAddon
- Replace the implementation with the following snippet
- Get the
MApplication
injected - Implement an event handler method that reacts on the event topic
UIEvents.UILifeCycle.APP_STARTUP_COMPLETE
and gets the welcome_message preference value injected, and shows a dialog if a message is set. - Implement a preference change listener method that gets the app_title preference value injected and changes the title of the main window.
- Get the
- Application → Add-ons → Add
package org.fipro.eclipse.tutorial.app.addon;
import org.eclipse.e4.core.di.annotations.Optional;
import org.eclipse.e4.core.di.extensions.EventTopic;
import org.eclipse.e4.core.di.extensions.Preference;
import org.eclipse.e4.ui.model.application.MApplication;
import org.eclipse.e4.ui.workbench.UIEvents;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.osgi.service.event.Event;
import jakarta.inject.Inject;
public class AppTitleAddon {
@Inject
private MApplication application;
@Inject
@Optional
public void applicationStarted(
@EventTopic(UIEvents.UILifeCycle.APP_STARTUP_COMPLETE) Event event,
@Preference(nodePath = "org.fipro.eclipse.tutorial.app", value = "welcome_message") String msg) {
if (msg != null) {
// need to ensure that the dialog is opened in the UI thread
Display.getDefault().asyncExec(() -> {
Shell shell = application.getContext().get(Shell.class);
MessageDialog.openInformation(shell, "Welcome", msg);
});
}
}
@Inject
@Optional
public void setAppTitle(
@Preference(nodePath = "org.fipro.eclipse.tutorial.app", value = "app_title") String title) {
if (title == null || title.isBlank()) {
title = "Eclipse Cookbook Application";
}
application.getChildren().get(0).setLabel(title);
}
}
The preference change listener method uses the @Preference
annotation in combination with @Inject
. We also use @Optional
to avoid errors if no value for the specified preference is available when the injection processing happens.
The @Preference
annotation has two parameters:
- The nodePath parameter is the file name used to save the preference values to disk. By default, this is the Bundle-SymbolicName of the plugin.
- The value parameter specifies the preference key for the value which should be injected.
-
Update the bundle dependencies
- Open the file META-INF/MANIFEST.MF in the project org.fipro.eclipse.tutorial.inverter
- Switch to the Dependencies tab
- Add the following packages to the Imported Packages
org.eclipse.e4.core.di.annotations
org.fipro.e4.service.preferences
- Add the following packages to the Imported Packages
-
Right click on the project org.fipro.eclipse.tutorial.inverter
-
New → Class
- Set Package to org.fipro.eclipse.tutorial.inverter.preferences
- Set Name to InverterPreferencePage
- Set Superclass to org.eclipse.jface.preference.PreferencePage
- Click Finish
package org.fipro.eclipse.tutorial.inverter.preferences;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.preference.PreferencePage;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
public class InverterPreferencePage extends PreferencePage {
// Names for preferences
private static final String INVERTER_COLOR = "inverter_color";
// The checkboxes
private Button checkOne;
private Button checkTwo;
public InverterPreferencePage() {
super("Inverter");
setDescription("The inverter preferences page");
}
/**
* Creates the controls for this page
*/
protected Control createContents(Composite parent) {
Composite composite = new Composite(parent, SWT.NONE);
composite.setLayout(new GridLayout(2, false));
// Get the preference store
IPreferenceStore preferenceStore = getPreferenceStore();
String color = preferenceStore.getString(INVERTER_COLOR);
boolean isBlack = (color != null && !color.isEmpty()) ? "black".equals(color) : true;
// Create the checkboxes
checkOne = new Button(composite, SWT.RADIO);
checkOne.setText("Text Color Black");
checkOne.setSelection(isBlack);
checkTwo = new Button(composite, SWT.RADIO);
checkTwo.setText("Text Color Blue");
checkTwo.setSelection(!isBlack);
checkOne.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
checkOne.setSelection(true);
checkTwo.setSelection(false);
}
});
checkTwo.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
checkOne.setSelection(false);
checkTwo.setSelection(true);
}
});
return composite;
}
/**
* Called when user clicks Restore Defaults
*/
protected void performDefaults() {
// Get the preference store
IPreferenceStore preferenceStore = getPreferenceStore();
String color = preferenceStore.getString(INVERTER_COLOR);
boolean isBlack = (color != null && !color.isEmpty()) ? "black".equals(color) : true;
// Reset the fields to the defaults
checkOne.setSelection(isBlack);
checkTwo.setSelection(!isBlack);
}
/**
* Called when user clicks Apply or OK
*
* @return boolean
*/
public boolean performOk() {
// Get the preference store
IPreferenceStore preferenceStore = getPreferenceStore();
// Set the values from the fields
if (checkOne != null && checkOne.getSelection()) {
preferenceStore.setValue(INVERTER_COLOR, "black");
} else if (checkTwo != null && checkTwo.getSelection()) {
preferenceStore.setValue(INVERTER_COLOR, "blue");
}
// Return true to allow dialog to close
return true;
}
}
Implement the PreferenceNodeContribution
service that contributes the PreferencePage
to the dialog:
- Right click on the project org.fipro.eclipse.tutorial.inverter
- New → Class
- Set Package to org.fipro.eclipse.tutorial.inverter.preferences
- Set Name to InverterPreferencesContribution
- Set Superclass to org.fipro.e4.service.preferences.PreferenceNodeContribution
- Click Finish
package org.fipro.eclipse.tutorial.inverter.preferences;
import org.fipro.e4.service.preferences.PreferenceNodeContribution;
import org.osgi.service.component.annotations.Component;
@Component(service = PreferenceNodeContribution.class)
public class InverterPreferencesContribution extends PreferenceNodeContribution {
public InverterPreferencesContribution() {
super("inverter", "Inverter", InverterPreferencePage.class);
}
}
- Open the
InverterPart
- CTRL + SHIFT + T
- In the Open Type dialog enter InverterPart and select it in the list view
- Change the
input
andoutput
Text
fields to class members - Introduce a class member for the text color that should be applied
- Add a method for the preference handling
package org.fipro.eclipse.tutorial.inverter.part;
import org.eclipse.e4.core.di.annotations.Optional;
import org.eclipse.e4.core.di.extensions.Preference;
import org.eclipse.e4.core.di.extensions.Service;
import org.eclipse.e4.core.services.events.IEventBroker;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Text;
import org.fipro.eclipse.tutorial.service.inverter.InverterService;
import jakarta.annotation.PostConstruct;
import jakarta.inject.Inject;
public class InverterPart {
@Inject
@Service
private InverterService inverter;
@Inject
IEventBroker broker;
Text input;
Text output;
Color textColor = Display.getDefault().getSystemColor(SWT.COLOR_BLACK);
@PostConstruct
public void postConstruct(Composite parent) {
parent.setLayout(new GridLayout(3, true));
Label inputLabel = new Label(parent, SWT.NONE);
inputLabel.setText("String to revert:");
GridDataFactory.fillDefaults().applyTo(inputLabel);
input = new Text(parent, SWT.BORDER);
input.setForeground(textColor);
GridDataFactory.fillDefaults().grab(true, false).applyTo(input);
Button button = new Button(parent, SWT.PUSH);
button.setText("Revert");
GridDataFactory.defaultsFor(button).applyTo(button);
Label outputLabel = new Label(parent, SWT.NONE);
outputLabel.setText("Inverted String:");
GridDataFactory.fillDefaults().applyTo(outputLabel);
output = new Text(parent, SWT.READ_ONLY | SWT.WRAP);
output.setForeground(textColor);
GridDataFactory.fillDefaults().grab(true, true).span(2, 1).applyTo(output);
button.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
output.setText(inverter.invert(input.getText()));
broker.post("TOPIC_LOGGING", "triggered via button");
}
});
input.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (e.keyCode == SWT.CR
|| e.keyCode == SWT.KEYPAD_CR) {
output.setText(inverter.invert(input.getText()));
broker.post("TOPIC_LOGGING", "triggered via field");
}
}
});
}
@Inject
@Optional
public void setTextColor(
@Preference(nodePath = "org.fipro.eclipse.tutorial.inverter", value = "inverter_color") String color) {
textColor = "blue".equals(color)
? Display.getDefault().getSystemColor(SWT.COLOR_BLUE)
: Display.getDefault().getSystemColor(SWT.COLOR_BLACK);
if (input != null && !input.isDisposed()) {
input.setForeground(textColor);
}
if (output != null && !output.isDisposed()) {
output.setForeground(textColor);
}
}
}
As we added a preference to specify a welcome message on application startup, it might be useful to add an option to restart the application. This is optional, as you can also stop the application and start it again.
- Open the file Application.e4xmi in the project org.fipro.eclipse.tutorial.app
- Add a restart command
- Application → Commands → Add
- Set Name to Restart
- Set ID to org.fipro.eclipse.tutorial.app.command.restart
- Application → Commands → Add
- Add a restart handler
- Application → Handlers → Add
- Set ID to org.fipro.eclipse.tutorial.app.handler.restart
- Set the Command reference to org.fipro.eclipse.tutorial.app.command.restart via Find... dialog
- Create a handler implementation by clicking on the Class URI link
- Set Package to org.fipro.eclipse.tutorial.app.handler
- Set Name to RestartHandler
@Execute public void execute(IWorkbench workbench, Shell shell) { if (MessageDialog.openConfirm(shell, "Restart", "Do you want to restart?")) { workbench.restart(); } }
- Application → Handlers → Add
- Add a Handled Menu Item to the File menu
- Set the Label to Restart
- Set the Command reference to the Restart command via Find... dialog
- Start the application from within the IDE
- Open the Product Configuration in the org.fipro.eclipse.tutorial.product project
- Select the Overview tab
- Click Launch an Eclipse Application in the Testing section
Alternatively you can also run the Tycho build and then start the created product as explained the Thermomix Recipe.
- In the started application
- Open the Preference Dialog via File → Preferences
- Select the General settings and enter values for Application Title and Welcome Message
- Select the Inverter settings and select the Text Color Blue
- Click Apply and Close
- Verify the changed settings
- The window title should now show the value you just entered
- Enter a value in the text field to trigger the action and verify that the text is shown in blue
- Restart the application and verify that a message dialog appears on startup showing the message you entered in the preferences.
Further information about Eclipse Preferences can be found in Eclipse Preferences - Tutorial @vogella