Using the Basic Custom Widget approach, our goal aims at the quick creation of a custom widget with a minimal amount of code even if it creates only a basic user interface for the specifier to use our widget. In this example, we will try to create a simple table widget.
In order to create a custom widget, we will have to think about the two kind of users that will interact with our work, the Sirius specifier and the end-user. With this basic approach, we will create a nice table widget for the end-user but we will be a bit limited regarding what we can offer to the specifier.
The implementation of the Properties view support in Eclipse Sirius is based on the Eclipse EEF project. While both projects are closely related, EEF can be used without Eclipse Sirius and in order to contribute a basic custom widget, we will only have to contribute to the EEF runtime. Both Eclipse EEF and Eclipse Sirius have the ability to let the specifier define a custom widget with a custom widget description.
This custom widget description in Eclipse Sirius is manipulated, like any other widget, in the odesign file. In this widget, you can specify the following properties of your widget:
Most of those properties are inherited from the description of the widget (identifier, labelExpression, helpExpression, isEnabledExpression) and the others are specific to the custom widget description (customExpressions, customOperations, style, conditionalStyles). In order to provide a basic custom widget to a Sirius specifier, you will have to indicate her/him the proper identifier to use for the widget, its expressions and its operations. For example, we will specify that in order to make our custom table widget work, the specifier will have to use a custom widget description with the identifier
com.example.awesomeproject.sirius.properties.ext.widgets.table
.
In order to provide the implementation of our table, we will need to use Eclipse EEF’s lifecycle manager provider extension point. This extension point let you define the appearance and behavior of a custom widget using a lifecycle manager.
You will have to start by creating an Eclipse plugin named
com.example.awesomeproject.eef.ide.ui.ext.widgets.table
which will contain the provider of the lifecycle manager. In Eclipse EEF, a lifecycle manager is used to create the user interface of a widget and manage its lifecycle (add listeners, refresh, remove listeners, dispose etc) while a controller is used to define its behavior. The controller is not compulsary but it is highly recommended. In order to create the lifecycle manager provider, you will have to define the following extension.
<?xml version="1.0" encoding="UTF-8"?> <?eclipse version="3.4"?> <plugin> <extension point="org.eclipse.eef.ide.ui.eefLifecycleManagerProvider"> <descriptor class="com.example.awesomeproject.eef.ide.ui.ext.widgets.table.internal.TableLifecycleManagerProvider" description="%tableLifecycleManagerProvider.Description" id="com.example.awesomeproject.eef.ide.ui.ext.widgets.table" label="%tableLifecycleManagerProvider.Label"> </descriptor> </extension> </plugin>
To make your code compile, you will have to add at least some dependencies (Required Bundle) to the following plugins:
The properties label and description should both be internationalized and the class should reference a Java class implementing IEEFLifecycleManagerProvider. Our lifecycle manager provider will have two methods, one to indicate if it can handle the description given by the EEF runtime and one to return a proper lifecycle manager for this description. In our case, the only description that we want to support is a custom widget with the identifier
com.example.awesomeproject.sirius.properties.ext.widgets.table
.
package com.example.awesomeproject.eef.ide.ui.ext.widgets.table.internal; import org.eclipse.eef.EEFControlDescription; import org.eclipse.eef.EEFCustomWidgetDescription; import org.eclipse.eef.core.api.EditingContextAdapter; import org.eclipse.eef.ide.ui.api.widgets.IEEFLifecycleManager; import org.eclipse.eef.ide.ui.api.widgets.IEEFLifecycleManagerProvider; import org.eclipse.sirius.common.interpreter.api.IInterpreter; import org.eclipse.sirius.common.interpreter.api.IVariableManager; public class TableLifecycleManagerProvider implements IEEFLifecycleManagerProvider { /** * The identifier of the control description supported. */ private static final String SUPPORTED_ID = "com.example.awesomeproject.sirius.properties.ext.widgets.table"; //$NON-NLS-1$ @Override public boolean canHandle(EEFControlDescription controlDescription) { // only support custom widgets with the proper identifier return SUPPORTED_ID.equals(controlDescription.getIdentifier()) && controlDescription instanceof EEFCustomWidgetDescription; } @Override public IEEFLifecycleManager getLifecycleManager(EEFControlDescription controlDescription, IVariableManager variableManager, IInterpreter interpreter, EditingContextAdapter contextAdapter) { if (controlDescription instanceof EEFCustomWidgetDescription) { return new TableLifecycleManager((EEFCustomWidgetDescription) controlDescription, variableManager, interpreter, contextAdapter); } throw new IllegalArgumentException(); } }
To create the lifecycle manager, the EEF runtime gives us access to several variables:
The lifecycle manager must implements the interface
IEEFLifecycleManager
but for a custom widget it is easier to extends on of its default abstract implementation, in our case
org.eclipse.eef.ide.ui.api.widgets.AbstractEEFWidgetLifecycleManager
.
package com.example.awesomeproject.eef.ide.ui.ext.widgets.table.internal; import com.example.awesomeproject.eef.core.ext.widgets.table.TableController; import org.eclipse.eef.EEFCustomWidgetDescription; import org.eclipse.eef.EEFWidgetDescription; import org.eclipse.eef.common.ui.api.IEEFFormContainer; import org.eclipse.eef.core.api.EditingContextAdapter; import org.eclipse.eef.core.api.controllers.IConsumer; import org.eclipse.eef.core.api.controllers.IEEFWidgetController; import org.eclipse.eef.ide.ui.api.widgets.AbstractEEFWidgetLifecycleManager; import org.eclipse.emf.edit.provider.ComposedAdapterFactory; import org.eclipse.emf.edit.ui.provider.AdapterFactoryLabelProvider; import org.eclipse.jface.viewers.ArrayContentProvider; import org.eclipse.jface.viewers.DelegatingStyledCellLabelProvider; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.TableViewer; import org.eclipse.sirius.common.interpreter.api.IInterpreter; import org.eclipse.sirius.common.interpreter.api.IVariableManager; import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Table; public class TableLifecycleManager extends AbstractEEFWidgetLifecycleManager { private EEFCustomWidgetDescription description; private TableViewer tableViewer; private ComposedAdapterFactory composedAdapterFactory; private SelectionListener onClickListener; private TableController controller; private IConsumer<Object> newValueConsumer; public TableLifecycleManager(EEFCustomWidgetDescription description, IVariableManager variableManager, IInterpreter interpreter, EditingContextAdapter contextAdapter) { super(variableManager, interpreter, contextAdapter); this.description = description; } @Override protected void createMainControl(Composite parent, IEEFFormContainer formContainer) { Table table = formContainer.getWidgetFactory().createTable(parent, SWT.READ_ONLY | SWT.V_SCROLL | SWT.FULL_SELECTION | SWT.BORDER | SWT.SINGLE); this.tableViewer = new TableViewer(table); this.composedAdapterFactory = new ComposedAdapterFactory(ComposedAdapterFactory.Descriptor.Registry.INSTANCE); this.tableViewer.setContentProvider(ArrayContentProvider.getInstance()); this.tableViewer.setLabelProvider(new DelegatingStyledCellLabelProvider(new AdapterFactoryLabelProvider.StyledLabelProvider( this.composedAdapterFactory, this.tableViewer))); this.controller = new TableController(description, variableManager, interpreter, contextAdapter); } @Override public void aboutToBeShown() { super.aboutToBeShown(); this.newValueConsumer = (newValue) -> this.tableViewer.setInput(newValue); this.controller.onNewValue(this.newValueConsumer); this.onClickListener = new SelectionListener() { @Override public void widgetSelected(SelectionEvent event) { Object selection = ((IStructuredSelection) TableLifecycleManager.this.tableViewer.getSelection()).getFirstElement(); TableLifecycleManager.this.controller.handleClick(selection); } @Override public void widgetDefaultSelected(SelectionEvent event) { Object selection = ((IStructuredSelection) TableLifecycleManager.this.tableViewer.getSelection()).getFirstElement(); TableLifecycleManager.this.controller.handleClick(selection); } }; this.tableViewer.getTable().addSelectionListener(this.onClickListener); } @Override public void refresh() { super.refresh(); this.controller.refresh(); } @Override public void aboutToBeHidden() { super.aboutToBeHidden(); this.controller.removeValueConsumer(); this.newValueConsumer = null; this.tableViewer.getTable().removeSelectionListener(this.onClickListener); this.onClickListener = null; } @Override protected IEEFWidgetController getController() { return this.controller; } @Override protected EEFWidgetDescription getWidgetDescription() { return this.description; } @Override protected Control getValidationControl() { return this.tableViewer.getTable(); } @Override public void dispose() { super.dispose(); this.composedAdapterFactory.dispose(); } }
The lifecycle manager will first create the controls of the table widget and it will initialized a controller for the table.
The controller is not required but it will handle several keys problems for us, for example by extending the proper abstract class it can handle the validation rules of your widget. For a basic custom widget, it is highly recommended to extend
org.eclipse.eef.core.api.controllers.AbstractEEFCustomWidgetController
. The controller does not have any reason to depend on the user interface, thus it will be created in another Eclipse plugin which will be usable without any dependency to an user interface specific plugins. This plugin will be named
com.example.awesomeproject.eef.core.ext.widgets.table
. It will depend on at least the following plugins:
package com.example.awesomeproject.eef.core.ext.widgets.table.internal; import java.util.HashMap; import java.util.Map; import org.eclipse.eef.EEFCustomWidgetDescription; import org.eclipse.eef.core.api.EditingContextAdapter; import org.eclipse.eef.core.api.controllers.AbstractEEFCustomWidgetController; import org.eclipse.eef.core.api.controllers.IConsumer; import org.eclipse.eef.core.api.utils.EvalFactory; import org.eclipse.sirius.common.interpreter.api.IInterpreter; import org.eclipse.sirius.common.interpreter.api.IVariableManager; public class TableController extends AbstractEEFCustomWidgetController { private static final String VALUE_EXPRESSION_ID = "valueExpression"; //$NON-NLS-1$ private static final String ON_CLICK_EXPRESSION_ID = "onClickExpression"; //$NON-NLS-1$ private static final String SELECTION_VARIABLE_NAME = "selection"; //$NON-NLS-1$ private IConsumer<Object> newValueConsumer; public TableController(EEFCustomWidgetDescription description, IVariableManager variableManager, IInterpreter interpreter, EditingContextAdapter contextAdapter) { super(description, variableManager, interpreter, contextAdapter); } @Override protected EEFCustomWidgetDescription getDescription() { return this.description; } @Override public void refresh() { super.refresh(); this.newEval().call(this.getCustomExpression(VALUE_EXPRESSION_ID), this.newValueConsumer); } public void handleClick(Object object) { this.contextAdapter.performModelChange(() -> { String onClickExpression = this.getCustomExpression(ON_CLICK_EXPRESSION_ID); Map<String, Object> variables = new HashMap<String, Object>(); variables.putAll(this.variableManager.getVariables()); variables.put(SELECTION_VARIABLE_NAME, object); EvalFactory.of(this.interpreter, variables).call(onClickExpression); }); } public void onNewValue(IConsumer<Object> consumer) { this.newValueConsumer = consumer; } public void removeValueConsumer() { this.newValueConsumer = null; } }
The controller here will handle two different situations. Firstly, it will react to the refresh in order to compute the new value of the table widget. For that, it will look for an expression with the identifier
valueExpression
and it will execute it. Once executed, it will give its result to the
newValueConsumer
. In the table lifecycle manager, we have set, in the method
aboutToBeShown
, this new value consumer to a lambda which will set a new input for the table. Each time that the lifecycle manager will be refreshed by the framework, its call to
super.refresh()
will trigger the refresh of the controller (among other things) which will compute the new value to display and given this value to the callback (consumer) registered by the lifecycle manager.
Secondly, the controller will be used to handle the click in the table. For that it has a method named
handleClick
which will be called with the selected object in the table. The controller will then evaluate an expression with the identifier
onClickExpression
with the variables available along with an additional variable specifically for this use case. The additional variable named
selection
will here be used to give access to the current selection to the
onClickExpression
.
Now your end users can use your two plugins to create tables in their Properties view using the custom widget description.