Using the Advanced Custom Widget approach, our goal aims at the creation of a custom widget with a great user experience for the Sirius specifier. This custom widget will be created after the development of the basic custom widget. You should be familiar with this approach first.
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 advanced approach, we will create a table widget for the end-user with a great experience for the Sirius specifier who will manipulate it.
The specification of the advanced custom widget will be the main difference with the basic custom widget. While with the basic approach, the Sirius specifier had to use a generic and bland custom widget description, with the advanced approach we will create a real definition for the Sirius specifier to manipulate.
As a first step, we want to contribute a piece of model to be displayed in the odesign file for our Sirius specifiers. For that, you will have to create a plugin named
com.example.awesomeproject.sirius.properties.ext.widgets.table
containing, in a folder named model, an Ecore model named
properties-ext-widgets-table.ecore
. In this Ecore model, you will have to load the resource
properties.ecore
used to define the Properties view description in the odesign. This meta-model can be found in the EPackage registry using its NsURI
http://www.eclipse.org/sirius/properties/1.0.0
.
Once this resource has been loaded, give a name, NsPrefix and NsURI to your EPackage (the root element), for example:
propertiesextwidgetstable
properties-ext-widgets-table
http://www.example.com/awesomeproject/properties/ext/widgets/table
After that, you can create an EClass under the EPackage with the named
ExtTableDescription
and with the EClass
WidgetDescription
as a supertype. This EClass should contain for our example two EAttributes named
onClickExpression
and
valueExpression
and both should have the type
InterpretedExpresion
.
You can then create a genmodel file for your Ecore model in the same repository and launch the generation of the model and its edit support. You will thus have two plugins:
com.example.awesomeproject.sirius.properties.ext.widgets.table
com.example.awesomeproject.sirius.properties.ext.widgets.table.edit
Eclipse EEF has a runtime independent of Eclipse Sirius and as such the definition of the widget that exist in the odesign file has to be transformed into a definition that can be maintained by Eclipse EEF. For most of the existing concepts this transformation is very basic since it only involves the transformation of the Sirius concepts directly into similar EEF concepts but both domain specific languages have very different roles.
The Eclipse Sirius Properties DSL is used as the user interface for the Eclipse Sirius specifier while the Eclipse EEF DSL is the model interpreted by the runtime. It is perfectly possible, and even recommended, to define, in the extension of the Sirius Properties DSL, high level concepts that should be user-friendly for the Sirius specifier and to keep in the extension of the EEF DSL raw concepts transformed from the extension of the Sirius Properties DSL.
You will now have to create a third plugin named
com.example.awesomeproject.eef.ext.widgets.table
with a model folder and an Ecore model inside named
eef-ext-widgets-table.ecore
. In this model you should load the resource
eef.ecore
containing the EEF DSL that you will have to extend. You can find it in the EPackage registry using its NsURI
http://www.eclipse.org/eef
. Once the EEF DSL is loaded, you can set the properties of your root EPackage:
eefextwidgetstable
eef-ext-widgets-table
http://www.example.com/awesomeproject/eef/ext/widgets/table
Under this root EPackage, you can now create your EClass named
EEFExtTableDescription
which should have
EEFWidgetDescription
as a supertype. This EClass should contain for our example two EAttributes named
onClickExpression
and
valueExpression
and both should have the type
EString
. In our example, both the extension of the Sirius DSL and the EEF DSL have the same properties since we are using a very simple example.
You can now create a genmodel for your extension of the EEF DSL and generate the code for the model (the Edit support is not necessary here). You will now have three plugins:
com.example.awesomeproject.sirius.properties.ext.widgets.table
com.example.awesomeproject.sirius.properties.ext.widgets.table.edit
com.example.awesomeproject.eef.ext.widgets.table
Now that you have your extension to the Sirius Properties DSL and the EEF DSL, you need to inform the Sirius bridge how to transform the concepts from your extension to the Sirius DSL into EEF concepts. For that an extension point is available in order to contribute an IDescriptionConverter. This converter will have to depend on the Sirius bridge for Eclipse EEF and at least the extension to both the Sirius Properties DSL and the EEF DSL. As a result, this code will have to go into another plugin because having those dependencies on an existing plugins would be against the best practices. We will thus create a fourth plugin named
com.example.awesomeproject.sirius.ui.properties.ext.widgets.table
. In this plugin, we will declare our description converter using the following extension.
<?xml version="1.0" encoding="UTF-8"?> <?eclipse version="3.4"?> <plugin> <extension point="org.eclipse.sirius.ui.properties.descriptionConverter"> <descriptor class="com.example.awesomeproject.sirius.ui.properties.ext.widgets.table.internal.ExtTableDescriptionConverter" description="%tableDescriptionConverter.Description" id="com.example.awesomeproject.sirius.ui.properties.ext.widgets.table.descriptionConverter" label="%tableDescriptionConverter.Label"> </descriptor> </extension> </plugin>
The transformation from the Sirius DSL to the EEF DSL occur in two parts, first the description are converted using an IDescriptionConverter and after an IDescriptionLinkResolver may be used to resolve some links. In our case, the description converter will simply be used to transform a instance of
ExtTableDescription
into an
EEFExtTableDescription
and we won’t use a description link resolver since it is not useful here.
package com.example.awesomeproject.sirius.ui.properties.ext.widgets.table.internal; import java.util.Map; import com.example.awesomeproject.eef.ext.widgets.table.EEFExtTableDescription; import com.example.awesomeproject.eef.ext.widgets.table.EefExtWidgetsTableFactory; import org.eclipse.emf.ecore.EObject; import com.example.awesomeproject.sirius.properties.ext.widgets.table.ExtTableDescription; import org.eclipse.sirius.ui.properties.api.DescriptionCache; import org.eclipse.sirius.ui.properties.api.IDescriptionConverter; public class ExtTableDescriptionConverter implements IDescriptionConverter { @Override public boolean canHandle(EObject description) { return description instanceof ExtTableDescription; } @Override public EObject convert(EObject description, Map<String, Object> parameters, DescriptionCache cache) { if (description instanceof ExtTableDescription) { ExtTableDescription extTableDescription = (ExtTableDescription) description; EEFExtTableDescription eefExtTableDescription = EefExtWidgetsTableFactory.eINSTANCE.createEEFExtTableDescription(); eefExtTableDescription.setIdentifier(extTableDescription.getIdentifier()); eefExtTableDescription.setHelpExpression(extTableDescription.getHelpExpression()); eefExtTableDescription.setIsEnabledExpression(extTableDescription.getIsEnabledExpression()); eefExtTableDescription.setLabelExpression(extTableDescription.getLabelExpression()); eefExtTableDescription.setValueExpression(extTableDescription.getValueExpression()); eefExtTableDescription.setOnClickExpression(extTableDescription.getOnClickExpression()); // Let's not forget to populate the cache for the other converters or link resolvers cache.put(extTableDescription, eefExtTableDescription); return eefExtTableDescription; } return null; } }
An IDescriptionConverter can be used to convert any element from the Sirius Properties DSL into an EEF DSL element. In order to support custom widgets, you only have to handle your own objects but you could still modify other kind of objects from the Sirius Properties DSL. As a result, this mechanism can be used to dynamically modify anything in the description of the Properties view at runtime. This usage of the IDescriptionConverter should almost never be used, it’s a very powerful mechanism for extremely advanced users only.
In order to help developer transform their Sirius entities into EEF entities, multiple possible super-classes are available:
If you try to edit your extension of the Sirius DSL in the odesign editor, you will find out that it does not work. In order to be able to view the properties of your widget, in our case the
onClickExpression
and the
valueExpression
, you will need to contribute to the Properties view of the odesign editor. Since this contribution will require a dependency with the framework used for the Properties view of the odesign editor, it will require another plugin named
com.example.awesomeproject.sirius.editor.properties.ext.widgets.table
with a dependency to at least:
<?xml version="1.0" encoding="UTF-8"?> <?eclipse version="3.4"?> <plugin> <extension point="org.eclipse.ui.views.properties.tabbed.propertySections"> <propertySections contributorId="org.eclipse.sirius.editor.editorPlugin.SiriusEditorContributor"> <propertySection afterSection="properties.section.widgetDescription.IsEnabledExpression" class="com.example.awesomeproject.sirius.editor.properties.ext.widgets.table.internal.ExtTableDescriptionValueExpressionPropertySection" filter="com.example.awesomeproject.sirius.editor.properties.ext.widgets.table.internal.ExtTableDescriptionValueExpressionFilter" id="properties.section.extTableDescription.valueExpression" tab="viewpoint.tab.general"> <input type="com.example.awesomeproject.sirius.editor.properties.ext.widgets.table.propertiesextwidgetstablee.ExtTableDescription"> </input> </propertySection> <propertySection afterSection="properties.section.extTableDescription.valueExpression" class="com.example.awesomeproject.sirius.editor.properties.ext.widgets.table.internal.ExtTableDescriptionOnClickExpressionPropertySection" filter="com.example.awesomeproject.sirius.editor.properties.ext.widgets.table.internal.ExtTableDescriptionOnClickExpressionFilter" id="properties.section.extTableDescription.valueExpression" tab="viewpoint.tab.general"> <input type="com.example.awesomeproject.sirius.editor.properties.ext.widgets.table.propertiesextwidgetstablee.ExtTableDescription"> </input> </propertySection> </propertySections> </extension> </plugin>
Two property sections will be necessary in order to display a text field for the value expression and the on click expression.
package com.example.awesomeproject.sirius.editor.properties.ext.widgets.table.internal; import org.eclipse.emf.ecore.EStructuralFeature; import org.eclipse.sirius.editor.properties.filters.common.ViewpointPropertyFilter; import com.example.awesomeproject.sirius.editor.properties.ext.widgets.table.propertiesextwidgetstablee.PropertiesExtTablePackage; public class ExtTableDescriptionOnClickExpressionFilter extends ViewpointPropertyFilter { @Override protected EStructuralFeature getFeature() { return PropertiesExtTablePackage.eINSTANCE.getExtTableDescription_OnClickExpression(); } @Override protected boolean isRightInputType(Object arg0) { return arg0 instanceof com.example.awesomeproject.sirius.editor.properties.ext.widgets.table.propertiesextwidgetstablee.ExtTableDescription; } }
Both filter will look almost the same, the only difference will be the structural feature that they will return.
package com.example.awesomeproject.sirius.editor.properties.ext.widgets.table.internal; import org.eclipse.emf.ecore.EAttribute; import org.eclipse.sirius.editor.editorPlugin.SiriusEditor; import org.eclipse.sirius.editor.properties.sections.common.AbstractTextWithButtonPropertySection; import org.eclipse.sirius.editor.tools.api.assist.TypeContentProposalProvider; import org.eclipse.sirius.editor.tools.internal.presentation.TextWithContentProposalDialog; import com.example.awesomeproject.sirius.editor.properties.ext.widgets.table.propertiesextwidgetstablee.PropertiesExtTablePackage; import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.layout.FormAttachment; import org.eclipse.swt.layout.FormData; import org.eclipse.swt.widgets.Composite; import org.eclipse.ui.views.properties.tabbed.TabbedPropertySheetPage; @SuppressWarnings("restriction") public class ExtTableDescriptionOnClickExpressionPropertySection extends AbstractTextWithButtonPropertySection { @Override protected String getDefaultLabelText() { return "On Click Expression"; //$NON-NLS-1$ } @Override protected String getLabelText() { String labelText; labelText = super.getLabelText() + "*:"; //$NON-NLS-1$ return labelText; } @Override public EAttribute getFeature() { return PropertiesExtTablePackage.eINSTANCE.getExtTableDescription_OnClickExpression(); } @Override protected Object getFeatureValue(String newText) { return newText; } @Override protected boolean isEqual(String newText) { return this.getFeatureAsText().equals(newText); } @Override public void createControls(Composite parent, TabbedPropertySheetPage tabbedPropertySheetPage) { super.createControls(parent, tabbedPropertySheetPage); text.setToolTipText(getToolTipText()); /* * We set the color as it's a InterpretedExpression */ text.setBackground(SiriusEditor.getColorRegistry().get("yellow")); //$NON-NLS-1$ TypeContentProposalProvider.bindPluginsCompletionProcessors(this, text); FormData data = new FormData(); data.top = new FormAttachment(text, 0, SWT.TOP); data.left = new FormAttachment(nameLabel); nameLabel.setFont(SiriusEditor.getFontRegistry().get("required")); //$NON-NLS-1$ } @Override protected SelectionListener createButtonListener() { return new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { TextWithContentProposalDialog dialog = new TextWithContentProposalDialog(composite.getShell(), ExtTableDescriptionOnClickExpressionPropertySection.this, text.getText()); dialog.open(); text.setText(dialog.getResult()); handleTextModified(); } }; } @Override protected String getPropertyDescription() { return ""; //$NON-NLS-1$ } }
This property section will create a text field and label in the Properties view of the odesign editor in order to let the Sirius specifier edit the
onClickExpression
of the table. Our text field will have the same appear and feature as the regular yellow text fields used by Sirius.
Now we can reuse our work in the basic approach but this time with our own specific DSL instead of reusing the CustomElements from the EEF DSL. First we will have to modify the lifecycle manager provider to support our EEFExtTableDescription.
package com.example.awesomeproject.eef.ide.ui.ext.widgets.table.internal; import com.example.awesomeproject.eef.ext.widgets.table.EEFExtTableDescription; import org.eclipse.eef.EEFControlDescription; 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 { @Override public boolean canHandle(EEFControlDescription controlDescription) { return controlDescription instanceof EEFExtTableDescription; } @Override public IEEFLifecycleManager getLifecycleManager(EEFControlDescription controlDescription, IVariableManager variableManager, IInterpreter interpreter, EditingContextAdapter contextAdapter) { if (controlDescription instanceof EEFExtTableDescription) { return new TableLifecycleManager((EEFExtTableDescription) controlDescription, variableManager, interpreter, contextAdapter); } throw new IllegalArgumentException(); } }
Now we can just modify our lifecycle manager to support our EEFExtTableDescription.
package com.example.awesomeproject.eef.ide.ui.ext.widgets.table.internal; import com.example.awesomeproject.eef.ext.widgets.table.EEFExtTableDescription; import com.example.awesomeproject.eef.core.ext.widgets.table.TableController; 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 EEFExtTableDescription description; private TableViewer tableViewer; private ComposedAdapterFactory composedAdapterFactory; private SelectionListener onClickListener; private TableController controller; private IConsumer<Object> newValueConsumer; public TableLifecycleManager(EEFExtTableDescription 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(); } }
And finally we can easily adapter our controller.
package com.example.awesomeproject.eef.core.ext.widgets.table.internal; import com.example.awesomeproject.eef.ext.widgets.table.EEFExtTableDescription; 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 AbstractEEFWidgetController { 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(EEFExtTableDescription description, IVariableManager variableManager, IInterpreter interpreter, EditingContextAdapter contextAdapter) { super(description, variableManager, interpreter, contextAdapter); } @Override protected EEFExtTableDescription getDescription() { return this.description; } @Override public void refresh() { super.refresh(); this.newEval().call(this.description.getValueExpression(), this.newValueConsumer); } public void handleClick(Object object) { this.contextAdapter.performModelChange(() -> { String onClickExpression = this.description.getOnClickExpression(); 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; } }
With minimal changes, we can reuse the code of the basic approach in this advanced approach. But now we have the ability to provide the Sirius specifiers with a proper user interface for the manipulation of our widget and we can manipulate our own concepts in the EEF runtime.