Sirius Properties – Advanced Custom Widget

Goal

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.

Strategy

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.

Specification of the custom widget in Eclipse Sirius

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:

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:

Specification of the custom widget in Eclipse EEF

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:

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:

Contribution of the converter from Sirius to EEF

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:

Contribution of the preprocessor from Sirius with extensibility features to flatten Sirius

Now that you have your extension to the Sirius Properties DSL, you need to inform the Sirius bridge how to transform the concepts from your new widget with extensibility to the Sirius DSL without extends or overrides mechanism. For that an extension point is available in order to contribute an IDescriptionPreprocessor. This preprocessor will have to depend on the Sirius bridge. As a result, this code will have to go into the same plugin as the converter for best practices. We will thus contribute to the plugin named com.example.awesomeproject.sirius.ui.properties.ext.widgets.table. In this plugin, we will declare our description preprocessor using the following extension.

<?xml version="1.0" encoding="UTF-8"?>
<?eclipse version="3.4"?>
<plugin>
   <extension
         point="org.eclipse.sirius.ui.properties.descriptionPreprocessor">
      <descriptor
            class="com.example.awesomeproject.sirius.ui.properties.ext.widgets.table.internal.ExtTableDescriptionPreprocessor"
            description="%tableDescriptionPreprocessor.Description"
            id="com.example.awesomeproject.sirius.ui.properties.ext.widgets.table.descriptionPreprocessor"
            label="%tableDescriptionPreprocessor.Label">
      </descriptor>
   </extension>
</plugin>


The transformation from the Sirius DSL with extends/overrides to the Sirius DSL occur in two parts, first the description are converted using an IDescriptionPreprocessor and after an IDescriptionLinkResolver may be used to resolve some links. In our case, the description preprocessor will simply be used to transform a instance of ExtTableDescription with extends into a flatten ExtTableDescription without extends/overrides 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.IDescriptionPreprocessor;

public class ExtTableDescriptionPreprocessor extends PreconfiguredPreprocessor<ExtTableDescription> {

    @Override
    public boolean canHandle(EObject description) {
        return description instanceof ExtTableDescription;
    }
}


An IDescriptionPreprocessor can be used to convert any element from the Sirius Properties DSL with extends/overrides to a flatten Sirius DSL.

In order to help developer transform their Sirius entities into Sirius flatten entities, multiple possible super-classes are available:

Contribution of the Properties section for the odesign

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.

Update to the plugins of our basic widget approach

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.AbstractEEFWidgetController;
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;

	private EEFExtTableDescription description;

	private EditingContextAdapter contextAdapter;

	public TableController(EEFExtTableDescription description, IVariableManager variableManager, IInterpreter interpreter,
			EditingContextAdapter contextAdapter) {
		super(variableManager, interpreter);
		this.description = description;
		this.contextAdapter = 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.